Repository: teleporthq/teleport-code-generators (ts)

Issue:
Revisit PropTypes handling
PropTypes are not too flexible at the moment
- [ ] each prop requires a default value to be displayed
- [x] we don't have the ability to set more complex default values (ex: object, array)
- [x] children props don't have a clear definition/standard (also check slots in vue)

Default props are not being generated in case of only boolean prop is present
When our UIDL have only a single PropDefinition which is a boolean, the ``defaultProps`` and ``propTypes`` are not generating.

UIDL to replicate

~~~
{
  "$schema": "https://raw.githubusercontent.com/teleporthq/teleport-code-generators/master/packages/teleport-generator-core/src/uidl-schemas/component.json",
  "name": "SimpleComponent",
  "propDefinitions": {
    "test": {
      "type": "boolean",
      "defaultValue": true
    }
  },
  "node": {
    "type": "element",
    "content": {
      "elementType": "container",
      "children": ["Hello"]
    }
  }
}
~~~

Pull request description:
feat(plugin-react-proptypes): Added isRequired attribute for proptypes

Removed generation of defaultProps if the component doesn't have any defaultValue, for any of the props. And supporting ``isRequired`` attribute for ``propTypes`` definition

fixes #213 fixes #12

Patch:
diff --git a/packages/teleport-generator-core/src/uidl-schemas/component.json b/packages/teleport-generator-core/src/uidl-schemas/component.json
index 85def68d..62546134 100644
--- a/packages/teleport-generator-core/src/uidl-schemas/component.json
+++ b/packages/teleport-generator-core/src/uidl-schemas/component.json
@@ -72,6 +72,9 @@
                 }
               ]
             },
+            "isRequired": {
+              "type": "boolean"
+            },
             "meta": {"type": "object"}
           }
         }
diff --git a/packages/teleport-generator-shared/src/typings/uidl.ts b/packages/teleport-generator-shared/src/typings/uidl.ts
index 66e847f7..86f60f81 100755
--- a/packages/teleport-generator-shared/src/typings/uidl.ts
+++ b/packages/teleport-generator-shared/src/typings/uidl.ts
@@ -34,6 +34,7 @@ export interface ComponentUIDL {
 export interface UIDLPropDefinition {
   type: string
   defaultValue?: string | number | boolean | any[] | object | (() => void)
+  isRequired?: boolean
   meta?: Record<string, any>
 }
 
diff --git a/packages/teleport-plugin-react-proptypes/src/index.ts b/packages/teleport-plugin-react-proptypes/src/index.ts
index 61bdd098..697e46de 100644
--- a/packages/teleport-plugin-react-proptypes/src/index.ts
+++ b/packages/teleport-plugin-react-proptypes/src/index.ts
@@ -37,10 +37,13 @@ export const createPlugin: ComponentPluginFactory<ReactJSPropTypesConfig> = (con
     }
 
     // TODO used the name from the mappings of the component, not from the UIDL
-    const defaultPropsAst = buildDefaultPropsAst(name, uidl.propDefinitions)
+    const hasDefaultProps = Object.keys(uidl.propDefinitions).some(
+      (prop) => typeof uidl.propDefinitions[prop].defaultValue !== 'undefined'
+    )
+
     const typesOfPropsAst = buildTypesOfPropsAst(name, 'PropTypes', uidl.propDefinitions)
 
-    if (!defaultPropsAst && !typesOfPropsAst) {
+    if (!hasDefaultProps && !typesOfPropsAst) {
       return structure
     }
 
@@ -50,12 +53,16 @@ export const createPlugin: ComponentPluginFactory<ReactJSPropTypesConfig> = (con
       version: '15.7.2',
     }
 
-    chunks.push({
-      type: 'js',
-      name: defaultPropsChunkName,
-      linkAfter: [componentChunkName],
-      content: defaultPropsAst,
-    })
+    if (hasDefaultProps) {
+      const defaultPropsAst = buildDefaultPropsAst(name, uidl.propDefinitions)
+      chunks.push({
+        type: 'js',
+        name: defaultPropsChunkName,
+        linkAfter: [componentChunkName],
+        content: defaultPropsAst,
+      })
+      exportChunk.linkAfter.push(defaultPropsChunkName)
+    }
 
     chunks.push({
       type: 'js',
@@ -65,7 +72,7 @@ export const createPlugin: ComponentPluginFactory<ReactJSPropTypesConfig> = (con
     })
 
     // push export of component after declarations of types
-    exportChunk.linkAfter.push(typesOfPropsChunkName, defaultPropsChunkName)
+    exportChunk.linkAfter.push(typesOfPropsChunkName)
 
     return structure
   }
diff --git a/packages/teleport-plugin-react-proptypes/src/utils.ts b/packages/teleport-plugin-react-proptypes/src/utils.ts
index 216a049a..986abe62 100644
--- a/packages/teleport-plugin-react-proptypes/src/utils.ts
+++ b/packages/teleport-plugin-react-proptypes/src/utils.ts
@@ -17,7 +17,7 @@ export const buildDefaultPropsAst = (
   const defaultValuesSearch = Object.keys(propDefinitions).reduce(
     (acc: any, key) => {
       const { defaultValue } = propDefinitions[key]
-      if (defaultValue) {
+      if (typeof defaultValue !== 'undefined') {
         acc.values[key] = defaultValue
         acc.count++
       }
@@ -54,12 +54,15 @@ export const buildTypesOfPropsAst = (
 
   const defaultValuesSearch = Object.keys(propDefinitions).reduce(
     (acc: any, key) => {
-      const { defaultValue, type } = propDefinitions[key]
-      if (defaultValue) {
-        const astValue = t.memberExpression(t.identifier(propTypesNames), t.identifier(type))
-        acc.values[key] = new ParsedASTNode(astValue)
-        acc.count++
-      }
+      const { type, isRequired } = propDefinitions[key]
+      const astValue = isRequired
+        ? t.memberExpression(
+            t.memberExpression(t.identifier(propTypesNames), t.identifier('type')),
+            t.identifier('isRequired')
+          )
+        : t.memberExpression(t.identifier(propTypesNames), t.identifier(type))
+      acc.values[key] = new ParsedASTNode(astValue)
+      acc.count++
       return acc
     },
     { values: {}, count: 0 }
diff --git a/packages/teleport-plugin-vue-base-component/src/utils.ts b/packages/teleport-plugin-vue-base-component/src/utils.ts
index 344043f3..9fae2d54 100644
--- a/packages/teleport-plugin-vue-base-component/src/utils.ts
+++ b/packages/teleport-plugin-vue-base-component/src/utils.ts
@@ -289,7 +289,7 @@ export const generateVueComponentJS = (
 const createVuePropsDefinition = (uidlPropDefinitions: Record<string, UIDLPropDefinition>) => {
   return Object.keys(uidlPropDefinitions).reduce((acc: { [key: string]: any }, name) => {
     let mappedType
-    const { type, defaultValue } = uidlPropDefinitions[name]
+    const { type, defaultValue, isRequired } = uidlPropDefinitions[name]
     switch (type) {
       case 'string':
         mappedType = String
@@ -318,7 +318,10 @@ const createVuePropsDefinition = (uidlPropDefinitions: Record<string, UIDLPropDe
         )
     }
 
-    acc[name] = defaultValue ? { type: mappedType, default: defaultValue } : mappedType
+    acc[name] =
+      typeof defaultValue !== 'undefined' ? { type: mappedType, default: defaultValue } : mappedType
+    acc[name] = isRequired ? { required: isRequired, ...acc[name] } : acc[name]
+
     return acc
   }, {})
 }


Test patch:
diff --git a/packages/teleport-component-generator-react/__tests__/integration/component-non-element-root.ts b/packages/teleport-component-generator-react/__tests__/integration/component-non-element-root.ts
index 697575f9..ff3ba2d7 100644
--- a/packages/teleport-component-generator-react/__tests__/integration/component-non-element-root.ts
+++ b/packages/teleport-component-generator-react/__tests__/integration/component-non-element-root.ts
@@ -52,7 +52,7 @@ describe('React Component Generator support for non elements as root', () => {
     expect(result.files.length).toBeTruthy()
   })
 
-  it('should support conditional string as root node', async () => {
+  it('should support conditional as root node', async () => {
     const result = await generator.generateComponent(ComponentWithConditionalRootStringNode)
     const jsFile = findFileByType(result.files, JS_FILE)
 
diff --git a/packages/teleport-plugin-react-proptypes/__tests__/index.ts b/packages/teleport-plugin-react-proptypes/__tests__/index.ts
new file mode 100644
index 00000000..5604a506
--- /dev/null
+++ b/packages/teleport-plugin-react-proptypes/__tests__/index.ts
@@ -0,0 +1,133 @@
+import { createPlugin } from '../src'
+import {
+  component,
+  elementNode,
+} from '@teleporthq/teleport-generator-shared/lib/builders/uidl-builders'
+import { ComponentStructure } from '@teleporthq/teleport-generator-shared/lib/typings/generators'
+
+describe('Should add Default Props and PropTypes to the component Generation', () => {
+  const plugin = createPlugin()
+  const reactChunk = {
+    type: 'js',
+    name: 'react-component',
+    meta: {},
+    content: {},
+    linkAfter: [],
+  }
+  const exportChunk = {
+    type: 'js',
+    name: 'export',
+    content: {},
+    linkAfter: ['react-component'],
+  }
+
+  it('Should throw error when the chunk is supplied', async () => {
+    const uidlSample = component('SimpleComponent', elementNode('container'))
+    const structure: ComponentStructure = {
+      uidl: uidlSample,
+      chunks: [],
+      dependencies: {},
+    }
+    try {
+      await plugin(structure)
+    } catch (e) {
+      expect(e.message).toContain('React component chunk with name')
+    }
+  })
+
+  it('Should generate chunks, defaultProps and propTypes', async () => {
+    const props = {
+      test: {
+        type: 'boolean',
+        defualtValue: 'true',
+      },
+      name: {
+        type: 'string',
+        defaultValue: 'Teleport',
+      },
+    }
+    // @ts-ignore-next-line
+    const uidlSample = component('SimpleComponent', elementNode('container'), props)
+    const structure: ComponentStructure = {
+      uidl: uidlSample,
+      chunks: [reactChunk, exportChunk],
+      dependencies: {},
+    }
+    const result = await plugin(structure)
+
+    const defaultProps = result.chunks.filter(
+      (chunk) => chunk.name === 'react-component-default-props'
+    )
+    const propTypes = result.chunks.filter(
+      (chunk) => chunk.name === 'react-component-types-of-props'
+    )
+
+    expect(defaultProps.length).toEqual(1)
+    expect(defaultProps[0].type).toBe('js')
+    expect(propTypes.length).toEqual(1)
+    expect(propTypes[0].type).toBe('js')
+  })
+
+  it('Should not generate defaultProps', async () => {
+    const props = {
+      test: {
+        type: 'boolean',
+      },
+      name: {
+        type: 'string',
+      },
+    }
+    // @ts-ignore-next-line
+    const uidlSample = component('SimpleComponent', elementNode('container'), props)
+    const structure: ComponentStructure = {
+      uidl: uidlSample,
+      chunks: [reactChunk, exportChunk],
+      dependencies: {},
+    }
+    const result = await plugin(structure)
+
+    const defaultProps = result.chunks.filter(
+      (chunk) => chunk.name === 'react-component-default-props'
+    )
+    const propTypes = result.chunks.filter(
+      (chunk) => chunk.name === 'react-component-types-of-props'
+    )
+
+    expect(defaultProps.length).toEqual(0)
+    expect(propTypes.length).toEqual(1)
+    expect(propTypes[0].type).toBe('js')
+  })
+
+  it('Should generate chunks after specifying required to props', async () => {
+    const props = {
+      test: {
+        type: 'boolean',
+        isRequired: true,
+      },
+      name: {
+        type: 'string',
+        isRequired: true,
+      },
+    }
+    // @ts-ignore-next-line
+    const uidlSample = component('SimpleComponent', elementNode('container'), props)
+    const structure: ComponentStructure = {
+      uidl: uidlSample,
+      chunks: [reactChunk, exportChunk],
+      dependencies: {},
+    }
+    const result = await plugin(structure)
+
+    const defaultProps = result.chunks.filter(
+      (chunk) => chunk.name === 'react-component-default-props'
+    )
+
+    const propTypes = result.chunks.filter(
+      (chunk) => chunk.name === 'react-component-types-of-props'
+    )
+
+    expect(defaultProps.length).toEqual(0)
+    expect(propTypes.length).toEqual(1)
+    expect(propTypes[0].type).toBe('js')
+  })
+})