Skip to content

Commit

Permalink
fix(web-components): fix no-direct default export
Browse files Browse the repository at this point in the history
  • Loading branch information
aralroca committed Nov 11, 2023
1 parent b55284e commit a8a930c
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 14 deletions.
45 changes: 32 additions & 13 deletions src/utils/transform-jsx-to-reactive/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => <div>{props.foo}</div>
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 <div>{props.foo}</div>
}
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",
Expand Down
5 changes: 4 additions & 1 deletion src/utils/transform-jsx-to-reactive/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ 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");

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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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) => <div>{props.foo}</div>;
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 <div>{props.foo}</div>;
}
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 <div>{props.foo}</div>;
}
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) => <div>{props.foo}</div>;
`);
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 <div>{props.foo}</div>;
}
`);
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 <div>{props.foo}</div>;
}
`);
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);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -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) => <div>{props.foo}</div>;
* export default MyComponent;
*
* Output:
* export default (props) => <div>{props.foo}</div>;
*
* @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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 <div>{props.baz.name}</div>;
}
`;
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"]);
});
});
});
});

0 comments on commit a8a930c

Please sign in to comment.