Repository: teleporthq/teleport-code-generators
Pull request #289 (2019-06-26 07:57:57)

Fix/small updates in plugins

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

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

```diff
@@            Coverage Diff            @@
##           master    #289      +/-   ##
=========================================
- Coverage   72.45%   72.3%   -0.15%     
=========================================
  Files          86      86              
  Lines        2955    2979      +24     
  Branches      584     587       +3     
=========================================
+ Hits         2141    2154      +13     
- Misses        802     813      +11     
  Partials       12      12
```


| [Impacted Files](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/289?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [...kages/teleport-postprocessor-vue-file/src/index.ts](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/289/diff?src=pr&el=tree#diff-cGFja2FnZXMvdGVsZXBvcnQtcG9zdHByb2Nlc3Nvci12dWUtZmlsZS9zcmMvaW5kZXgudHM=) | `100% <ø> (ø)` | :arrow_up: |
| [...kages/teleport-plugin-react-proptypes/src/utils.ts](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/289/diff?src=pr&el=tree#diff-cGFja2FnZXMvdGVsZXBvcnQtcGx1Z2luLXJlYWN0LXByb3B0eXBlcy9zcmMvdXRpbHMudHM=) | `88.88% <100%> (+0.31%)` | :arrow_up: |
| [...es/teleport-plugin-vue-base-component/src/utils.ts](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/289/diff?src=pr&el=tree#diff-cGFja2FnZXMvdGVsZXBvcnQtcGx1Z2luLXZ1ZS1iYXNlLWNvbXBvbmVudC9zcmMvdXRpbHMudHM=) | `19.8% <20%> (+0.49%)` | :arrow_up: |
| [packages/teleport-shared/src/utils/uidl-utils.ts](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/289/diff?src=pr&el=tree#diff-cGFja2FnZXMvdGVsZXBvcnQtc2hhcmVkL3NyYy91dGlscy91aWRsLXV0aWxzLnRz) | `39.43% <27.77%> (-0.67%)` | :arrow_down: |
| [.../teleport-plugin-react-base-component/src/utils.ts](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/289/diff?src=pr&el=tree#diff-cGFja2FnZXMvdGVsZXBvcnQtcGx1Z2luLXJlYWN0LWJhc2UtY29tcG9uZW50L3NyYy91dGlscy50cw==) | `35.03% <33.33%> (+0.63%)` | :arrow_up: |
| [...teleport-component-generator/src/resolver/utils.ts](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/289/diff?src=pr&el=tree#diff-cGFja2FnZXMvdGVsZXBvcnQtY29tcG9uZW50LWdlbmVyYXRvci9zcmMvcmVzb2x2ZXIvdXRpbHMudHM=) | `66.66% <80%> (+0.54%)` | :arrow_up: |
| [...ges/teleport-uidl-validator/src/validator/utils.ts](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/289/diff?src=pr&el=tree#diff-cGFja2FnZXMvdGVsZXBvcnQtdWlkbC12YWxpZGF0b3Ivc3JjL3ZhbGlkYXRvci91dGlscy50cw==) | `97.87% <91.3%> (-2.13%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/289?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/289?src=pr&el=footer). Last update [ee3baaf...4cd174c](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/289?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 e99911e2..bd8f64f3 100644
--- a/examples/uidl-samples/project.json
+++ b/examples/uidl-samples/project.json
@@ -6,7 +6,24 @@
       "language": "en",
       "title": "Show-off for a few capabilities"
     },
-    "assets": [],
+    "assets": [
+      {
+        "type": "font",
+        "path": "https://fonts.googleapis.com/css?family=Roboto"
+      },
+      {
+        "type": "style",
+        "content": "body{font-family: 'Roboto', sans-serif; color: #2c3e50; -webkit-font-smoothing: antialiased; font-size: 16px;}"
+      },
+      {
+        "type": "icon",
+        "path": "/playground_assets/favicon-32x32.png",
+        "meta": {
+          "type": "image/png",
+          "sizes": "32x32"
+        }
+      }
+    ],
     "meta": []
   },
   "root": {
@@ -61,6 +78,153 @@
                           "type": "local"
                         }
                       }
+                    },
+                    {
+                      "type": "element",
+                      "content": {
+                        "elementType": "main",
+                        "style": {
+                          "width": "960px",
+                          "text-align": "center"
+                        },
+                        "children": [{
+                          "type": "element",
+                          "content": {
+                            "elementType": "image",
+                            "attrs": {
+                              "url": "http://lorempixel.com/960/200/",
+                              "alt": "Main banner on the home page"
+                            }
+                          }
+                        },{
+                          "type": "element",
+                          "content": {
+                            "style": {
+                              "margin": "30px auto"
+                            },
+                            "elementType": "heading1",
+                            "children": ["Welcome to the teleportHQ sample website!"]
+                          }
+                        }, {
+                          "type": "element",
+                          "content": {
+                            "elementType": "textblock",
+                            "style": {
+                              "color": "#6a8bad",
+                              "fontSize": "26px",
+                              "maxWidth": "560px",
+                              "margin": "30px auto",
+                              "text-align": "center"
+                            },
+                            "children": ["Building the infrastructure for the next generation of web and mobile interfaces"]
+                          }
+                        }, {
+                          "type": "element",
+                          "content": {
+                            "elementType": "textblock",
+                            "children": [
+                              {
+                                "type": "element",
+                                "content": {
+                                  "elementType": "link",
+                                  "style": {
+                                    "color": "white",
+                                    "backgroundColor": "#822cec",
+                                    "padding": "12px 24px",
+                                    "borderRadius": "4px",
+                                    "margin": "auto",
+                                    "text-decoration": "none"
+                                  },
+                                  "attrs": {
+                                    "url": "https://docs.teleporthq.io/guides/getting-started.html"
+                                  },
+                                  "children": ["Get Started →"]
+                                }
+                              }
+                            ]
+                          }
+                        },{
+                          "type": "element",
+                          "content": {
+                            "elementType": "container",
+                            "name": "columns",
+                            "style": {
+                              "display": "flex",
+                              "justify-content": "space-between",
+                              "margin-top": "50px",
+                              "border-top": "1px solid #ccc"
+                            },
+                            "children": [
+                              {
+                                "type": "element",
+                                "content": {
+                                  "elementType": "container",
+                                  "style": {
+                                    "max-width": "30%"
+                                  },
+                                  "children": [{
+                                    "type": "element",
+                                    "content": {
+                                      "elementType": "heading2",
+                                      "children": ["Platform Independent"]
+                                    }
+                                  },{
+                                    "type": "element",
+                                    "content": {
+                                      "elementType": "textblock",
+                                      "children": ["Using the same UI representation, you can build modern Web & Mobile applications."]
+                                    }
+                                  }]
+                                }
+                              },
+                              {
+                                "type": "element",
+                                "content": {
+                                  "elementType": "container",
+                                  "style": {
+                                    "max-width": "30%"
+                                  },
+                                  "children": [{
+                                    "type": "element",
+                                    "content": {
+                                      "elementType": "heading2",
+                                      "children": ["Plugable Architecture"]
+                                    }
+                                  },{
+                                    "type": "element",
+                                    "content": {
+                                      "elementType": "textblock",
+                                      "children": ["Our plugin system allows you to customize your exported code to the last detail."]
+                                    }
+                                  }]
+                                }
+                              },
+                              {
+                                "type": "element",
+                                "content": {
+                                  "elementType": "container",
+                                  "style": {
+                                    "max-width": "30%"
+                                  },
+                                  "children": [{
+                                    "type": "element",
+                                    "content": {
+                                      "elementType": "heading2",
+                                      "children": ["An Open Community"]
+                                    }
+                                  },{
+                                    "type": "element",
+                                    "content": {
+                                      "elementType": "textblock",
+                                      "children": ["We open-sourced our entire ecosystem and we invite everyone to contribute to it!"]
+                                    }
+                                  }]
+                                }
+                              }
+                            ]
+                          }
+                        }]
+                      }
                     }
                   ]
                 }
@@ -200,6 +364,10 @@
                             "border-bottom": "3px solid red",
                             "padding-bottom": "7px"
                           }
+                        },
+                        "& a": {
+                          "color": "inherit",
+                          "text-decoration": "none"
                         }
                       },
                       "children": [{
@@ -230,6 +398,10 @@
                             "border-bottom": "3px solid red",
                             "padding-bottom": "7px"
                           }
+                        },
+                        "& a": {
+                          "color": "inherit",
+                          "text-decoration": "none"
                         }
                       },
                       "children": [{
@@ -260,6 +432,10 @@
                             "border-bottom": "3px solid red",
                             "padding-bottom": "7px"
                           }
+                        },
+                        "& a": {
+                          "color": "inherit",
+                          "text-decoration": "none"
                         }
                       },
                       "children": [{
@@ -435,6 +611,7 @@
         "type": "element",
         "content": {
           "elementType": "container",
+          "name": "wrapper",
           "style": {
             "display": "flex",
             "flex-wrap": "wrap",
@@ -499,6 +676,7 @@
             "url": "/"
           },
           "style": {
+            "margin": "20px",
             "font-size": "18px"
           },
           "children": [
@@ -659,6 +837,172 @@
           ]
         }
       }
-    }  
+    },
+    "ExpandableArea": {
+      "name": "ExpandableArea",
+      "propDefinitions": {
+        "title": {
+          "type": "string",
+          "defaultValue": "This whole area is clickable!"
+        },
+        "expandableHeight": {
+          "type": "string",
+          "defaultValue": "100px"
+        }
+      },
+      "stateDefinitions": {
+        "isExpanded": {
+          "type": "boolean",
+          "defaultValue": false
+        }
+      },
+      "node": {
+        "type": "element",
+        "content": {
+          "elementType": "container",
+          "style": {
+            "border-bottom": "1px solid #ccc"
+          },
+          "children": [
+            {
+              "type": "element",
+              "content": {
+                "elementType": "container",
+                "name": "clickable-area",
+                "style": {
+                  "cursor": "pointer",
+                  "padding": "10px 0"
+                },
+                "events": {
+                  "click": [
+                    {
+                      "type": "stateChange",
+                      "modifies": "isExpanded",
+                      "newState": "$toggle"
+                    }
+                  ]
+                },
+                "children": [
+                  {
+                    "type": "element",
+                    "content": {
+                      "elementType": "text",
+                      "style": {
+                        "font-size": "20px",
+                        "font-weight": "600",
+                        "line-height": "1.5"
+                      },
+                      "children": [
+                        {
+                          "type": "dynamic",
+                          "content": {
+                            "referenceType": "prop",
+                            "id": "title"
+                          }
+                        }
+                      ]
+                    }
+                  },
+                  {
+                    "type": "element",
+                    "content": {
+                      "elementType": "text",
+                      "style": {
+                        "float": "right",
+                        "margin-top": "4px",
+                        "line-height": "1.5"
+                      },
+                      "name": "button",
+                      "children": [
+                        {
+                          "type": "element",
+                          "content": {
+                            "elementType": "svg",
+                            "attrs": {
+                              "width": "15",
+                              "height": "15",
+                              "viewBox": "0 0 15 15",
+                              "fill": "none"
+                            },
+                            "children": [
+                              {
+                                "type": "element",
+                                "content": {
+                                  "elementType": "line",
+                                  "attrs": {
+                                    "x1": "15",
+                                    "y1": "7.5",
+                                    "x2": "0",
+                                    "y2": "7.5",
+                                    "stroke": "#000"
+                                  }
+                                }
+                              },
+                              {
+                                "type": "conditional",
+                                "content": {
+                                  "node": {
+                                    "type": "element",
+                                    "content": {
+                                      "elementType": "line",
+                                      "attrs": {
+                                        "x1": "7.5",
+                                        "y1": "0",
+                                        "x2": "7.5",
+                                        "y2": "15",
+                                        "stroke": "#000"
+                                      }
+                                    }
+                                  },
+                                  "reference": {
+                                    "type": "dynamic",
+                                    "content": {
+                                      "referenceType": "state",
+                                      "id": "isExpanded"
+                                    }
+                                  },
+                                  "value": false
+                                }
+                              }
+                            ]
+                          }
+                        }
+                      ]
+                    }
+                  }
+                ]
+              }
+            },
+            {
+              "type": "element",
+              "content": {
+                "elementType": "container",
+                "name": "collapsible-area",
+                "attrs": {
+                  "data-is-expanded": "$state.isExpanded"
+                },
+                "style": {
+                  "height": "0",
+                  "overflow": "hidden",
+                  "transition": "height .2s ease-out",
+                  "&[data-is-expanded='true']": {
+                    "type": "nested-style",
+                    "content": {
+                      "height": "$props.expandableHeight"
+                    }
+                  }
+                },
+                "children": [
+                  {
+                    "type": "slot",
+                    "content": {}
+                  }
+                ]
+              }
+            }
+          ]
+        }
+      }
+    }
   }
 }
\ No newline at end of file
diff --git a/packages/teleport-component-generator/src/builder/index.ts b/packages/teleport-component-generator/src/builder/index.ts
index 76dfdf09..5329d5ed 100644
--- a/packages/teleport-component-generator/src/builder/index.ts
+++ b/packages/teleport-component-generator/src/builder/index.ts
@@ -30,7 +30,7 @@ export default class Builder {
         name: chunk.name,
         type: chunk.type,
         content: chunk.content,
-        linkAfter: [...chunk.linkAfter],
+        linkAfter: this.cleanupInvalidChunks(chunk.linkAfter, chunks),
       }
     })
 
@@ -82,4 +82,10 @@ export default class Builder {
 
     return this.generators[type](content)
   }
+
+  // remove invalid chunks (which did not end up being created) from the linkAfter fields
+  // one use-case is when you want to remove the import plugin
+  private cleanupInvalidChunks(linkAfter: string[], chunks: ChunkDefinition[]) {
+    return linkAfter.filter((chunkName) => chunks.some((chunk) => chunk.name === chunkName))
+  }
 }
diff --git a/packages/teleport-component-generator/src/html-mapping.json b/packages/teleport-component-generator/src/html-mapping.json
index eeab013f..878ae9b6 100644
--- a/packages/teleport-component-generator/src/html-mapping.json
+++ b/packages/teleport-component-generator/src/html-mapping.json
@@ -204,7 +204,7 @@
     "svg": {
       "elementType": "svg"
     },
-    "line": {
+    "separator": {
       "elementType": "hr"
     }
   },
diff --git a/packages/teleport-component-generator/src/index.ts b/packages/teleport-component-generator/src/index.ts
index e83510ca..2889458f 100644
--- a/packages/teleport-component-generator/src/index.ts
+++ b/packages/teleport-component-generator/src/index.ts
@@ -70,7 +70,7 @@ export const createComponentGenerator = (
       codeChunks = processor(codeChunks)
     })
 
-    const fileName = getComponentFileName(uidl)
+    const fileName = getComponentFileName(resolvedUIDL)
     const files = fileBundler(fileName, codeChunks)
 
     return {
diff --git a/packages/teleport-component-generator/src/resolver/utils.ts b/packages/teleport-component-generator/src/resolver/utils.ts
index afec9130..7bc81eb6 100644
--- a/packages/teleport-component-generator/src/resolver/utils.ts
+++ b/packages/teleport-component-generator/src/resolver/utils.ts
@@ -217,9 +217,9 @@ const resolveRepeat = (repeatContent: UIDLRepeatContent, parentNode: UIDLNode) =
     const nodeDataSourceAttr = dataSource.content.id
     const parentElement = parentNode.type === 'element' ? parentNode.content : null
 
-    if (parentElement) {
+    if (parentElement && parentElement.attrs) {
       repeatContent.dataSource = parentElement.attrs[nodeDataSourceAttr]
-      // remove original attribute so it doesn't get added as a static/dynamic value on the node
+      // remove original attribute so it is not added as a static/dynamic value on the node
       delete parentElement.attrs[nodeDataSourceAttr]
     }
   }
@@ -294,13 +294,18 @@ export const ensureDataSourceUniqueness = (node: UIDLNode) => {
   let index = 0
 
   traverseRepeats(node, (repeat) => {
-    if (repeat.dataSource.type === 'static' && !repeat.meta.dataSourceIdentifier) {
+    if (repeat.dataSource.type === 'static' && !customDataSourceIdentifierExists(repeat)) {
+      repeat.meta = repeat.meta || {}
       repeat.meta.dataSourceIdentifier = index === 0 ? 'items' : `items${index}`
       index += 1
     }
   })
 }
 
+const customDataSourceIdentifierExists = (repeat: UIDLRepeatContent) => {
+  return !!(repeat.meta && repeat.meta.dataSourceIdentifier)
+}
+
 /**
  * Prefixes all urls inside the style object with the assetsPrefix
  * @param style the style object on the current node
diff --git a/packages/teleport-plugin-react-base-component/src/utils.ts b/packages/teleport-plugin-react-base-component/src/utils.ts
index e7b01ac9..f9ae2101 100644
--- a/packages/teleport-plugin-react-base-component/src/utils.ts
+++ b/packages/teleport-plugin-react-base-component/src/utils.ts
@@ -6,7 +6,7 @@ import {
   addDynamicAttributeOnTag,
 } from '@teleporthq/teleport-shared/lib/utils/ast-jsx-utils'
 import { capitalize } from '@teleporthq/teleport-shared/lib/utils/string-utils'
-
+import { getRepeatIteratorNameAndKey } from '@teleporthq/teleport-shared/lib/utils/uidl-utils'
 import {
   UIDLPropDefinition,
   UIDLAttributeValue,
@@ -15,6 +15,7 @@ import {
   EventHandlerStatement,
   AttributeAssignCodeMod,
   UIDLConditionalExpression,
+  UIDLRepeatMeta,
 } from '@teleporthq/teleport-types'
 
 import { ERROR_LOG_NAME } from './constants'
@@ -110,11 +111,10 @@ export const addEventHandlerToTag = (
 export const createRepeatStructureWithMap = (
   dataSource: UIDLAttributeValue,
   content: types.JSXElement,
-  meta: Record<string, any> = {},
+  meta: UIDLRepeatMeta = {},
   t = types
 ) => {
-  const iteratorName = meta.iteratorName || 'item'
-  const keyIdentifier = meta.useIndex ? 'index' : iteratorName
+  const { iteratorName, iteratorKey } = getRepeatIteratorNameAndKey(meta)
 
   const source = getSourceIdentifier(dataSource)
 
@@ -122,7 +122,7 @@ export const createRepeatStructureWithMap = (
     type: 'dynamic',
     content: {
       referenceType: 'local',
-      id: keyIdentifier,
+      id: iteratorKey,
     },
   }
   addAttributeToNode(content, 'key', dynamicLocalReference)
diff --git a/packages/teleport-plugin-react-proptypes/src/utils.ts b/packages/teleport-plugin-react-proptypes/src/utils.ts
index db862f58..cdb0a5b8 100644
--- a/packages/teleport-plugin-react-proptypes/src/utils.ts
+++ b/packages/teleport-plugin-react-proptypes/src/utils.ts
@@ -55,12 +55,10 @@ export const buildTypesOfPropsAst = (
   const defaultValuesSearch = Object.keys(propDefinitions).reduce(
     (acc: any, key) => {
       const { type, isRequired } = propDefinitions[key]
+      const astProp = t.memberExpression(t.identifier(propTypesNames), t.identifier(type))
       const astValue = isRequired
-        ? t.memberExpression(
-            t.memberExpression(t.identifier(propTypesNames), t.identifier('type')),
-            t.identifier('isRequired')
-          )
-        : t.memberExpression(t.identifier(propTypesNames), t.identifier(type))
+        ? t.memberExpression(astProp, t.identifier('isRequired'))
+        : astProp
       acc.values[key] = new ParsedASTNode(astValue)
       acc.count++
       return acc
diff --git a/packages/teleport-plugin-vue-base-component/src/utils.ts b/packages/teleport-plugin-vue-base-component/src/utils.ts
index 04bef9ba..df110d52 100644
--- a/packages/teleport-plugin-vue-base-component/src/utils.ts
+++ b/packages/teleport-plugin-vue-base-component/src/utils.ts
@@ -2,7 +2,7 @@ import * as types from '@babel/types'
 
 import * as htmlUtils from '@teleporthq/teleport-shared/lib/utils/html-utils'
 import { createHTMLNode } from '@teleporthq/teleport-shared/lib/builders/html-builders'
-
+import { getRepeatIteratorNameAndKey } from '@teleporthq/teleport-shared/lib/utils/uidl-utils'
 import {
   objectToObjectExpression,
   convertValueToLiteral,
@@ -117,6 +117,11 @@ export const generateRepeatNode = (
 ) => {
   const { dataSource, node: repeatContent, meta = {} } = node.content
   const repeatContentTag = generateNodeSyntax(repeatContent, accumulators)
+  if (typeof repeatContentTag === 'string') {
+    throw new Error(
+      `${ERROR_LOG_NAME} generateRepeatNode received an invalid content ${repeatContentTag}`
+    )
+  }
 
   let dataObjectIdentifier = meta.dataSourceIdentifier || `items`
   if (dataSource.type === 'dynamic') {
@@ -125,18 +130,11 @@ export const generateRepeatNode = (
     accumulators.dataObject[dataObjectIdentifier] = dataSource.content
   }
 
-  const iteratorName = meta.iteratorName || 'item'
+  const { iteratorName, iteratorKey } = getRepeatIteratorNameAndKey(meta)
   const iterator = meta.useIndex ? `(${iteratorName}, index)` : iteratorName
-  const keyIdentifier = meta.useIndex ? 'index' : iteratorName
-
-  if (typeof repeatContentTag === 'string') {
-    throw new Error(
-      `${ERROR_LOG_NAME} generateRepeatNode received an invalid content ${repeatContentTag}`
-    )
-  }
 
   htmlUtils.addAttributeToNode(repeatContentTag, 'v-for', `${iterator} in ${dataObjectIdentifier}`)
-  htmlUtils.addAttributeToNode(repeatContentTag, ':key', `${keyIdentifier}`)
+  htmlUtils.addAttributeToNode(repeatContentTag, ':key', `${iteratorKey}`)
   return repeatContentTag
 }
 
diff --git a/packages/teleport-postprocessor-vue-file/src/index.ts b/packages/teleport-postprocessor-vue-file/src/index.ts
index 0dd52e3c..f80be4a5 100644
--- a/packages/teleport-postprocessor-vue-file/src/index.ts
+++ b/packages/teleport-postprocessor-vue-file/src/index.ts
@@ -49,7 +49,7 @@ ${jsCode}
 
   if (cssCode) {
     code += `
-<style>
+<style scoped>
 ${cssCode}
 </style>
 `
diff --git a/packages/teleport-project-generator/src/utils.ts b/packages/teleport-project-generator/src/utils.ts
index 20a2464d..3ab7dce4 100644
--- a/packages/teleport-project-generator/src/utils.ts
+++ b/packages/teleport-project-generator/src/utils.ts
@@ -141,7 +141,15 @@ export const injectFilesToPath = (
     folder = createFolderInPath(rootFolder, path)
   }
 
-  folder.files = folder.files.concat(files)
+  files.forEach((fileToInject) => {
+    const existingFile = findFileInFolder(fileToInject, folder)
+    if (existingFile) {
+      existingFile.content = fileToInject.content
+      existingFile.contentEncoding = fileToInject.contentEncoding
+    } else {
+      folder.files.push(fileToInject)
+    }
+  })
 }
 
 const createFolderInPath = (rootFolder: GeneratedFolder, folderPath: string[]): GeneratedFolder => {
@@ -182,3 +190,7 @@ const findSubFolderByName = (rootFolder: GeneratedFolder, folderName: string): G
     return folder.name === folderName
   })
 }
+
+const findFileInFolder = (file: GeneratedFile, folder: GeneratedFolder) => {
+  return folder.files.find((f) => f.name === file.name && f.fileType === file.fileType)
+}
diff --git a/packages/teleport-shared/src/utils/uidl-utils.ts b/packages/teleport-shared/src/utils/uidl-utils.ts
index c3ae494b..cdba9ae8 100644
--- a/packages/teleport-shared/src/utils/uidl-utils.ts
+++ b/packages/teleport-shared/src/utils/uidl-utils.ts
@@ -11,6 +11,7 @@ import {
   UIDLAttributeValue,
   UIDLDynamicReference,
   UIDLRepeatContent,
+  UIDLRepeatMeta,
 } from '@teleporthq/teleport-types'
 
 /**
@@ -75,6 +76,15 @@ export const getComponentFileName = (component: ComponentUIDL) => {
 export const getComponentPath = (component: ComponentUIDL) =>
   component.meta ? component.meta.path : []
 
+export const getRepeatIteratorNameAndKey = (meta: UIDLRepeatMeta = {}) => {
+  const iteratorName = meta.iteratorName || 'item'
+  const iteratorKey = meta.iteratorKey || (meta.useIndex ? 'index' : iteratorName)
+  return {
+    iteratorKey,
+    iteratorName,
+  }
+}
+
 export const prefixPlaygroundAssetsURL = (prefix: string, originalString: string | undefined) => {
   if (!originalString || !originalString.startsWith(ASSETS_IDENTIFIER)) {
     return originalString
@@ -101,8 +111,19 @@ export const traverseNodes = (
 
   switch (node.type) {
     case 'element':
-      if (node.content.children) {
-        node.content.children.forEach((child) => {
+      const { attrs, children, style } = node.content
+      if (attrs) {
+        Object.keys(attrs).forEach((attrKey) => {
+          traverseNodes(attrs[attrKey], fn, node)
+        })
+      }
+
+      if (style) {
+        traverseStyleObject(style, fn, node)
+      }
+
+      if (children) {
+        children.forEach((child) => {
           traverseNodes(child, fn, node)
         })
       }
@@ -135,6 +156,21 @@ export const traverseNodes = (
   }
 }
 
+const traverseStyleObject = (
+  style: UIDLStyleDefinitions,
+  fn: (node: UIDLNode, parentNode: UIDLNode) => void,
+  parent: UIDLNode
+) => {
+  Object.keys(style).forEach((styleKey) => {
+    const styleValue = style[styleKey]
+    if (styleValue.type === 'nested-style') {
+      traverseStyleObject(styleValue.content, fn, parent)
+    } else {
+      fn(styleValue, parent)
+    }
+  })
+}
+
 // Parses a node structure recursively and applies a function to each UIDLElement instance
 export const traverseElements = (node: UIDLNode, fn: (element: UIDLElement) => void) => {
   switch (node.type) {
@@ -206,7 +242,7 @@ export const traverseRepeats = (node: UIDLNode, fn: (element: UIDLRepeatContent)
 
     default:
       throw new Error(
-        `traverseElements was given an unsupported node type ${JSON.stringify(node, null, 2)}`
+        `traverseRepeats was given an unsupported node type ${JSON.stringify(node, null, 2)}`
       )
   }
 }
diff --git a/packages/teleport-types/src/uidl.ts b/packages/teleport-types/src/uidl.ts
index ac2540a4..e7d18e2a 100755
--- a/packages/teleport-types/src/uidl.ts
+++ b/packages/teleport-types/src/uidl.ts
@@ -92,11 +92,14 @@ export interface UIDLRepeatNode {
 export interface UIDLRepeatContent {
   node: UIDLNode
   dataSource: UIDLAttributeValue
-  meta?: {
-    useIndex?: boolean
-    iteratorName?: string
-    dataSourceIdentifier?: string
-  }
+  meta?: UIDLRepeatMeta
+}
+
+export interface UIDLRepeatMeta {
+  useIndex?: boolean
+  iteratorName?: string
+  dataSourceIdentifier?: string
+  iteratorKey?: string
 }
 
 export interface UIDLConditionalNode {
diff --git a/packages/teleport-uidl-validator/src/uidl-schemas/component.json b/packages/teleport-uidl-validator/src/uidl-schemas/component.json
index 34da2cdf..31df1e2a 100644
--- a/packages/teleport-uidl-validator/src/uidl-schemas/component.json
+++ b/packages/teleport-uidl-validator/src/uidl-schemas/component.json
@@ -266,7 +266,7 @@
       "additionalProperties": false,
       "properties": {
         "additionalProperties": false,
-        "elementType": {"type": "string"},
+        "elementType": {"type": "string", "pattern": "^([a-zA-Z0-9])*$"},
         "name": {"type": "string"},
         "key": {"type": "string"},
         "dependency": {"$ref": "#/definitions/componentDependency"},
diff --git a/packages/teleport-uidl-validator/src/validator/utils.ts b/packages/teleport-uidl-validator/src/validator/utils.ts
index c39910dc..c3bfb532 100644
--- a/packages/teleport-uidl-validator/src/validator/utils.ts
+++ b/packages/teleport-uidl-validator/src/validator/utils.ts
@@ -1,4 +1,8 @@
-import { traverseNodes, traverseElements } from '@teleporthq/teleport-shared/lib/utils/uidl-utils'
+import {
+  traverseNodes,
+  traverseRepeats,
+  traverseElements,
+} from '@teleporthq/teleport-shared/lib/utils/uidl-utils'
 
 import { ProjectUIDL, UIDLElement, ComponentUIDL } from '@teleporthq/teleport-types'
 
@@ -22,33 +26,40 @@ export const checkForDuplicateDefinitions = (input: ComponentUIDL) => {
 export const checkForLocalVariables = (input: ComponentUIDL) => {
   const errors = []
 
-  traverseNodes(input.node, (node) => {
-    if (node.type === 'repeat') {
-      traverseNodes(node, (childNode) => {
-        if (childNode.type === 'dynamic' && childNode.content.referenceType === 'local') {
-          if (
-            childNode.content.id &&
-            node.content.meta.iteratorName &&
-            childNode.content.id !== node.content.meta.iteratorName
-          ) {
-            const errorMsg = `\n"${
-              childNode.content.id
-            }" is used in the "repeat" structure but the iterator name has this value: "${
-              node.content.meta.iteratorName
-            }"`
-            errors.push(errorMsg)
-          }
-          if (childNode.content.id && !node.content.meta.useIndex) {
+  traverseRepeats(input.node, (repeatContent) => {
+    traverseNodes(repeatContent.node, (childNode) => {
+      if (childNode.type === 'dynamic' && childNode.content.referenceType === 'local') {
+        if (childNode.content.id === 'index') {
+          if (!repeatContent.meta.useIndex) {
             const errorMsg = `\nIndex variable is used but the "useIndex" meta information is false.`
             errors.push(errorMsg)
           }
+
+          // we are dealing with local index here
+          return
         }
-      })
-    }
+
+        if (!validLocalVariableUsage(childNode.content.id, repeatContent.meta.iteratorName)) {
+          const errorMsg = `\n"${childNode.content.id}" is used in the "repeat" structure but the iterator name has this value: "${repeatContent.meta.iteratorName}"`
+          errors.push(errorMsg)
+        }
+      }
+    })
   })
   return errors
 }
 
+const validLocalVariableUsage = (dynamicId: string, repeatIteratorName: string) => {
+  const iteratorName = repeatIteratorName || 'item'
+
+  if (!dynamicId.includes('.')) {
+    return dynamicId === iteratorName
+  }
+
+  const dynamicIdRoot = dynamicId.split('.')[0]
+  return dynamicIdRoot === iteratorName
+}
+
 // All referenced props and states should be previously defined in the
 // "propDefinitions" and "stateDefinitions" sections
 // If props or states are defined and not used, a warning witll be displayed
@@ -62,20 +73,16 @@ export const checkDynamicDefinitions = (input: any) => {
 
   traverseNodes(input.node, (node) => {
     if (node.type === 'dynamic' && node.content.referenceType === 'prop') {
-      if (!propKeys.includes(node.content.id)) {
-        const errorMsg = `"${
-          node.content.id
-        }" is used but not defined. Please add it in propDefinitions`
+      if (!dynamicPathExistsInDefinitions(node.content.id, propKeys)) {
+        const errorMsg = `"${node.content.id}" is used but not defined. Please add it in propDefinitions`
         errors.push(errorMsg)
       }
       usedPropKeys.push(node.content.id)
     }
 
     if (node.type === 'dynamic' && node.content.referenceType === 'state') {
-      if (!stateKeys.includes(node.content.id)) {
-        const errorMsg = `\n"${
-          node.content.id
-        }" is used but not defined. Please add it in stateDefinitions`
+      if (!dynamicPathExistsInDefinitions(node.content.id, stateKeys)) {
+        const errorMsg = `\n"${node.content.id}" is used but not defined. Please add it in stateDefinitions`
         errors.push(errorMsg)
       }
       usedstateKeys.push(node.content.id)
@@ -97,6 +104,18 @@ export const checkDynamicDefinitions = (input: any) => {
   return errors
 }
 
+const dynamicPathExistsInDefinitions = (path: string, defKeys: string[]) => {
+  if (!path.includes('.')) {
+    // prop/state is a scalar value, not a dot notation
+    return defKeys.includes(path)
+  }
+
+  // TODO: Expand validation logic to check if the path exists on the prop/state definition
+  // ex: if prop reference is `user.name`, we should check that prop type is object and has a valid field name
+  const rootIdentifier = path.split('.')[0]
+  return defKeys.includes(rootIdentifier)
+}
+
 // A projectUIDL must contain "route" key
 export const checkRouteDefinition = (input: ProjectUIDL) => {
   const errors = []
@@ -121,9 +140,7 @@ export const checkComponentExistence = (input: ProjectUIDL) => {
       element.dependency.type === 'local' &&
       !components.includes(element.elementType)
     ) {
-      const errorMsg = `\nThe component "${
-        element.elementType
-      }" is not defined in the UIDL's component section.`
+      const errorMsg = `\nThe component "${element.elementType}" is not defined in the UIDL's component section.`
       errors.push(errorMsg)
     }
   })
@@ -159,9 +176,7 @@ export const checkRootComponent = (input: ProjectUIDL) => {
   const rootNode = input.root.node.content as UIDLElement
   rootNode.children.forEach((child) => {
     if (child.type !== 'conditional') {
-      const errorMsg = `\nRoot Node contains elements of type "${
-        child.type
-      }". It should contain only elements of type "conditional"`
+      const errorMsg = `\nRoot Node contains elements of type "${child.type}". It should contain only elements of type "conditional"`
       errors.push(errorMsg)
     } else {
       routeNaming.push(child.content.value)
@@ -171,9 +186,7 @@ export const checkRootComponent = (input: ProjectUIDL) => {
   input.root.stateDefinitions.route.values
     .filter((route) => !routeNaming.includes(route.value))
     .forEach((route) => {
-      const errorMsg = `\nRoot Node contains routes that don't have corresponding components. Check the "value" for the following routes: ${
-        route.meta.path
-      }.`
+      const errorMsg = `\nRoot Node contains routes that don't have corresponding components. Check the "value" for the following routes: ${route.meta.path}.`
       errors.push(errorMsg)
     })
 


Test patch:
diff --git a/packages/teleport-postprocessor-vue-file/__tests__/index.ts b/packages/teleport-postprocessor-vue-file/__tests__/index.ts
index 2f056dc6..ed7427c2 100644
--- a/packages/teleport-postprocessor-vue-file/__tests__/index.ts
+++ b/packages/teleport-postprocessor-vue-file/__tests__/index.ts
@@ -24,7 +24,7 @@ describe('vue file concat', () => {
 export default {}
 </script>
 
-<style>
+<style scoped>
 .button { margin: 10px; }
 </style>
 `)
diff --git a/packages/teleport-project-generator/__tests__/mocks.ts b/packages/teleport-project-generator/__tests__/mocks.ts
index 1239f227..4f266a62 100644
--- a/packages/teleport-project-generator/__tests__/mocks.ts
+++ b/packages/teleport-project-generator/__tests__/mocks.ts
@@ -1,4 +1,4 @@
-import { Mapping, CompiledComponent } from '@teleporthq/teleport-types'
+import { Mapping, CompiledComponent, GeneratedFolder } from '@teleporthq/teleport-types'
 import { ProjectStrategy } from '../src/types'
 
 export const mockMapping: Mapping = {
@@ -101,3 +101,30 @@ export const secondStrategy: ProjectStrategy = {
     path: ['test', 'static'],
   },
 }
+
+export const emptyFolder = (name: string = 'test'): GeneratedFolder => {
+  return {
+    name,
+    files: [],
+    subFolders: [],
+  }
+}
+
+export const folderWithFiles = (name: string = 'test'): GeneratedFolder => {
+  return {
+    name,
+    files: [
+      {
+        name: 'index',
+        fileType: 'js',
+        content: 'var x = 0;',
+      },
+      {
+        name: 'index',
+        fileType: 'css',
+        content: 'h1 { margin: 10px; }',
+      },
+    ],
+    subFolders: [],
+  }
+}
diff --git a/packages/teleport-project-generator/__tests__/utils.ts b/packages/teleport-project-generator/__tests__/utils.ts
index 7412b7e5..44d70947 100644
--- a/packages/teleport-project-generator/__tests__/utils.ts
+++ b/packages/teleport-project-generator/__tests__/utils.ts
@@ -1,4 +1,5 @@
-import { generateLocalDependenciesPrefix } from '../src/utils'
+import { generateLocalDependenciesPrefix, injectFilesToPath } from '../src/utils'
+import { emptyFolder, folderWithFiles } from './mocks'
 
 describe('generateLocalDependenciesPrefix', () => {
   it('works when there is a common parent', () => {
@@ -36,3 +37,101 @@ describe('generateLocalDependenciesPrefix', () => {
     expect(generateLocalDependenciesPrefix(from, to)).toBe('./')
   })
 })
+
+describe('injectFilesToPath', () => {
+  it('adds a file in an empty folder', () => {
+    const folder = emptyFolder()
+    const newFile = {
+      name: 'test',
+      fileType: 'js',
+      content: 'random',
+    }
+
+    injectFilesToPath(folder, [], [newFile])
+
+    expect(folder.files.length).toBe(1)
+    expect(folder.files[0].name).toBe('test')
+    expect(folder.files[0].fileType).toBe('js')
+    expect(folder.files[0].content).toBe('random')
+  })
+
+  it('adds a file in a folder with existing files', () => {
+    const folder = folderWithFiles()
+    const newFile = {
+      name: 'test',
+      fileType: 'js',
+      content: 'random',
+    }
+
+    injectFilesToPath(folder, [], [newFile])
+
+    expect(folder.files.length).toBe(3)
+    expect(folder.files[2].name).toBe('test')
+    expect(folder.files[2].fileType).toBe('js')
+    expect(folder.files[2].content).toBe('random')
+  })
+
+  it('overrides existing files', () => {
+    const folder = folderWithFiles()
+    const newFiles = [
+      {
+        name: 'index',
+        fileType: 'js',
+        content: 'new-content',
+      },
+      {
+        name: 'index',
+        fileType: 'html',
+        content: '<html>',
+      },
+    ]
+
+    injectFilesToPath(folder, [], newFiles)
+
+    expect(folder.files.length).toBe(3)
+    expect(folder.files[0].name).toBe('index')
+    expect(folder.files[0].fileType).toBe('js')
+    expect(folder.files[0].content).toBe('new-content')
+    expect(folder.files[1].name).toBe('index')
+    expect(folder.files[1].fileType).toBe('css')
+    expect(folder.files[1].content).toBe('h1 { margin: 10px; }')
+    expect(folder.files[2].name).toBe('index')
+    expect(folder.files[2].fileType).toBe('html')
+    expect(folder.files[2].content).toBe('<html>')
+  })
+
+  it('adds the files in a newly created subfolder', () => {
+    const folder = folderWithFiles()
+    const newFiles = [
+      {
+        name: 'index',
+        fileType: 'js',
+        content: 'new-content',
+      },
+      {
+        name: 'index',
+        fileType: 'html',
+        content: '<html>',
+      },
+    ]
+
+    injectFilesToPath(folder, ['subfolder'], newFiles)
+
+    expect(folder.files.length).toBe(2)
+    expect(folder.files[0].name).toBe('index')
+    expect(folder.files[0].fileType).toBe('js')
+    expect(folder.files[0].content).toBe('var x = 0;')
+    expect(folder.files[1].name).toBe('index')
+    expect(folder.files[1].fileType).toBe('css')
+    expect(folder.files[1].content).toBe('h1 { margin: 10px; }')
+
+    expect(folder.subFolders[0].name).toBe('subfolder')
+    expect(folder.subFolders[0].files.length).toBe(2)
+    expect(folder.subFolders[0].files[0].name).toBe('index')
+    expect(folder.subFolders[0].files[0].fileType).toBe('js')
+    expect(folder.subFolders[0].files[0].content).toBe('new-content')
+    expect(folder.subFolders[0].files[1].name).toBe('index')
+    expect(folder.subFolders[0].files[1].fileType).toBe('html')
+    expect(folder.subFolders[0].files[1].content).toBe('<html>')
+  })
+})
diff --git a/packages/teleport-shared/__tests__/utils/uidl-utils.ts b/packages/teleport-shared/__tests__/utils/uidl-utils.ts
index 8a144528..ab2f3805 100644
--- a/packages/teleport-shared/__tests__/utils/uidl-utils.ts
+++ b/packages/teleport-shared/__tests__/utils/uidl-utils.ts
@@ -6,6 +6,7 @@ import {
   findFirstElementNode,
   getComponentPath,
   getComponentFileName,
+  getRepeatIteratorNameAndKey,
 } from '../../src/utils/uidl-utils'
 import { component, staticNode } from '../../src/builders/uidl-builders'
 import {
@@ -366,3 +367,54 @@ describe('getComponentPath', () => {
     expect(path.length).toBe(2)
   })
 })
+
+describe('getRepeatIteratorNameAndKey', () => {
+  it('returns the fallback as name and key', () => {
+    const { iteratorName, iteratorKey } = getRepeatIteratorNameAndKey()
+    expect(iteratorName).toBe('item')
+    expect(iteratorKey).toBe('item')
+  })
+
+  it('returns the fallback as name and index as key', () => {
+    const { iteratorName, iteratorKey } = getRepeatIteratorNameAndKey({
+      useIndex: true,
+    })
+    expect(iteratorName).toBe('item')
+    expect(iteratorKey).toBe('index')
+  })
+
+  it('returns the iteratorName as name and as key', () => {
+    const { iteratorName, iteratorKey } = getRepeatIteratorNameAndKey({
+      iteratorName: 'listItem',
+    })
+    expect(iteratorName).toBe('listItem')
+    expect(iteratorKey).toBe('listItem')
+  })
+
+  it('returns the iteratorName as name and index as key', () => {
+    const { iteratorName, iteratorKey } = getRepeatIteratorNameAndKey({
+      iteratorName: 'listItem',
+      useIndex: true,
+    })
+    expect(iteratorName).toBe('listItem')
+    expect(iteratorKey).toBe('index')
+  })
+
+  it('returns the iteratorName as name and iteratorKey as key', () => {
+    const { iteratorName, iteratorKey } = getRepeatIteratorNameAndKey({
+      iteratorName: 'listItem',
+      iteratorKey: 'listItem.id',
+      useIndex: true,
+    })
+    expect(iteratorName).toBe('listItem')
+    expect(iteratorKey).toBe('listItem.id')
+  })
+
+  it('returns the fallback iterator name as name and iteratorKey as key', () => {
+    const { iteratorName, iteratorKey } = getRepeatIteratorNameAndKey({
+      iteratorKey: 'item.id',
+    })
+    expect(iteratorName).toBe('item')
+    expect(iteratorKey).toBe('item.id')
+  })
+})
diff --git a/packages/teleport-uidl-validator/__tests__/validator/component-invalid-sample.json b/packages/teleport-uidl-validator/__tests__/validator/component-invalid-sample.json
index 63f02d91..57f433dd 100644
--- a/packages/teleport-uidl-validator/__tests__/validator/component-invalid-sample.json
+++ b/packages/teleport-uidl-validator/__tests__/validator/component-invalid-sample.json
@@ -77,6 +77,13 @@
                       "id": "item"
                     }
                   },
+                  {
+                    "type": "dynamic",
+                    "content": {
+                      "referenceType": "local",
+                      "id": "index"
+                    }
+                  },
                   {
                     "type": "element",
                     "content": {