diff --git a/packages/unminify/src/transformations/__tests__/un-jsx.spec.ts b/packages/unminify/src/transformations/__tests__/un-jsx.spec.ts index b853d453..18fb2b77 100644 --- a/packages/unminify/src/transformations/__tests__/un-jsx.spec.ts +++ b/packages/unminify/src/transformations/__tests__/un-jsx.spec.ts @@ -151,6 +151,62 @@ function fn() { `, ) +inlineTest('jsx with dynamic Component tag', + ` +function fn() { + return React.createElement( + r ? "a" : "div", + null, + "Hello", + ); +} +`, + ` +function fn() { + const Component = r ? "a" : "div"; + return Hello; +} +`, +) + +inlineTest('jsx with dynamic Component tag #2', + ` +function fn() { + return React.createElement( + components[0], + null, + "Hello", + ); +} +`, + ` +function fn() { + const Component = components[0]; + return Hello; +} +`, +) + +inlineTest('jsx with dynamic Component tag #3', + ` +const Foo = () => { + return jsxs("div", { + children: [ + jsx(r ? "a" : "div", { children: "bar" }, "b"), + jsx(g ? "p" : "div", { children: "baz" }, c), + ] + }); +}; +`, + ` +const Foo = () => { + const Component = g ? "p" : "div"; + const Component$0 = r ? "a" : "div"; + return
barbaz
; +}; +`, +) + inlineTest('jsx with child text that should be wrapped', ` function fn() { diff --git a/packages/unminify/src/transformations/un-jsx.ts b/packages/unminify/src/transformations/un-jsx.ts index 19627ae2..0e84cbbc 100644 --- a/packages/unminify/src/transformations/un-jsx.ts +++ b/packages/unminify/src/transformations/un-jsx.ts @@ -1,13 +1,15 @@ +import { assertScopeExists } from '@wakaru/ast-utils/assert' import { pascalCase } from '@wakaru/ast-utils/case' import { removePureAnnotation } from '@wakaru/ast-utils/comments' import { generateName } from '@wakaru/ast-utils/identifier' +import { insertBefore } from '@wakaru/ast-utils/insert' import { isNull, isTrue, isUndefined } from '@wakaru/ast-utils/matchers' import { wrapAstTransformation } from '@wakaru/ast-utils/wrapAstTransformation' import { z } from 'zod' import { nonNullable } from '../utils/utils' import type { ASTTransformation } from '@wakaru/ast-utils/wrapAstTransformation' import type { ExpressionKind, LiteralKind } from 'ast-types/lib/gen/kinds' -import type { ASTNode, CallExpression, Collection, Identifier, JSCodeshift, JSXAttribute, JSXElement, JSXExpressionContainer, JSXFragment, JSXIdentifier, JSXMemberExpression, JSXSpreadAttribute, JSXSpreadChild, JSXText, MemberExpression, RestElement, SpreadElement, StringLiteral, VariableDeclarator } from 'jscodeshift' +import type { ASTNode, ASTPath, CallExpression, Collection, Identifier, JSCodeshift, JSXAttribute, JSXElement, JSXExpressionContainer, JSXFragment, JSXIdentifier, JSXMemberExpression, JSXSpreadAttribute, JSXSpreadChild, JSXText, MemberExpression, RestElement, SpreadElement, StringLiteral, VariableDeclarator } from 'jscodeshift' export const Schema = z.object({ pragma: z.string().optional().describe('The pragma to use for JSX transformation.'), @@ -100,7 +102,7 @@ export const transformAST: ASTTransformation = (context, params) => { // bottom-up transformation .reverse() .forEach((path) => { - const jsxElement = toJSX(j, path.node, pragmas, pragmaFrags) + const jsxElement = toJSX(j, path, pragmas, pragmaFrags) if (jsxElement) { const parentWithComments = j.ExpressionStatement.check(path.parent.node) ? path.parent : path removePureAnnotation(j, parentWithComments.node) @@ -110,7 +112,11 @@ export const transformAST: ASTTransformation = (context, params) => { }) } -function toJSX(j: JSCodeshift, node: CallExpression, pragmas: string[], pragmaFrags: string[]): JSXElement | JSXFragment | null { +function toJSX(j: JSCodeshift, path: ASTPath, pragmas: string[], pragmaFrags: string[]): JSXElement | JSXFragment | null { + const scope = path.scope + assertScopeExists(scope) + + const node = path.node const pragma = getPragma(j, node.callee, pragmas) if (!pragma) return null @@ -124,7 +130,16 @@ function toJSX(j: JSCodeshift, node: CallExpression, pragmas: string[], pragmaFr if (isCapitalizationInvalid(j, type)) return null - const tag = toJsxTag(j, type) + let tag = toJsxTag(j, type) + // If a tag cannot be converted to JSX tag, convert it to a variable + if (!tag && !j.SpreadElement.check(type)) { + const name = generateName('Component', scope) + tag = j.jsxIdentifier(name) + + const variableDeclaration = j.variableDeclaration('const', [j.variableDeclarator(j.identifier(name), type)]) + insertBefore(j, path, variableDeclaration) + scope.markAsStale() + } if (!tag) return null const attributes = toJsxAttributes(j, props) @@ -204,9 +219,13 @@ function toJsxTag(j: JSCodeshift, node: SpreadElement | ExpressionKind): JSXIden return j.jsxIdentifier(node.name) } else if (j.MemberExpression.check(node)) { + const object = toJsxTag(j, node.object) + const property = toJsxTag(j, node.property) + if (!object || !property) return null + return j.jsxMemberExpression( - toJsxTag(j, node.object) as JSXIdentifier | JSXMemberExpression, - toJsxTag(j, node.property) as JSXIdentifier, + object as JSXIdentifier | JSXMemberExpression, + property as JSXIdentifier, ) }