Skip to content

Commit

Permalink
Add support for new prop types (#947)
Browse files Browse the repository at this point in the history
* Add support for object and array prop ref path nesting, add support for function props

* Try another build

* Fix tests and small bug
  • Loading branch information
TudorCe authored Nov 26, 2024
1 parent 10518d3 commit 8271807
Show file tree
Hide file tree
Showing 16 changed files with 163 additions and 51 deletions.
12 changes: 8 additions & 4 deletions examples/uidl-samples/component.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
},
{
Expand All @@ -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"]
}
}
},
Expand Down
9 changes: 6 additions & 3 deletions examples/uidl-samples/tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'] }}"
)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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`)
Expand All @@ -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
Expand All @@ -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<string, unknown>
for (const path of refPath) {
currentValue = currentValue?.[path] as Record<string, unknown>
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,
}

Expand Down
5 changes: 4 additions & 1 deletion packages/teleport-plugin-common/src/utils/ast-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
64 changes: 47 additions & 17 deletions packages/teleport-plugin-html-base-component/src/node-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -143,7 +144,7 @@ export const generateHtmlSyntax: NodeToHTML<UIDLNode, Promise<HastNode | HastTex
}

const {
content: { referenceType, id },
content: { referenceType, id, refPath },
} = reference

switch (referenceType) {
Expand All @@ -153,13 +154,23 @@ export const generateHtmlSyntax: NodeToHTML<UIDLNode, Promise<HastNode | HastTex
return conditionalNodeComment
}

let defaultValue = usedProp.defaultValue
for (const path of refPath) {
defaultValue = (defaultValue as Record<string, unknown[]>)?.[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} `)
Expand Down Expand Up @@ -634,21 +645,21 @@ const generateDynamicNode: NodeToHTML<UIDLDynamicReference, Promise<HastNode | H
node.content.referenceType === 'prop' ? propDefinitions : stateDefinitions
)

if (usedReferenceValue.type === 'object' && usedReferenceValue.defaultValue) {
if (
(usedReferenceValue.type === 'object' || usedReferenceValue.type === 'array') &&
usedReferenceValue.defaultValue
) {
// Let's say users are biding the prop to a node using something like this "fields.Title"
// But the fields in the object is the value where the object is defined either in propDefinitions
// or on the attrs. So, we just need to parsed the rest of the object path and get the value from the object.
const pathKeys: string[] = node.content.id.split(/\.|\[(['"]?)(.+?)\1\]/).filter(Boolean)
pathKeys.shift()

const value = GenericUtils.getValueFromPath(
pathKeys.join('.'),
usedReferenceValue.defaultValue as Record<string, UIDLPropDefinition>
return HASTBuilders.createTextNode(
String(
extractDefaultValueFromRefPath(
usedReferenceValue.defaultValue as Record<string, UIDLPropDefinition>,
node.content.refPath
)
)
)

if (value) {
return HASTBuilders.createTextNode(String(value))
}
}

if (usedReferenceValue.type === 'element') {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -817,15 +834,17 @@ const getValueFromReference = (
key: string,
definitions: Record<string, UIDLPropDefinition>
): 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(
`Definition for ${key} is missing from ${JSON.stringify(definitions, null, 2)}`
)
}

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,
Expand All @@ -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
}
18 changes: 17 additions & 1 deletion packages/teleport-plugin-jsx-proptypes/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions packages/teleport-project-generator/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>
} else {
Expand Down
Loading

0 comments on commit 8271807

Please sign in to comment.