Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions packages/teleport-plugin-common/src/utils/ast-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1271,19 +1271,36 @@ export const convertFilterDestinationToExpression = (
export const getExpressionFromUIDLExpressionNode = (
node: UIDLExpressionValue
): types.Expression => {
const ast = parse(node.content, {
sourceType: 'module' as const,
})
let ast
try {
ast = parse(node.content, {
sourceType: 'module' as const,
})
} catch (err) {
// Malformed expression content in the UIDL (e.g. `?.subtitle` missing its
// left-hand identifier). Don't abort the whole generation — warn and fall
// back to `undefined`, which renders as nothing in JSX.
// tslint:disable-next-line:no-console
console.warn(
`Failed to parse UIDL expression content ${JSON.stringify(node.content)}: ${
(err as Error).message
}. Falling back to 'undefined'.`
)
return types.identifier('undefined')
}

if (!('program' in ast)) {
if (!ast || !('program' in ast)) {
throw new Error(
`The AST does not have a program node in the expression inside addDynamicExpressionAttributeToJSXTag`
)
}

const theStatementOnlyWihtoutTheProgram = ast.program.body[0]

if (theStatementOnlyWihtoutTheProgram.type !== 'ExpressionStatement') {
if (
!theStatementOnlyWihtoutTheProgram ||
theStatementOnlyWihtoutTheProgram.type !== 'ExpressionStatement'
) {
throw new Error(`Expr dynamic attribute only support expressions statements at the moment.`)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,11 @@ const getValueType = (value: UIDLPropDefinition['defaultValue']) => {
case 'boolean':
return value
case 'object':
// `typeof null === 'object'` — render as the null literal so comparisons
// against a missing/null default evaluate sensibly.
if (value === null) {
return 'null'
}
// Handle dynamic references (local, prop, state)
if (value && typeof value === 'object' && 'type' in value) {
const dynamicValue = value as unknown as UIDLDynamicReference
Expand All @@ -451,6 +456,16 @@ const getValueType = (value: UIDLPropDefinition['defaultValue']) => {
}
}
}
// Handle link-type prop default values ({ url, newTab }). Collapse to the
// url string so comparisons like `mapUrl !== '--'` evaluate sensibly.
if (
value &&
typeof value === 'object' &&
'url' in (value as Record<string, unknown>) &&
typeof (value as Record<string, unknown>).url === 'string'
) {
return `"${(value as Record<string, unknown>).url}"`
}
throw new HTMLComponentGeneratorError(
`Conditional node received an operand of type ${valueType} \n
Received ${JSON.stringify(value)}`
Expand Down Expand Up @@ -1313,6 +1328,12 @@ const handleAttributes = (
content.referenceType === 'prop' ? propDefinitions : stateDefinitions
)

// A `func` prop has no meaningful static HTML representation; skip
// rather than emitting the stringified function body as an attribute.
if (value.type === 'func') {
break
}

const extracted = extractDefaultValueFromRefPath(value.defaultValue, content.refPath)
const extractedValue = String(extracted)

Expand Down Expand Up @@ -1383,7 +1404,7 @@ const getValueFromReference = (

if (
usedReferenceValue?.type &&
['string', 'number', 'object', 'element', 'array', 'boolean', 'link'].includes(
['string', 'number', 'object', 'element', 'array', 'boolean', 'link', 'func'].includes(
usedReferenceValue.type
) === false
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,18 @@ const transformLanguageSwitcherLinks = (node: types.Node): boolean => {
) {
const localeExpr = hrefAttr.value.expression
const originalChildren = [...node.children]
const nonHrefAttrs = opening.attributes.filter((_, i) => i !== hrefAttrIndex)
// Drop href/target/rel on the inner anchor — a locale switch should
// always stay in the same tab and is driven by Next's router, so the
// new-tab affordances from the UIDL's `newTab: true` would be wrong.
const innerAnchorStrippedAttrs = new Set(['href', 'target', 'rel'])
const nonHrefAttrs = opening.attributes.filter(
(attr) =>
!(
types.isJSXAttribute(attr) &&
types.isJSXIdentifier(attr.name) &&
innerAnchorStrippedAttrs.has(attr.name.name)
)
)

// Mutate node: change <a> to <Link>
opening.name = types.jsxIdentifier('Link')
Expand Down
20 changes: 17 additions & 3 deletions packages/teleport-uidl-resolver/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,11 @@ export const resolveElement = (element: UIDLElement, options: GeneratorOptions)
// For eg: If we have additational props on top of Link in react-router-dom. They are passed to the child.
// So, we need to manually find the attributes that are not supported by next and pass to the actual anchor tag.
// https://github.com/vercel/next.js/blob/v12.3.4/packages/next/client/link.tsx#L29-L54
//
// Note: we generate against next/link@^12 with legacy behavior (<Link><a>...</a></Link>).
// In that mode Next.js requires onClick/onMouseEnter/onTouchStart on the inner <a>, not on
// <Link> — otherwise it logs: `"onClick" was passed to <Link> … but "legacyBehavior" was set`.
// So these handlers are intentionally *not* in the Link-allowed list below and get pushed down.
if (isNextMappings && originalElement.elementType === 'Link' && originalElement.attrs) {
const unSupportedattributesForNextLink = Object.fromEntries(
Object.entries(originalElement.attrs).filter(
Expand All @@ -277,9 +282,6 @@ export const resolveElement = (element: UIDLElement, options: GeneratorOptions)
'prefetch',
'locale',
'legacyBehavior',
'onMouseEnter',
'onTouchStart',
'onClick',
].includes(key) === false
)
)
Expand All @@ -292,6 +294,18 @@ export const resolveElement = (element: UIDLElement, options: GeneratorOptions)
(key) => delete originalElement.attrs[key]
)
}

// Move event handlers from <Link> down to the inner <a>. Legacy next/link
// behavior (default in next@12) requires click/mouse/touch handlers on the
// child anchor, not on the Link itself.
if (originalElement.events && originalElement.children[0].type === 'element') {
const innerAnchor = originalElement.children[0]
innerAnchor.content.events = {
...innerAnchor.content.events,
...originalElement.events,
}
originalElement.events = {}
}
}
}

Expand Down
Loading