From 82718070e01bc6eab82a430780588033f4a28c33 Mon Sep 17 00:00:00 2001 From: TudorCe <101194278+TudorCe@users.noreply.github.com> Date: Tue, 26 Nov 2024 13:04:48 +0200 Subject: [PATCH] Add support for new prop types (#947) * Add support for object and array prop ref path nesting, add support for function props * Try another build * Fix tests and small bug --- examples/uidl-samples/component.json | 12 ++-- examples/uidl-samples/tests.json | 9 ++- .../integration/company-with-object-prop.ts | 4 +- .../integration/component-with-object-prop.ts | 2 +- .../integration/component-with-object-prop.ts | 4 +- .../node-handlers/node-to-jsx/utils.ts | 4 +- .../src/node-handlers/node-to-jsx/utils.ts | 37 +++++++---- .../src/utils/ast-utils.ts | 5 +- .../src/node-handlers.ts | 64 ++++++++++++++----- .../src/utils.ts | 18 +++++- .../teleport-project-generator/src/index.ts | 1 + .../teleport-shared/src/utils/uidl-utils.ts | 21 +++++- packages/teleport-types/src/uidl.ts | 16 ++++- .../src/component-builders.ts | 7 +- .../src/decoders/utils.ts | 2 +- .../src/parser/index.ts | 8 ++- 16 files changed, 163 insertions(+), 51 deletions(-) diff --git a/examples/uidl-samples/component.json b/examples/uidl-samples/component.json index 67dd07980..a01fc87b4 100644 --- a/examples/uidl-samples/component.json +++ b/examples/uidl-samples/component.json @@ -103,14 +103,16 @@ "type": "dynamic", "content": { "referenceType": "prop", - "id": "company.name" + "id": "company", + "refPath": ["name"] } }, { "type": "dynamic", "content": { "referenceType": "prop", - "id": "company.location.city" + "id": "company", + "refPath": ["location", "city"] } }, { @@ -122,14 +124,16 @@ "type": "dynamic", "content": { "referenceType": "prop", - "id": "config.height" + "id": "config", + "refPath": ["height"] } }, "width": { "type": "dynamic", "content": { "referenceType": "prop", - "id": "config.width" + "id": "config", + "refPath": ["width"] } } }, diff --git a/examples/uidl-samples/tests.json b/examples/uidl-samples/tests.json index 1474aa2f0..9763691f0 100644 --- a/examples/uidl-samples/tests.json +++ b/examples/uidl-samples/tests.json @@ -5386,21 +5386,24 @@ "type": "dynamic", "content": { "referenceType": "prop", - "id": "fields.Title" + "id": "fields", + "refPath": ["Title"] } }, { "type": "dynamic", "content": { "referenceType": "prop", - "id": "company.name" + "id": "company", + "refPath": ["name"] } }, { "type": "dynamic", "content": { "referenceType": "prop", - "id": "company.location" + "id": "company", + "refPath": ["location"] } } ] diff --git a/packages/teleport-component-generator-angular/__tests__/integration/company-with-object-prop.ts b/packages/teleport-component-generator-angular/__tests__/integration/company-with-object-prop.ts index 73965edf0..59c887759 100644 --- a/packages/teleport-component-generator-angular/__tests__/integration/company-with-object-prop.ts +++ b/packages/teleport-component-generator-angular/__tests__/integration/company-with-object-prop.ts @@ -16,6 +16,8 @@ describe('declares a propDefinitions with type object and use it', () => { city: 'Cluj', }, }`) - expect(htmlFile?.content).toContain('{{ company.name }}{{ company.location.city }}') + expect(htmlFile?.content).toContain( + "{{ company?.['name'] }}{{ company?.['location']?.['city'] }}" + ) }) }) diff --git a/packages/teleport-component-generator-react/__tests__/integration/component-with-object-prop.ts b/packages/teleport-component-generator-react/__tests__/integration/component-with-object-prop.ts index 9fcb49241..e6fcf499c 100644 --- a/packages/teleport-component-generator-react/__tests__/integration/component-with-object-prop.ts +++ b/packages/teleport-component-generator-react/__tests__/integration/component-with-object-prop.ts @@ -10,7 +10,7 @@ describe('declares a propDefinitions with type object and use it', () => { expect(jsFile).toBeDefined() expect(jsFile?.content).toContain('config: PropTypes.object') - expect(jsFile?.content).toContain('height: props.config.height') + expect(jsFile?.content).toContain("height: props.config?.['height']") expect(jsFile?.content).toContain(`config: { height: 30, width: 30, diff --git a/packages/teleport-component-generator-vue/__tests__/integration/component-with-object-prop.ts b/packages/teleport-component-generator-vue/__tests__/integration/component-with-object-prop.ts index 868364706..ef518a7e0 100644 --- a/packages/teleport-component-generator-vue/__tests__/integration/component-with-object-prop.ts +++ b/packages/teleport-component-generator-vue/__tests__/integration/component-with-object-prop.ts @@ -8,7 +8,9 @@ describe('declares a propDefinitions with type object and use it', () => { const result = await generator.generateComponent(componentUIDL) const vueFile = result.files.find((file) => file.fileType === FileType.VUE) - expect(vueFile?.content).toContain('{{ company.name }}{{ company.location.city }}') + expect(vueFile?.content).toContain( + "{{ company?.['name'] }}{{ company?.['location']?.['city'] }}" + ) expect(vueFile?.content).toContain(`company: { type: Object, default: () => ({ diff --git a/packages/teleport-plugin-common/__tests__/node-handlers/node-to-jsx/utils.ts b/packages/teleport-plugin-common/__tests__/node-handlers/node-to-jsx/utils.ts index 699390d7d..cad8a308b 100644 --- a/packages/teleport-plugin-common/__tests__/node-handlers/node-to-jsx/utils.ts +++ b/packages/teleport-plugin-common/__tests__/node-handlers/node-to-jsx/utils.ts @@ -144,10 +144,10 @@ describe('createConditionIdentifier', () => { }) it('works on member expressions', () => { - const node = dynamicNode('prop', 'fields.title') + const node = dynamicNode('prop', 'fields', ['title']) const result = createConditionIdentifier(node, params, options) - expect(result.key).toBe('fields.title') + expect(result.key).toBe("fields?.['title']") expect(result.prefix).toBe('this.props') expect(result.type).toBe('object') }) diff --git a/packages/teleport-plugin-common/src/node-handlers/node-to-jsx/utils.ts b/packages/teleport-plugin-common/src/node-handlers/node-to-jsx/utils.ts index 621b1d82c..b5c407fd2 100644 --- a/packages/teleport-plugin-common/src/node-handlers/node-to-jsx/utils.ts +++ b/packages/teleport-plugin-common/src/node-handlers/node-to-jsx/utils.ts @@ -5,7 +5,7 @@ import { convertToUnaryOperator, convertValueToLiteral, } from '../../utils/ast-utils' -import { StringUtils } from '@teleporthq/teleport-shared' +import { StringUtils, UIDLUtils } from '@teleporthq/teleport-shared' import { UIDLPropDefinition, UIDLAttributeValue, @@ -25,6 +25,7 @@ import { JSXGenerationParams, JSXGenerationOptions, } from './types' +import { generateIdWithRefPath } from '@teleporthq/teleport-shared/dist/cjs/utils/uidl-utils' // Adds all the event handlers and all the instructions for each event handler // in case there is more than one specified in the UIDL @@ -153,12 +154,9 @@ export const createDynamicValueExpression = ( ) => { const identifierContent = identifier.content const refPath = identifier.content.refPath || [] - const { referenceType } = identifierContent + const { referenceType, id } = identifierContent - let id = identifierContent.id - refPath?.forEach((pathItem) => { - id = id.concat(`?.${pathItem}`) - }) + const idWithPath = generateIdWithRefPath(id, refPath) if (referenceType === 'attr' || referenceType === 'children' || referenceType === 'token') { throw new Error(`Dynamic reference type "${referenceType}" is not supported yet`) @@ -168,8 +166,8 @@ export const createDynamicValueExpression = ( options.dynamicReferencePrefixMap[referenceType as 'prop' | 'state' | 'local'] || '' return prefix === '' - ? t.identifier(id) - : t.memberExpression(t.identifier(prefix), t.identifier(id)) + ? t.identifier(idWithPath) + : t.memberExpression(t.identifier(prefix), t.identifier(idWithPath)) } // Prepares an identifier (from props or state or an expr) to be used as a conditional rendering identifier @@ -186,22 +184,35 @@ export const createConditionIdentifier = ( } } - const { id, referenceType } = dynamicReference.content + const { id, referenceType, refPath } = dynamicReference.content // in case the id is a member expression: eg: fields.name const referenceRoot = id.split('.')[0] + const currentType = + referenceType === 'prop' + ? params.propDefinitions[referenceRoot]?.type + : params.stateDefinitions[referenceRoot]?.type + + let type = currentType + if (refPath?.length) { + let currentValue = params.propDefinitions[referenceRoot].defaultValue as Record + for (const path of refPath) { + currentValue = currentValue?.[path] as Record + type = currentValue ? typeof currentValue : currentType + } + } switch (referenceType) { case 'prop': return { - key: id, - type: params.propDefinitions[referenceRoot].type, + key: UIDLUtils.generateIdWithRefPath(id, refPath), + type, prefix: options.dynamicReferencePrefixMap.prop, } case 'state': return { - key: id, - type: params.stateDefinitions[referenceRoot].type, + key: UIDLUtils.generateIdWithRefPath(id, refPath), + type, prefix: options.dynamicReferencePrefixMap.state, } diff --git a/packages/teleport-plugin-common/src/utils/ast-utils.ts b/packages/teleport-plugin-common/src/utils/ast-utils.ts index 5915e5a40..5dcca046c 100644 --- a/packages/teleport-plugin-common/src/utils/ast-utils.ts +++ b/packages/teleport-plugin-common/src/utils/ast-utils.ts @@ -324,7 +324,10 @@ export const objectToObjectExpression = ( const value = objectMap[key] let computedLiteralValue = null - if (value instanceof ParsedASTNode || value.constructor.name === 'ParsedASTNode') { + // TODO: Is this safe? This is for function props + if (value?.constructor?.name === 'Node') { + computedLiteralValue = value + } else if (value instanceof ParsedASTNode || value.constructor.name === 'ParsedASTNode') { computedLiteralValue = (value as ParsedASTNode).ast } else if (typeof value === 'boolean') { computedLiteralValue = t.booleanLiteral(value) diff --git a/packages/teleport-plugin-html-base-component/src/node-handlers.ts b/packages/teleport-plugin-html-base-component/src/node-handlers.ts index 44935904a..5fb52eb1f 100644 --- a/packages/teleport-plugin-html-base-component/src/node-handlers.ts +++ b/packages/teleport-plugin-html-base-component/src/node-handlers.ts @@ -23,6 +23,7 @@ import { UIDLElement, ElementsLookup, UIDLConditionalNode, + PropDefaultValueTypes, } from '@teleporthq/teleport-types' import { join, relative } from 'path' import { HASTBuilders, HASTUtils, ASTUtils } from '@teleporthq/teleport-plugin-common' @@ -143,7 +144,7 @@ export const generateHtmlSyntax: NodeToHTML)?.[path] + } + + // Safety measure in case no value is found + if (!defaultValue) { + defaultValue = usedProp.defaultValue + } + // Since we know the operand and the default value from the prop. // We can try building the condition and check if the condition is true or false. // @todo: You can only use a 'value' in UIDL or 'conditions' but not both. // UIDL validations need to be improved on this aspect. const dynamicConditions = createConditionalStatement( staticValue !== undefined ? [{ operand: staticValue, operation: '===' }] : conditions, - usedProp.defaultValue + defaultValue ) const matchCondition = matchingCriteria && matchingCriteria === 'all' ? '&&' : '||' const conditionString = dynamicConditions.join(` ${matchCondition} `) @@ -634,21 +645,21 @@ const generateDynamicNode: NodeToHTML + return HASTBuilders.createTextNode( + String( + extractDefaultValueFromRefPath( + usedReferenceValue.defaultValue as Record, + node.content.refPath + ) + ) ) - - if (value) { - return HASTBuilders.createTextNode(String(value)) - } } if (usedReferenceValue.type === 'element') { @@ -695,7 +706,9 @@ const handleStyles = ( style.content.referenceType === 'prop' ? propDefinitions : stateDefinitions ) if (referencedValue.type === 'string' || referencedValue.type === 'number') { - style = String(referencedValue.defaultValue) + style = String( + extractDefaultValueFromRefPath(referencedValue.defaultValue, style?.content?.refPath) + ) } node.content.style[styleKey] = typeof style === 'string' ? staticNode(style) : style } @@ -789,7 +802,11 @@ const handleAttributes = ( content.referenceType === 'prop' ? propDefinitions : stateDefinitions ) - HASTUtils.addAttributeToNode(htmlNode, attrKey, String(value.defaultValue)) + HASTUtils.addAttributeToNode( + htmlNode, + attrKey, + String(extractDefaultValueFromRefPath(value.defaultValue, content.refPath)) + ) break } @@ -817,7 +834,7 @@ const getValueFromReference = ( key: string, definitions: Record ): UIDLPropDefinition | undefined => { - const usedReferenceValue = definitions[key.includes('.') ? key.split('.')[0] : key] + const usedReferenceValue = definitions[key.includes('?.') ? key.split('?.')[0] : key] if (!usedReferenceValue) { throw new HTMLComponentGeneratorError( @@ -825,7 +842,9 @@ const getValueFromReference = ( ) } - if (['string', 'number', 'object', 'element'].includes(usedReferenceValue?.type) === false) { + if ( + ['string', 'number', 'object', 'element', 'array'].includes(usedReferenceValue?.type) === false + ) { throw new HTMLComponentGeneratorError( `Attribute is using dynamic value, but received of type ${JSON.stringify( usedReferenceValue, @@ -850,3 +869,14 @@ const getValueFromReference = ( return usedReferenceValue } + +const extractDefaultValueFromRefPath = ( + propDefaultValue: PropDefaultValueTypes, + refPath?: string[] +) => { + if (typeof propDefaultValue !== 'object' || !refPath?.length) { + return propDefaultValue + } + + return GenericUtils.getValueFromPath(refPath.join('.'), propDefaultValue) as PropDefaultValueTypes +} diff --git a/packages/teleport-plugin-jsx-proptypes/src/utils.ts b/packages/teleport-plugin-jsx-proptypes/src/utils.ts index d76ee2456..04bb45bb1 100644 --- a/packages/teleport-plugin-jsx-proptypes/src/utils.ts +++ b/packages/teleport-plugin-jsx-proptypes/src/utils.ts @@ -1,4 +1,5 @@ import * as types from '@babel/types' +import { parse } from '@babel/core' import { ASTUtils, ParsedASTNode } from '@teleporthq/teleport-plugin-common' import { UIDLPropDefinition } from '@teleporthq/teleport-types' @@ -17,9 +18,24 @@ export const buildDefaultPropsAst = ( const { defaultValue, type } = propDefinitions[key] if (type === 'func') { - acc.values[key] = new ParsedASTNode( + // Initialize with empty function + let parsedFunction: unknown = new ParsedASTNode( types.arrowFunctionExpression([], types.blockStatement([])) ) + + try { + const options = { + sourceType: 'module' as const, + } + const parseResult = parse(defaultValue.toString(), options)?.program?.body?.[0] + if (parseResult.type === 'ExpressionStatement') { + parsedFunction = parseResult.expression + } + } catch (err) { + // silet fail. + } + + acc.values[key] = parsedFunction acc.count++ return acc } diff --git a/packages/teleport-project-generator/src/index.ts b/packages/teleport-project-generator/src/index.ts index d441eb6d8..e243ecf02 100644 --- a/packages/teleport-project-generator/src/index.ts +++ b/packages/teleport-project-generator/src/index.ts @@ -167,6 +167,7 @@ export class ProjectGenerator implements ProjectGeneratorType { const rootFolder = UIDLUtils.cloneObject(template || DEFAULT_TEMPLATE) const schemaValidationResult = this.validator.validateProjectSchema(input) const { valid, projectUIDL } = schemaValidationResult + if (valid && projectUIDL) { cleanedUIDL = projectUIDL as unknown as Record } else { diff --git a/packages/teleport-shared/src/utils/uidl-utils.ts b/packages/teleport-shared/src/utils/uidl-utils.ts index 8524918ae..50a766393 100644 --- a/packages/teleport-shared/src/utils/uidl-utils.ts +++ b/packages/teleport-shared/src/utils/uidl-utils.ts @@ -818,7 +818,7 @@ export const transformStylesAssignmentsToJson = ( type, content: { ...content, - id: StringUtils.createStateOrPropStoringValue(content.id), + id: generateIdWithRefPath(content.id, content.refPath), }, } } else { @@ -841,6 +841,23 @@ export const transformStylesAssignmentsToJson = ( return newStyleObject } +export const generateIdWithRefPath = (contentId: string, refPath?: string[]) => { + let processedId = contentId + if (refPath) { + const processedRefPath = refPath.reduce((acc, path) => { + return `${acc}?.['${path}']` + }, '') + + // This is a bit ugly, but in some cases props are parsed twice or already generated. + // To avoid possible bugs, this should be a good safety measure + if (!processedId.includes(processedRefPath)) { + processedId = `${processedId}${processedRefPath}` + } + } + + return StringUtils.createStateOrPropStoringValue(processedId) +} + /* All the props passed to the components are transformed to a unique case to minimize the collision of using cameCalse in one place and dashCase in @@ -902,7 +919,7 @@ export const transformAttributesAssignmentsToJson = ( type, content: { ...content, - id: StringUtils.createStateOrPropStoringValue(content.id), + id: generateIdWithRefPath(content.id, content.refPath), }, } } else { diff --git a/packages/teleport-types/src/uidl.ts b/packages/teleport-types/src/uidl.ts index ca8982793..05d3c3e26 100755 --- a/packages/teleport-types/src/uidl.ts +++ b/packages/teleport-types/src/uidl.ts @@ -30,6 +30,7 @@ export interface UIDLPropValue { type: 'dynamic' content: { referenceType: 'prop' + refPath?: string[] id: string } } @@ -38,6 +39,7 @@ export interface UIDLStateValue { type: 'dynamic' content: { referenceType: 'state' + refPath?: string[] id: string } } @@ -264,9 +266,18 @@ export interface UIDLComponentSEO { export type UIDLMetaTag = Record +export type PropDefaultValueTypes = + | string + | number + | boolean + | unknown[] + | object + | (() => void) + | UIDLElementNode + export interface UIDLPropDefinition { type: string - defaultValue?: string | number | boolean | unknown[] | object | (() => void) | UIDLElementNode + defaultValue?: PropDefaultValueTypes isRequired?: boolean id?: string meta?: { @@ -349,7 +360,7 @@ export interface UIDLExpressionValue { export interface UIDLStaticValue { type: 'static' - content: string | number | boolean | unknown[] // unknown[] for data sources + content: string | number | boolean | Record | unknown[] // unknown[] for data sources } export interface UIDLRawValue { @@ -762,6 +773,7 @@ export type UIDLCompDynamicReference = { type: 'dynamic' content: { referenceType: 'prop' | 'comp' + refPath?: string[] id: string } } diff --git a/packages/teleport-uidl-builders/src/component-builders.ts b/packages/teleport-uidl-builders/src/component-builders.ts index 8b4b87ec1..115afec15 100644 --- a/packages/teleport-uidl-builders/src/component-builders.ts +++ b/packages/teleport-uidl-builders/src/component-builders.ts @@ -120,12 +120,17 @@ export const staticNode = (content: string | boolean | number): UIDLStaticValue } } -export const dynamicNode = (referenceType: ReferenceType, id: string): UIDLDynamicReference => { +export const dynamicNode = ( + referenceType: ReferenceType, + id: string, + refPath?: string[] +): UIDLDynamicReference => { return { type: 'dynamic', content: { referenceType, id, + refPath, }, } } diff --git a/packages/teleport-uidl-validator/src/decoders/utils.ts b/packages/teleport-uidl-validator/src/decoders/utils.ts index 1fd6b03db..5085df275 100644 --- a/packages/teleport-uidl-validator/src/decoders/utils.ts +++ b/packages/teleport-uidl-validator/src/decoders/utils.ts @@ -143,7 +143,7 @@ export const expressionValueDecoder: Decoder = object({ export const staticValueDecoder: Decoder = object({ type: constant('static'), - content: union(string(), number(), boolean(), array()), + content: union(string(), number(), boolean(), array(), object()), }) export const rawValueDecoder: Decoder = object({ diff --git a/packages/teleport-uidl-validator/src/parser/index.ts b/packages/teleport-uidl-validator/src/parser/index.ts index 7907303c9..58280b8b8 100644 --- a/packages/teleport-uidl-validator/src/parser/index.ts +++ b/packages/teleport-uidl-validator/src/parser/index.ts @@ -390,6 +390,8 @@ const parseComponentNode = (node: Record, component: ComponentU id: StringUtils.createStateOrPropStoringValue( ((content as UIDLURLLinkNode['content']).url as UIDLDynamicReference).content.id ), + refPath: ((content as UIDLURLLinkNode['content']).url as UIDLDynamicReference).content + .refPath, } } @@ -438,6 +440,7 @@ const parseComponentNode = (node: Record, component: ComponentU id: StringUtils.createStateOrPropStoringValue( conditionalNode.content.reference.content.id ), + refPath: reference.content.refPath, } } @@ -472,7 +475,10 @@ const parseComponentNode = (node: Record, component: ComponentU case 'dynamic': const dyamicNode = node as unknown as UIDLDynamicReference if (['state', 'prop'].includes(dyamicNode.content.referenceType)) { - dyamicNode.content.id = StringUtils.createStateOrPropStoringValue(dyamicNode.content.id) + dyamicNode.content.id = UIDLUtils.generateIdWithRefPath( + dyamicNode.content.id, + dyamicNode.content.refPath + ) } return dyamicNode case 'static':