diff --git a/src/utils/transform-jsx-to-reactive/index.test.ts b/src/utils/transform-jsx-to-reactive/index.test.ts index 646d738d1..b65969800 100644 --- a/src/utils/transform-jsx-to-reactive/index.test.ts +++ b/src/utils/transform-jsx-to-reactive/index.test.ts @@ -713,28 +713,47 @@ describe("utils", () => { expect(output).toBe(expected); }); - it.todo( - "should work with a component as an arrow function with blockstatement and the default export on a different line", - () => { - const input = ` + it("should work with a component as an arrow function with blockstatement and the default export on a different line", () => { + const input = ` const MyComponent = (props) =>
{props.foo}
export default MyComponent `; - const output = toInline( - transformJSXToReactive(input, "src/web-components/my-component.tsx"), - ); + const output = toInline( + transformJSXToReactive(input, "src/web-components/my-component.tsx"), + ); - const expected = toInline(` + const expected = toInline(` import {brisaElement} from "brisa/client"; - const MyComponent = (props, {h}) => h('div', {}, () => props.foo.value); - export default brisaElement(MyComponent, ['foo']); + export default brisaElement(function (props, {h}) {return h('div', {}, () => props.foo.value);}, ['foo']); `); - expect(output).toBe(expected); - }, - ); + expect(output).toBe(expected); + }); + + it("should work default export on a different line in a function declaration", () => { + const input = ` + function MyComponent(props) { + return
{props.foo}
+ } + export default MyComponent + `; + + const output = toInline( + transformJSXToReactive(input, "src/web-components/my-component.tsx"), + ); + + const expected = toInline(` + import {brisaElement} from "brisa/client"; + + export default brisaElement(function (props, {h}) { + return h('div', {}, () => props.foo.value); + }, ['foo']); + `); + + expect(output).toBe(expected); + }); it.todo( "should wrap conditional renders in different returns inside an hyperScript function", diff --git a/src/utils/transform-jsx-to-reactive/index.ts b/src/utils/transform-jsx-to-reactive/index.ts index c86dd2501..7deb33e8f 100644 --- a/src/utils/transform-jsx-to-reactive/index.ts +++ b/src/utils/transform-jsx-to-reactive/index.ts @@ -6,6 +6,7 @@ import defineBrisaElement from "./define-brisa-element"; import getComponentVariableNames from "./get-component-variable-names"; import { ALTERNATIVE_FOLDER_REGEX, WEB_COMPONENT_REGEX } from "./constants"; import transformToReactiveProps from "./transform-to-reactive-props"; +import transformToDirectExport from "./transform-to-direct-export"; const { parseCodeToAST, generateCodeFromAST } = AST("tsx"); @@ -13,7 +14,9 @@ export default function transformJSXToReactive(code: string, path: string) { if (path.match(ALTERNATIVE_FOLDER_REGEX)) return code; const ast = parseCodeToAST(code); - const [astWithPropsDotValue, propNames] = transformToReactiveProps(ast); + const astWithDirectExport = transformToDirectExport(ast); + const [astWithPropsDotValue, propNames] = + transformToReactiveProps(astWithDirectExport); const reactiveAst = transformToReactiveArrays(astWithPropsDotValue); const [componentBranch, index] = getWebComponentAst(reactiveAst) as [ ESTree.FunctionDeclaration, diff --git a/src/utils/transform-jsx-to-reactive/transform-to-direct-export/index.test.ts b/src/utils/transform-jsx-to-reactive/transform-to-direct-export/index.test.ts new file mode 100644 index 000000000..925ae87ed --- /dev/null +++ b/src/utils/transform-jsx-to-reactive/transform-to-direct-export/index.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from "bun:test"; +import AST from "../../ast"; +import transformToDirectExport from "."; + +const { parseCodeToAST, generateCodeFromAST } = AST(); +const toInline = (s: string) => s.replace(/\s*\n\s*/g, "").replaceAll("'", '"'); + +describe("utils", () => { + describe("transform-jsx-to-reactive", () => { + describe("transform-to-direct-export", () => { + it("should transform the web-component to a direct export if the component is a variable declaration", () => { + const ast = parseCodeToAST(` + const MyComponent = (props) =>
{props.foo}
; + export default MyComponent; + `); + const outputAst = transformToDirectExport(ast); + const outputCode = toInline(generateCodeFromAST(outputAst)); + const expectedCode = toInline(` + export default props => jsxDEV("div", {children: props.foo}, undefined, false, undefined, this); + `); + + expect(outputCode).toBe(expectedCode); + }); + + it("should transform the web-component to a direct export if the component is a function declaration", () => { + const ast = parseCodeToAST(` + function MyComponent(props) { + return
{props.foo}
; + } + export default MyComponent; + `); + const outputAst = transformToDirectExport(ast); + const outputCode = toInline(generateCodeFromAST(outputAst)); + const expectedCode = toInline(` + export default function (props) {return jsxDEV("div", {children: props.foo}, undefined, false, undefined, this);} + `); + + expect(outputCode).toBe(expectedCode); + }); + + it("should transform the web-component to a direct export if the component is an arrow function with block statement declaration", () => { + const ast = parseCodeToAST(` + const MyComponent = (props) => { + return
{props.foo}
; + } + export default MyComponent; + `); + const outputAst = transformToDirectExport(ast); + const outputCode = toInline(generateCodeFromAST(outputAst)); + const expectedCode = toInline(` + export default props => {return jsxDEV("div", {children: props.foo}, undefined, false, undefined, this);}; + `); + + expect(outputCode).toBe(expectedCode); + }); + + it("should not transform the web-component to a direct export if the component is a direct export", () => { + const ast = parseCodeToAST(` + export default (props) =>
{props.foo}
; + `); + const outputAst = transformToDirectExport(ast); + const outputCode = toInline(generateCodeFromAST(outputAst)); + const expectedCode = toInline(` + export default props => jsxDEV("div", {children: props.foo}, undefined, false, undefined, this); + `); + + expect(outputCode).toBe(expectedCode); + }); + + it("should not transform the web-component to a direct export if the component is a direct export with block statement", () => { + const ast = parseCodeToAST(` + export default (props) => { + return
{props.foo}
; + } + `); + const outputAst = transformToDirectExport(ast); + const outputCode = toInline(generateCodeFromAST(outputAst)); + const expectedCode = toInline(` + export default props => {return jsxDEV("div", {children: props.foo}, undefined, false, undefined, this);}; + `); + + expect(outputCode).toBe(expectedCode); + }); + + it("should not transform the web-component to a direct export if the component is a direct export with a function declaration", () => { + const ast = parseCodeToAST(` + export default function MyComponent(props) { + return
{props.foo}
; + } + `); + const outputAst = transformToDirectExport(ast); + const outputCode = toInline(generateCodeFromAST(outputAst)); + const expectedCode = toInline(` + export default function MyComponent(props) {return jsxDEV("div", {children: props.foo}, undefined, false, undefined, this);} + `); + + expect(outputCode).toBe(expectedCode); + }); + }); + }); +}); diff --git a/src/utils/transform-jsx-to-reactive/transform-to-direct-export/index.ts b/src/utils/transform-jsx-to-reactive/transform-to-direct-export/index.ts new file mode 100644 index 000000000..a9b47265a --- /dev/null +++ b/src/utils/transform-jsx-to-reactive/transform-to-direct-export/index.ts @@ -0,0 +1,72 @@ +import { ESTree } from "meriyah"; + +const DIRECT_TYPES = new Set([ + "ArrowFunctionExpression", + "FunctionExpression", + "VariableDeclaration", +]); + +/** + * transformToDirectExport + * + * @description Transform no-direct default export to a direct default export + * @example + * Input: + * const MyComponent = (props) =>
{props.foo}
; + * export default MyComponent; + * + * Output: + * export default (props) =>
{props.foo}
; + * + * @param {ESTree.Program} ast + * @returns {ESTree.Program} + */ +export default function transformToDirectExport( + ast: ESTree.Program, +): ESTree.Program { + const defaultExportIndex = ast.body.findIndex( + (node) => node.type === "ExportDefaultDeclaration", + ); + + if (defaultExportIndex === -1) return ast; + + const defaultExportNode = ast.body[defaultExportIndex] as any; + + if (DIRECT_TYPES.has(defaultExportNode.declaration.type)) return ast; + + const astWithoutDefaultExport = { + ...ast, + body: ast.body.filter((node, index) => index !== defaultExportIndex), + }; + + const componentDeclarationIndex = ast.body.findIndex((node: any) => { + const name = + node.id?.name ?? + node.declaration?.name ?? + node?.declarations?.[0]?.id?.name; + return ( + DIRECT_TYPES.has(node.type) && name === defaultExportNode.declaration.name + ); + }); + + if (componentDeclarationIndex === -1) return ast; + + const componentDeclaration = ast.body[componentDeclarationIndex]; + + if (componentDeclaration.type === "VariableDeclaration") { + const updatedBody = astWithoutDefaultExport.body.map((node, index) => { + if (index === componentDeclarationIndex) { + return { + ...node, + type: "ExportDefaultDeclaration", + declaration: componentDeclaration.declarations[0].init, + }; + } + return node; + }); + + return { ...astWithoutDefaultExport, body: updatedBody } as ESTree.Program; + } + + return ast; +} diff --git a/src/utils/transform-jsx-to-reactive/transform-to-reactive-props/index.test.ts b/src/utils/transform-jsx-to-reactive/transform-to-reactive-props/index.test.ts index e69778fc4..17f64bbf0 100644 --- a/src/utils/transform-jsx-to-reactive/transform-to-reactive-props/index.test.ts +++ b/src/utils/transform-jsx-to-reactive/transform-to-reactive-props/index.test.ts @@ -190,6 +190,38 @@ describe("utils", () => { expect(outputCode).toBe(expectedCode); expect(propNames).toEqual(["foo", "bar", "baz"]); }); + + it("should work consuming a property of some props", () => { + const code = ` + const outsideComponent = (props) => { + console.log(props.foo.name); + if(props.bar.name) return props.baz.name; + } + + export default function InsideWebComoponent(props) { + console.log(props.foo.name); + if(props.bar?.name) return
{props.baz.name}
; + } + `; + const ast = parseCodeToAST(code); + const [outputAst, propNames] = transformToReactiveProps(ast); + const outputCode = toInline(generateCodeFromAST(outputAst)); + + const expectedCode = toInline(` + const outsideComponent = props => { + console.log(props.foo.name); + if (props.bar.name) return props.baz.name; + }; + + export default function InsideWebComoponent(props) { + console.log(props.foo.value.name); + if (props.bar.value?.name) return jsxDEV("div", {children: props.baz.value.name}, undefined, false, undefined, this); + } + `); + + expect(outputCode).toBe(expectedCode); + expect(propNames).toEqual(["foo", "bar", "baz"]); + }); }); }); });