Repository: teleporthq/teleport-code-generators
Pull request #379 (2019-10-02 13:43:20)

feat: string blacklists on mappings and regex validation for some strings in the UIDL
This fixes lots of potential issues and edge cases with the input UIDL

codecov[bot]: # [Codecov](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/379?src=pr&el=h1) Report
> Merging [#379](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/379?src=pr&el=desc) into [master](https://codecov.io/gh/teleporthq/teleport-code-generators/commit/3564dd97d9aff2cdff139c453f291704501b80de?src=pr&el=desc) will **decrease** coverage by `0.22%`.
> The diff coverage is `63.46%`.

[![Impacted file tree graph](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/379/graphs/tree.svg?width=650&token=LI2WPEFm1R&height=150&src=pr)](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/379?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##           master    #379      +/-   ##
=========================================
- Coverage   81.62%   81.4%   -0.23%     
=========================================
  Files         131     131              
  Lines        4148    4174      +26     
  Branches      883     895      +12     
=========================================
+ Hits         3386    3398      +12     
- Misses        716     728      +12     
- Partials       46      48       +2
```


| [Impacted Files](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/379?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/teleport-shared/src/utils/uidl-utils.ts](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/379/diff?src=pr&el=tree#diff-cGFja2FnZXMvdGVsZXBvcnQtc2hhcmVkL3NyYy91dGlscy91aWRsLXV0aWxzLnRz) | `72.47% <20%> (-2.65%)` | :arrow_down: |
| [...ort-shared/src/node-handlers/node-to-html/index.ts](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/379/diff?src=pr&el=tree#diff-cGFja2FnZXMvdGVsZXBvcnQtc2hhcmVkL3NyYy9ub2RlLWhhbmRsZXJzL25vZGUtdG8taHRtbC9pbmRleC50cw==) | `73.61% <60%> (+0.37%)` | :arrow_up: |
| [...port-shared/src/node-handlers/node-to-jsx/index.ts](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/379/diff?src=pr&el=tree#diff-cGFja2FnZXMvdGVsZXBvcnQtc2hhcmVkL3NyYy9ub2RlLWhhbmRsZXJzL25vZGUtdG8tanN4L2luZGV4LnRz) | `88.42% <66.66%> (+0.12%)` | :arrow_up: |
| [packages/teleport-uidl-resolver/src/utils.ts](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/379/diff?src=pr&el=tree#diff-cGFja2FnZXMvdGVsZXBvcnQtdWlkbC1yZXNvbHZlci9zcmMvdXRpbHMudHM=) | `60.46% <70%> (+0.77%)` | :arrow_up: |
| [packages/teleport-uidl-resolver/src/resolver.ts](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/379/diff?src=pr&el=tree#diff-cGFja2FnZXMvdGVsZXBvcnQtdWlkbC1yZXNvbHZlci9zcmMvcmVzb2x2ZXIudHM=) | `93.93% <90.9%> (-0.66%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/379?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/379?src=pr&el=footer). Last update [3564dd9...27cf307](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/379?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/examples/uidl-samples/project.json b/examples/uidl-samples/project.json
index b1e0a038..b350fbe8 100644
--- a/examples/uidl-samples/project.json
+++ b/examples/uidl-samples/project.json
@@ -874,7 +874,7 @@
     "ExpandableArea": {
       "name": "ExpandableArea",
       "propDefinitions": {
-        "title": {
+        "header": {
           "type": "string",
           "defaultValue": "This whole area is clickable!"
         },
@@ -930,7 +930,7 @@
                           "type": "dynamic",
                           "content": {
                             "referenceType": "prop",
-                            "id": "title"
+                            "id": "header"
                           }
                         }
                       ]
diff --git a/packages/teleport-component-generator-react/src/react-mapping.json b/packages/teleport-component-generator-react/src/react-mapping.json
index 44b6e449..b2d14214 100644
--- a/packages/teleport-component-generator-react/src/react-mapping.json
+++ b/packages/teleport-component-generator-react/src/react-mapping.json
@@ -35,5 +35,6 @@
     "readonly": "readOnly",
     "class": "className",
     "contenteditable": "contentEditable"
-  } 
+  },
+  "illegalClassNames": ["React", "Fragment", "ReactDOM", "PropTypes"]
 }
\ No newline at end of file
diff --git a/packages/teleport-component-generator-stencil/src/stencil-mapping.json b/packages/teleport-component-generator-stencil/src/stencil-mapping.json
index 8963b8ee..c56206eb 100644
--- a/packages/teleport-component-generator-stencil/src/stencil-mapping.json
+++ b/packages/teleport-component-generator-stencil/src/stencil-mapping.json
@@ -25,5 +25,7 @@
     "readonly": "readOnly",
     "class": "className",
     "contenteditable": "contentEditable"
-  }
+  },
+  "illegalClassNames": ["Prop", "State"],
+  "illegalPropNames": ["title"]
 }
\ No newline at end of file
diff --git a/packages/teleport-shared/src/node-handlers/node-to-html/index.ts b/packages/teleport-shared/src/node-handlers/node-to-html/index.ts
index e5ea77ad..d09b9fe3 100644
--- a/packages/teleport-shared/src/node-handlers/node-to-html/index.ts
+++ b/packages/teleport-shared/src/node-handlers/node-to-html/index.ts
@@ -16,13 +16,14 @@ import { DEFAULT_TEMPLATE_SYNTAX } from './constants'
 const generateElementNode: NodeToHTML<UIDLElementNode, HastNode> = (node, params, syntax) => {
   const templateSyntax = { ...DEFAULT_TEMPLATE_SYNTAX, ...syntax }
   const { dependencies, templateLookup } = params
-  const { elementType, name, key, children, attrs, dependency, events } = node.content
+  const { elementType, name, key, children, attrs, dependency, events, selfClosing } = node.content
+  const tagName = elementType || 'component'
   const htmlNode = dependency
-    ? createHTMLNode(templateSyntax.customElementTagName(elementType))
-    : createHTMLNode(elementType)
+    ? createHTMLNode(templateSyntax.customElementTagName(tagName))
+    : createHTMLNode(tagName)
 
   if (dependency && templateSyntax.dependencyHandling === 'import') {
-    dependencies[elementType] = { ...dependency }
+    dependencies[tagName] = { ...dependency }
   }
 
   if (attrs) {
@@ -38,7 +39,7 @@ const generateElementNode: NodeToHTML<UIDLElementNode, HastNode> = (node, params
     )
   }
 
-  if (children) {
+  if (!selfClosing && children) {
     children.forEach((child) => {
       const childTag = generateNode(child, params, templateSyntax)
 
diff --git a/packages/teleport-shared/src/node-handlers/node-to-jsx/index.ts b/packages/teleport-shared/src/node-handlers/node-to-jsx/index.ts
index a76d62bd..2ec540e8 100644
--- a/packages/teleport-shared/src/node-handlers/node-to-jsx/index.ts
+++ b/packages/teleport-shared/src/node-handlers/node-to-jsx/index.ts
@@ -37,13 +37,14 @@ const generateElementNode: NodeToJSX<UIDLElementNode, types.JSXElement> = (
 ) => {
   const options = { ...DEFAULT_JSX_OPTIONS, ...jsxOptions }
   const { dependencies, nodesLookup } = params
-  const { elementType, children, key, attrs, dependency, events } = node.content
+  const { elementType, selfClosing, children, key, attrs, dependency, events } = node.content
 
+  const tagName = elementType || 'component'
   const elementName =
     dependency && dependency.type === 'local' && options.customElementTag
-      ? options.customElementTag(elementType)
-      : elementType
-  const elementTag = children ? createJSXTag(elementName) : createSelfClosingJSXTag(elementName)
+      ? options.customElementTag(tagName)
+      : tagName
+  const elementTag = selfClosing ? createSelfClosingJSXTag(elementName) : createJSXTag(elementName)
 
   if (attrs) {
     Object.keys(attrs).forEach((attrKey) => {
@@ -74,7 +75,7 @@ const generateElementNode: NodeToJSX<UIDLElementNode, types.JSXElement> = (
   if (dependency) {
     if (options.dependencyHandling === 'import') {
       // Make a copy to avoid reference leaking
-      dependencies[elementType] = { ...dependency }
+      dependencies[tagName] = { ...dependency }
     }
   }
 
@@ -84,7 +85,7 @@ const generateElementNode: NodeToJSX<UIDLElementNode, types.JSXElement> = (
     })
   }
 
-  if (children) {
+  if (!selfClosing && children) {
     children.forEach((child) => {
       const childTag = generateNode(child, params, options)
 
diff --git a/packages/teleport-shared/src/utils/uidl-utils.ts b/packages/teleport-shared/src/utils/uidl-utils.ts
index ad628b88..33c4367f 100644
--- a/packages/teleport-shared/src/utils/uidl-utils.ts
+++ b/packages/teleport-shared/src/utils/uidl-utils.ts
@@ -1,5 +1,9 @@
 import { ASSETS_IDENTIFIER } from '../constants'
-import { camelCaseToDashCase } from './string-utils'
+import {
+  camelCaseToDashCase,
+  removeIllegalCharacters,
+  dashCaseToUpperCamelCase,
+} from './string-utils'
 import {
   ComponentUIDL,
   UIDLStyleDefinitions,
@@ -24,6 +28,23 @@ export const extractRoutes = (rootComponent: ComponentUIDL) => {
   ) as UIDLConditionalNode[]
 }
 
+export const setFriendlyOutputOptions = (uidl: ComponentUIDL) => {
+  uidl.outputOptions = uidl.outputOptions || {}
+  const friendlyName = removeIllegalCharacters(uidl.name)
+  if (!uidl.outputOptions.fileName) {
+    uidl.outputOptions.fileName = camelCaseToDashCase(friendlyName)
+  }
+  if (!uidl.outputOptions.componentClassName) {
+    uidl.outputOptions.componentClassName = dashCaseToUpperCamelCase(friendlyName)
+  }
+
+  // failsafe for invalid UIDL samples with illegal characters as element names
+  // when used in projects, resolveLocalDependencies should handle this
+  traverseElements(uidl.node, (element) => {
+    element.elementType = removeIllegalCharacters(element.elementType)
+  })
+}
+
 export const getComponentFileName = (component: ComponentUIDL) => {
   return component.outputOptions && component.outputOptions.fileName
     ? component.outputOptions.fileName
diff --git a/packages/teleport-types/src/uidl.ts b/packages/teleport-types/src/uidl.ts
index d5e66f4a..444ed265 100755
--- a/packages/teleport-types/src/uidl.ts
+++ b/packages/teleport-types/src/uidl.ts
@@ -154,6 +154,7 @@ export interface UIDLElement {
   elementType: string
   name?: string
   key?: string // internal usage
+  selfClosing?: boolean
   dependency?: UIDLDependency
   style?: UIDLStyleDefinitions
   attrs?: Record<string, UIDLAttributeValue>
@@ -212,4 +213,6 @@ export interface Mapping {
   elements?: Record<string, UIDLElement>
   events?: Record<string, string>
   attributes?: Record<string, string>
+  illegalClassNames?: string[]
+  illegalPropNames?: string[]
 }
diff --git a/packages/teleport-uidl-resolver/src/html-mapping.json b/packages/teleport-uidl-resolver/src/html-mapping.json
index 60ee3704..43610cec 100644
--- a/packages/teleport-uidl-resolver/src/html-mapping.json
+++ b/packages/teleport-uidl-resolver/src/html-mapping.json
@@ -64,37 +64,43 @@
       "elementType": "img",
       "attrs": {
         "src": { "type": "dynamic", "content": { "referenceType": "attr", "id": "url" } }
-      }
+      },
+      "selfClosing": true
     },
     "textinput": {
       "elementType": "input",
       "attrs": {
         "type": { "type": "static", "content": "text" }
-      }
+      },
+      "selfClosing": true
     },
     "passwordinput": {
       "elementType": "input",
       "attrs": {
         "type":  { "type":"static", "content": "password"}
-      }
+      },
+      "selfClosing": true
     },
     "numberinput": {
       "elementType": "input",
       "attrs": {
         "type": { "type": "static", "content": "number" }
-      }
+      },
+      "selfClosing": true
     },
     "checkbox": {
       "elementType": "input",
       "attrs": {
         "type": { "type": "static", "content": "checkbox" }
-      }
+      },
+      "selfClosing": true
     },
     "radiobutton": {
       "elementType": "input",
       "attrs": {
         "type": { "type": "static", "content": "radio" }
-      }
+      },
+      "selfClosing": true
     },
     "textarea": {
       "elementType": "textarea"
@@ -199,7 +205,8 @@
       "elementType": "source",
       "attrs": {
         "src": { "type": "dynamic", "content": { "referenceType": "attr", "id": "url" } }
-      }
+      },
+      "selfClosing": true
     },
     "svg": {
       "elementType": "svg"
@@ -209,5 +216,7 @@
     }
   },
   "events": {},
-  "attributes": {}
+  "attributes": {},
+  "illegalClassNames": ["Component"],
+  "illegalPropNames": ["this", "prop", "props", "state", "window", "document"]
 }
\ No newline at end of file
diff --git a/packages/teleport-uidl-resolver/src/resolver.ts b/packages/teleport-uidl-resolver/src/resolver.ts
index 76f29f10..91c96a8c 100644
--- a/packages/teleport-uidl-resolver/src/resolver.ts
+++ b/packages/teleport-uidl-resolver/src/resolver.ts
@@ -1,5 +1,5 @@
 import * as utils from './utils'
-import { UIDLUtils, StringUtils } from '@teleporthq/teleport-shared'
+import { UIDLUtils } from '@teleporthq/teleport-shared'
 import { ComponentUIDL, UIDLElement, Mapping, GeneratorOptions } from '@teleporthq/teleport-types'
 
 /**
@@ -10,6 +10,9 @@ export default class Resolver {
   private mapping: Mapping = {
     elements: {},
     events: {},
+    attributes: {},
+    illegalClassNames: [],
+    illegalPropNames: [],
   }
 
   constructor(mapping?: Mapping | Mapping[]) {
@@ -24,44 +27,35 @@ export default class Resolver {
     this.mapping = utils.mergeMappings(this.mapping, mapping)
   }
 
-  public resolveUIDL(uidl: ComponentUIDL, options: GeneratorOptions = {}) {
+  public resolveUIDL(input: ComponentUIDL, options: GeneratorOptions = {}) {
     const mapping = utils.mergeMappings(this.mapping, options.mapping)
     const newOptions = {
       ...options,
       mapping,
     }
 
-    const node = UIDLUtils.cloneObject(uidl.node)
+    const uidl = UIDLUtils.cloneObject(input)
 
-    uidl.outputOptions = uidl.outputOptions || {}
-    const friendlyName = StringUtils.removeIllegalCharacters(uidl.name)
-    if (!uidl.outputOptions.fileName) {
-      uidl.outputOptions.fileName = StringUtils.camelCaseToDashCase(friendlyName)
-    }
+    UIDLUtils.setFriendlyOutputOptions(uidl)
 
-    if (!uidl.outputOptions.componentClassName) {
-      uidl.outputOptions.componentClassName = StringUtils.dashCaseToUpperCamelCase(friendlyName)
-    }
+    utils.checkForIllegalNames(uidl, mapping)
 
     if (options.projectRouteDefinition) {
-      utils.resolveNavlinks(node, options.projectRouteDefinition)
+      utils.resolveNavlinks(uidl.node, options.projectRouteDefinition)
     }
 
-    utils.resolveNode(node, newOptions)
+    utils.resolveNode(uidl.node, newOptions)
 
     const nodesLookup = {}
-    utils.createNodesLookup(node, nodesLookup)
-    utils.generateUniqueKeys(node, nodesLookup)
+    utils.createNodesLookup(uidl.node, nodesLookup)
+    utils.generateUniqueKeys(uidl.node, nodesLookup)
 
-    utils.ensureDataSourceUniqueness(node)
+    utils.ensureDataSourceUniqueness(uidl.node)
 
     // There might be urls that need to be prefixed in the metaTags of the component
     utils.resolveMetaTags(uidl, options)
 
-    return {
-      ...uidl,
-      node,
-    }
+    return uidl
   }
 
   public resolveElement(element: UIDLElement, options: GeneratorOptions = {}) {
diff --git a/packages/teleport-uidl-resolver/src/utils.ts b/packages/teleport-uidl-resolver/src/utils.ts
index 5e8da0e8..07e46420 100644
--- a/packages/teleport-uidl-resolver/src/utils.ts
+++ b/packages/teleport-uidl-resolver/src/utils.ts
@@ -18,7 +18,11 @@ const STYLE_PROPERTIES_WITH_URL = ['background', 'backgroundImage']
 
 type ElementsLookup = Record<string, { count: number; nextKey: string }>
 
-export const mergeMappings = (oldMapping: Mapping, newMapping?: Mapping, deepMerge = false) => {
+export const mergeMappings = (
+  oldMapping: Mapping,
+  newMapping?: Mapping,
+  deepMerge = false
+): Mapping => {
   if (!newMapping) {
     return oldMapping
   }
@@ -31,6 +35,14 @@ export const mergeMappings = (oldMapping: Mapping, newMapping?: Mapping, deepMer
     elements: { ...oldMapping.elements, ...newMapping.elements },
     events: { ...oldMapping.events, ...newMapping.events },
     attributes: { ...oldMapping.attributes, ...newMapping.attributes },
+    illegalClassNames: [
+      ...(oldMapping.illegalClassNames || []),
+      ...(newMapping.illegalClassNames || []),
+    ],
+    illegalPropNames: [
+      ...(oldMapping.illegalPropNames || []),
+      ...(newMapping.illegalPropNames || []),
+    ],
   }
 }
 
@@ -124,6 +136,10 @@ export const resolveElement = (element: UIDLElement, options: GeneratorOptions)
   // Mapping the type according to the elements mapping
   originalElement.elementType = mappedElement.elementType
 
+  if (mappedElement.selfClosing) {
+    originalElement.selfClosing = mappedElement.selfClosing
+  }
+
   // Resolve dependency with the UIDL having priority
   if (originalElement.dependency || mappedElement.dependency) {
     originalElement.dependency = resolveDependency(
@@ -465,3 +481,26 @@ const resolveEvents = (events: UIDLEventDefinitions, eventsMapping: Record<strin
 
   return resultedEvents
 }
+
+export const checkForIllegalNames = (uidl: ComponentUIDL, mapping: Mapping) => {
+  const { illegalClassNames, illegalPropNames } = mapping
+  if (illegalClassNames.includes(uidl.outputOptions.componentClassName)) {
+    throw new Error(`Illegal component name '${uidl.outputOptions.componentClassName}'`)
+  }
+
+  if (uidl.propDefinitions) {
+    Object.keys(uidl.propDefinitions).forEach((prop) => {
+      if (illegalPropNames.includes(prop)) {
+        throw new Error(`Illegal prop key '${prop}'`)
+      }
+    })
+  }
+
+  if (uidl.stateDefinitions) {
+    Object.keys(uidl.stateDefinitions).forEach((state) => {
+      if (illegalPropNames.includes(state)) {
+        throw new Error(`Illegal state key '${state}'`)
+      }
+    })
+  }
+}
diff --git a/packages/teleport-uidl-validator/src/uidl-schemas/component.json b/packages/teleport-uidl-validator/src/uidl-schemas/component.json
index a6ea38c0..8986e87a 100644
--- a/packages/teleport-uidl-validator/src/uidl-schemas/component.json
+++ b/packages/teleport-uidl-validator/src/uidl-schemas/component.json
@@ -23,7 +23,18 @@
         "content": { "$ref": "#/definitions/elementNodeContent" }
       }
     },
-    "outputOptions": {"type": "object"},
+    "outputOptions": {
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "componentClassName": { "type": "string", "pattern": "^[A-Z]+[a-zA-Z0-9]*$" },
+        "fileName": { "type": "string", "pattern": "^[a-zA-Z0-9-_.]*$" },
+        "styleFileName": { "type": "string", "pattern": "^[a-zA-Z0-9-_.]*$" },
+        "templateFileName": { "type": "string", "pattern": "^[a-zA-Z0-9-_.]*$" },
+        "moduleName": { "type": "string", "pattern": "^[a-zA-Z0-9-_.]*$" },
+        "folderPath": { "type": "array", "items": { "type": "string", "pattern": "^[a-zA-Z0-9-_.]*$" } }
+      }
+    },
     "stateDefinitions":  { "$ref": "#/definitions/stateDefinitions" },
     "propDefinitions": { "$ref": "#/definitions/propDefinitions" }
   },
@@ -85,9 +96,9 @@
                     "type": "object",
                     "additionalProperties": false,
                     "properties": {
-                      "componentName": { "type": "string" },
-                      "navLink": { "type": "string" },
-                      "fileName": { "type": "string" }
+                      "componentName": { "type": "string", "pattern": "^[A-Z]+[a-zA-Z0-9]*$" },
+                      "navLink": { "type": "string", "pattern": "^\/[a-zA-Z0-9-_]*$" },
+                      "fileName": { "type": "string", "pattern": "^[a-zA-Z0-9-_.]*$" }
                     }
                   },
                   "seo": {
@@ -284,7 +295,7 @@
       "properties": {
         "additionalProperties": false,
         "elementType": {"type": "string" },
-        "name": {"type": "string"},
+        "name": {"type": "string", "pattern": "^[a-zA-Z]+[a-zA-Z0-9-_]*$"},
         "key": {"type": "string"},
         "dependency": {"$ref": "#/definitions/componentDependency"},
         "style": {"$ref": "#/definitions/styleDefinitions"},
diff --git a/scripts/watcher.js b/scripts/watcher.js
index 6fa8345b..7545caed 100644
--- a/scripts/watcher.js
+++ b/scripts/watcher.js
@@ -8,7 +8,7 @@ const ignorePackages = [
   'teleport-test'
 ]
 
-const watcher = chokidar.watch('packages/**/src/**/*.ts', { 
+const watcher = chokidar.watch(['packages/**/src/**/*.ts', 'packages/**/src/**/*.json'], { 
   persistent: true ,
   usePolling: true,
   interval: 500


Test patch:
diff --git a/examples/test-samples/component-invalid-sample.json b/examples/test-samples/component-invalid-sample.json
index c8b298a0..88e9a3fd 100644
--- a/examples/test-samples/component-invalid-sample.json
+++ b/examples/test-samples/component-invalid-sample.json
@@ -1,7 +1,7 @@
 {
   "name": "New UIDL",
   "propDefinitions": {
-    "title": {
+    "header": {
       "type": "string",
       "defaultValue": "Hello"
     },
@@ -30,7 +30,7 @@
           "type": "dynamic",
           "content": {
             "referenceType": "prop",
-            "id": "title"
+            "id": "header"
           }
         }
       },
@@ -48,7 +48,7 @@
                 "type": "dynamic",
                 "content": {
                   "referenceType": "prop",
-                  "id": "title"
+                  "id": "header"
                 }
               }
             ]
diff --git a/examples/test-samples/component-sample.json b/examples/test-samples/component-sample.json
index 5b374348..f5ae7cd4 100644
--- a/examples/test-samples/component-sample.json
+++ b/examples/test-samples/component-sample.json
@@ -1,7 +1,7 @@
 {
   "name": "New UIDL",
   "propDefinitions": {
-    "title": {
+    "header": {
       "type": "string",
       "defaultValue": "Hello"
     },
@@ -29,7 +29,7 @@
           "type": "dynamic",
           "content": {
             "referenceType": "prop",
-            "id": "title"
+            "id": "header"
           }
         }
       },
@@ -47,7 +47,7 @@
                 "type": "dynamic",
                 "content": {
                   "referenceType": "prop",
-                  "id": "title"
+                  "id": "header"
                 }
               },
               {
diff --git a/examples/test-samples/project-invalid-sample.json b/examples/test-samples/project-invalid-sample.json
index 2dab06e1..e3044342 100644
--- a/examples/test-samples/project-invalid-sample.json
+++ b/examples/test-samples/project-invalid-sample.json
@@ -192,7 +192,7 @@
     "OneComponent": {
       "name": "OneComponent",
       "propDefinitions": {
-        "title": {
+        "header": {
           "type": "string",
           "defaultValue": "Hello"
         },
@@ -220,7 +220,7 @@
               "type": "dynamic",
               "content": {
                 "referenceType": "prop",
-                "id": "title"
+                "id": "header"
               }
             }
           },
@@ -238,7 +238,7 @@
                     "type": "dynamic",
                     "content": {
                       "referenceType": "prop",
-                      "id": "title"
+                      "id": "header"
                     }
                   }
                 ]
diff --git a/examples/test-samples/project-sample.json b/examples/test-samples/project-sample.json
index 35f58426..9110bb93 100644
--- a/examples/test-samples/project-sample.json
+++ b/examples/test-samples/project-sample.json
@@ -195,7 +195,7 @@
     "OneComponent": {
       "name": "OneComponent",
       "propDefinitions": {
-        "title": {
+        "titleValue": {
           "type": "string",
           "defaultValue": "Hello"
         },
@@ -223,7 +223,7 @@
               "type": "dynamic",
               "content": {
                 "referenceType": "prop",
-                "id": "title"
+                "id": "titleValue"
               }
             }
           },
@@ -241,7 +241,7 @@
                     "type": "dynamic",
                     "content": {
                       "referenceType": "prop",
-                      "id": "title"
+                      "id": "titleValue"
                     }
                   }
                 ]
@@ -320,7 +320,7 @@
     "ExpandableArea": {
       "name": "ExpandableArea",
       "propDefinitions": {
-        "title": {
+        "titleValue": {
           "type": "string",
           "defaultValue": "Hello"
         },
@@ -351,7 +351,7 @@
                   "type": "dynamic",
                   "content": {
                     "referenceType": "prop",
-                    "id": "title"
+                    "id": "titleValue"
                   }
                 }]
               }
diff --git a/packages/teleport-shared/__tests__/node-handlers/node-to-html/index.ts b/packages/teleport-shared/__tests__/node-handlers/node-to-html/index.ts
index 905a61c4..589e0cf8 100644
--- a/packages/teleport-shared/__tests__/node-handlers/node-to-html/index.ts
+++ b/packages/teleport-shared/__tests__/node-handlers/node-to-html/index.ts
@@ -24,7 +24,7 @@ describe('generateHTMLTemplateSyntax', () => {
       expect(hastElement.children[0].type).toBe('element')
 
       const firstChild = hastElement.children[0] as HastNode
-      expect((firstChild.children[1] as HastText).value).toBe('{{ title }}')
+      expect((firstChild.children[1] as HastText).value).toBe('{{ header }}')
 
       const secondChild = hastElement.children[1] as HastNode
       expect(secondChild.properties['v-for']).toBe('(item, index) in items')
@@ -47,7 +47,7 @@ describe('generateHTMLTemplateSyntax', () => {
       expect(hastElement.children[0].type).toBe('element')
 
       const firstChild = hastElement.children[0] as HastNode
-      expect((firstChild.children[1] as HastText).value).toBe('[title]')
+      expect((firstChild.children[1] as HastText).value).toBe('[header]')
 
       const secondChild = hastElement.children[1] as HastNode
       expect(secondChild.properties['test-repeat']).toBe('(item, index) in items')
diff --git a/packages/teleport-uidl-resolver/__tests__/mapping.json b/packages/teleport-uidl-resolver/__tests__/mapping.json
index 1ac005f0..8f6b151f 100644
--- a/packages/teleport-uidl-resolver/__tests__/mapping.json
+++ b/packages/teleport-uidl-resolver/__tests__/mapping.json
@@ -2,6 +2,10 @@
   "elements": {
     "text": {
       "elementType": "span"
+    },
+    "image": {
+      "elementType": "img",
+      "selfClosing": true
     }
   }
 }
\ No newline at end of file
diff --git a/packages/teleport-uidl-resolver/__tests__/resolver.ts b/packages/teleport-uidl-resolver/__tests__/resolver.ts
index 0119fde8..7cbcf811 100644
--- a/packages/teleport-uidl-resolver/__tests__/resolver.ts
+++ b/packages/teleport-uidl-resolver/__tests__/resolver.ts
@@ -33,6 +33,17 @@ describe('resolveElement', () => {
     expect(resolvedElement.elementType).toBe('span')
     expect(resolvedElement.attrs.dummy.content).toBe('remains here')
   })
+
+  it('maps a seflClosing tag', () => {
+    const resolver = new Resolver()
+    const imageElement = element('image', {
+      dummy: staticNode('remains here'),
+    })
+    const resolvedElement = resolver.resolveElement(imageElement, { mapping })
+    expect(resolvedElement.elementType).toBe('img')
+    expect(resolvedElement.selfClosing).toBe(true)
+    expect(resolvedElement.attrs.dummy.content).toBe('remains here')
+  })
 })
 
 describe('resolveUIDL', () => {
diff --git a/packages/teleport-uidl-resolver/__tests__/utils.ts b/packages/teleport-uidl-resolver/__tests__/utils.ts
index 1e1eadd1..a367f232 100644
--- a/packages/teleport-uidl-resolver/__tests__/utils.ts
+++ b/packages/teleport-uidl-resolver/__tests__/utils.ts
@@ -288,7 +288,7 @@ describe('resolveNavlinks', () => {
 })
 
 describe('mergeMappings', () => {
-  const oldMapping = {
+  const oldMapping: Mapping = {
     elements: {
       text: {
         elementType: 'span',
@@ -300,6 +300,8 @@ describe('mergeMappings', () => {
     },
     events: {},
     attributes: {},
+    illegalClassNames: [],
+    illegalPropNames: ['title'],
   }
 
   const newMapping = {
@@ -327,7 +329,7 @@ describe('mergeMappings', () => {
   it('merges the mappings using deepmerge if deepMerge parameter is present', () => {
     const mergedMapping = mergeMappings(oldMapping as Mapping, newMapping as Mapping, true)
 
-    const expectedMapping = {
+    const expectedMapping: Mapping = {
       elements: {
         text: {
           elementType: 'span',
@@ -342,6 +344,8 @@ describe('mergeMappings', () => {
       },
       events: {},
       attributes: {},
+      illegalClassNames: [],
+      illegalPropNames: ['title'],
     }
 
     expect(mergedMapping).toEqual(expectedMapping)
@@ -350,7 +354,7 @@ describe('mergeMappings', () => {
   it('merges the mapping using the spread operator ', () => {
     const mergedMapping = mergeMappings(oldMapping as Mapping, newMapping as Mapping)
 
-    const expectedMapping = {
+    const expectedMapping: Mapping = {
       elements: {
         text: {
           elementType: 'span',
@@ -364,6 +368,8 @@ describe('mergeMappings', () => {
       },
       events: {},
       attributes: {},
+      illegalClassNames: [],
+      illegalPropNames: ['title'],
     }
 
     expect(mergedMapping).toEqual(expectedMapping)