Skip to content

Commit

Permalink
fix(web-components): fix reactivity in default props
Browse files Browse the repository at this point in the history
  • Loading branch information
aralroca committed Nov 18, 2023
1 parent fe3a46d commit 97f2758
Show file tree
Hide file tree
Showing 11 changed files with 385 additions and 119 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,4 @@
"astring": "1.8.6",
"meriyah": "4.3.8"
}
}
}
2 changes: 1 addition & 1 deletion src/create-brisa-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@
"bin": {
"create-brisa-app": "./create-brisa-app.sh"
}
}
}
4 changes: 2 additions & 2 deletions src/utils/brisa-element/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1255,9 +1255,9 @@ describe("utils", () => {

it("should reactively update the DOM after adding a new property to the web-component", () => {
type Props = { count: { value: number } };
function Test({ count }: Props, { h }: any) {
function Test({ count }: Props, { h, effect }: any) {
// This is the code line after compiling: function Test({ count = 1 })
if (count.value == null) count.value = 1;
effect(() => (count.value ??= 1));
return h("div", {}, () => count?.value);
}

Expand Down
1 change: 1 addition & 0 deletions src/utils/transform-jsx-to-reactive/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const NO_REACTIVE_CHILDREN_EXPRESSION = new Set([
"Literal",
"ArrayExpression",
]);
export const SUPPORTED_DEFAULT_PROPS_OPERATORS = new Set(["??", "||"]);
export const JSX_NAME = new Set(["jsx", "jsxDEV"]);
export const ALTERNATIVE_FOLDER_REGEX = new RegExp(".*/web-components/@.*?/");
export const WEB_COMPONENT_REGEX = new RegExp(".*/web-components/.*");
Expand Down
29 changes: 18 additions & 11 deletions src/utils/transform-jsx-to-reactive/define-brisa-element/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import getReactiveReturnStatement from "../get-reactive-return-statement";
export default function defineBrisaElement(
component: ESTree.FunctionDeclaration,
componentPropsNames: string[],
hyperScriptVarName: string = "h"
hyperScriptVarName: string = "h",
effectVarName?: string
) {
const componentParams = component.params;
const componentBody = (component.body?.body ?? [
Expand All @@ -31,7 +32,9 @@ export default function defineBrisaElement(
async: component.async,
};

declareH(newComponentAst, hyperScriptVarName);
declareWebContextField(newComponentAst, hyperScriptVarName, "h");
if (effectVarName)
declareWebContextField(newComponentAst, effectVarName, "effect");

const args = [newComponentAst] as ESTree.Expression[];

Expand All @@ -58,21 +61,25 @@ export default function defineBrisaElement(
return [BRISA_IMPORT, newComponent];
}

function declareH(componentAST: any, hyperScriptVarName: string) {
const hProperty = {
function declareWebContextField(
componentAST: any,
fieldName: string,
originalFieldName: string
) {
const property = {
type: "Property",
key: {
type: "Identifier",
name: "h",
name: originalFieldName,
},
value: {
type: "Identifier",
name: hyperScriptVarName,
name: fieldName,
},
kind: "init",
computed: false,
method: false,
shorthand: hyperScriptVarName === "h",
shorthand: fieldName === originalFieldName,
};

// convert function () {} to function ({}) {}
Expand All @@ -87,23 +94,23 @@ function declareH(componentAST: any, hyperScriptVarName: string) {
if (componentAST.params?.length === 1) {
componentAST.params.push({
type: "ObjectPattern",
properties: [hProperty],
properties: [property],
});
}
// convert function ({}, { state }) {} to function ({}, { state, h }) {}
else if (componentAST.params[1]?.type === "ObjectPattern") {
const existH = componentAST.params[1].properties.some(
(prop: any) => prop.key.name === "h"
(prop: any) => prop.key.name === originalFieldName
);
if (!existH) componentAST.params[1].properties.push(hProperty);
if (!existH) componentAST.params[1].properties.push(property);
}
// convert function ({}, context) {} to function ({ h, ...context }) {}
else if (componentAST.params[1]?.type === "Identifier") {
const props = componentAST.params[1];
componentAST.params[1] = {
type: "ObjectPattern",
properties: [
hProperty,
property,
{
type: "RestElement",
argument: props,
Expand Down
24 changes: 18 additions & 6 deletions src/utils/transform-jsx-to-reactive/get-props-names/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ESTree } from "meriyah";
import { SUPPORTED_DEFAULT_PROPS_OPERATORS } from "../constants";

const CHILDREN = "children";

Expand Down Expand Up @@ -39,14 +40,13 @@ export default function getPropsNames(
}
}

const [, renames, defaultProps] = getPropsNamesFromIdentifier(
const [, renames] = getPropsNamesFromIdentifier(
"",
webComponentAst,
new Set(propNames)
);

renamedPropNames.push(...renames);
defaultPropsValues = { ...defaultPropsValues, ...defaultProps };

return [propNames, renamedPropNames, defaultPropsValues];
}
Expand Down Expand Up @@ -85,7 +85,7 @@ function getPropsNamesFromIdentifier(
}

// const { name } = props
if (
else if (
value?.init?.type === "Identifier" &&
value?.init?.name === identifier &&
value?.id?.properties
Expand All @@ -100,7 +100,7 @@ function getPropsNamesFromIdentifier(
}

// const foo = props.name
if (
else if (
value?.type === "VariableDeclarator" &&
value?.init?.object?.type === "Identifier" &&
value?.init?.object?.name === identifier &&
Expand All @@ -110,9 +110,21 @@ function getPropsNamesFromIdentifier(
renamedPropsNames.add(value?.id?.name);
}

// Track renamed current props not from identifier
// const foo = props.name ?? 'default value'
else if (
value?.type === "VariableDeclarator" &&
value?.id?.type === "Identifier" &&
value?.init?.type === "LogicalExpression" &&
SUPPORTED_DEFAULT_PROPS_OPERATORS.has(value?.init?.operator) &&
value?.init?.left?.object?.name === identifier
) {
renamedPropsNames.add(value?.id?.name);
defaultPropsValues[`${identifier}.${value?.init?.left?.property?.name}`] =
{ ...value.init.right, usedOperator: value.init.operator };
}

// const foo = bar // bar is a prop
if (
else if (
value?.type === "VariableDeclarator" &&
currentPropsNames.has(
value?.init?.left?.name ?? value?.init?.name ?? value?.id?.name
Expand Down
101 changes: 89 additions & 12 deletions src/utils/transform-jsx-to-reactive/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -875,8 +875,8 @@ describe("utils", () => {
const expected = toInline(`
import {brisaElement, _on, _off} from "brisa/client";
export default brisaElement(function MyComponent({foo}, {h}) {
if (foo.value == null) foo.value = 'bar';
export default brisaElement(function MyComponent({foo}, {h, effect}) {
effect(() => foo.value ??= 'bar');
const someVar = 'test';
return h('div', {}, () => foo.value);
}, ['foo']);
Expand All @@ -902,8 +902,8 @@ describe("utils", () => {
const expected = toInline(`
import {brisaElement, _on, _off} from "brisa/client";
export default brisaElement(function ({foo}, {h}) {
if (foo.value == null) foo.value = 'bar';
export default brisaElement(function ({foo}, {h, effect}) {
effect(() => foo.value ??= 'bar');
const someVar = 'test';
return h('div', {}, () => foo.value);
}, ['foo']);
Expand All @@ -929,8 +929,8 @@ describe("utils", () => {
const expected = toInline(`
import {brisaElement, _on, _off} from "brisa/client";
export default brisaElement(function ({foo}, {h}) {
if (foo.value == null) foo.value = 'bar';
export default brisaElement(function ({foo}, {h, effect}) {
effect(() => foo.value ??= 'bar');
const someVar = 'test';
return h('div', {}, () => foo.value);
}, ['foo']);
Expand All @@ -956,8 +956,8 @@ describe("utils", () => {
const expected = toInline(`
import {brisaElement, _on, _off} from "brisa/client";
export default brisaElement(function ({foo: renamedFoo}, {h}) {
if (renamedFoo.value == null) renamedFoo.value = 'bar';
export default brisaElement(function ({foo: renamedFoo}, {h, effect}) {
effect(() => renamedFoo.value ??= 'bar');
const someVar = 'test';
return h('div', {}, () => renamedFoo.value);
}, ['foo']);
Expand Down Expand Up @@ -1037,8 +1037,8 @@ describe("utils", () => {
const expected = toInline(`
import {brisaElement, _on, _off} from "brisa/client";
export default brisaElement(function MyComponent({foo}, {h}) {
if (!foo.value) foo.value = 'bar';
export default brisaElement(function MyComponent({foo}, {h, effect}) {
effect(() => foo.value ||= 'bar');
const bar = foo;
return h('div', {}, () => bar.value);
}, ['foo']);
Expand All @@ -1047,6 +1047,31 @@ describe("utils", () => {
expect(output).toBe(expected);
});

it("should be possible to set default props inside code with props identifier and || operator", () => {
const input = `
export default function MyComponent(props) {
const bar = props.foo || 'bar';
return <div>{bar}</div>
}
`;

const output = toInline(
transformJSXToReactive(input, "src/web-components/my-component.tsx")
);

const expected = toInline(`
import {brisaElement, _on, _off} from "brisa/client";
export default brisaElement(function MyComponent(props, {h, effect}) {
effect(() => props.foo.value ||= 'bar');
const bar = props.foo;
return h('div', {}, () => bar.value);
}, ['foo']);
`);

expect(output).toBe(expected);
});

it("should be possible to set default props inside code with ?? operator", () => {
const input = `
export default function MyComponent({foo}) {
Expand All @@ -1062,9 +1087,61 @@ describe("utils", () => {
const expected = toInline(`
import {brisaElement, _on, _off} from "brisa/client";
export default brisaElement(function MyComponent({foo}, {h}) {
if (foo.value == null) foo.value = 'bar';
export default brisaElement(function MyComponent({foo}, {h, effect}) {
effect(() => foo.value ??= 'bar');
const bar = foo;
return h('div', {}, () => bar.value);
}, ['foo']);
`);

expect(output).toBe(expected);
});

it("should be possible to set default props inside code with props identifier and ?? operator", () => {
const input = `
export default function MyComponent(props) {
const bar = props.foo ?? 'bar';
return <div>{bar}</div>
}
`;

const output = toInline(
transformJSXToReactive(input, "src/web-components/my-component.tsx")
);

const expected = toInline(`
import {brisaElement, _on, _off} from "brisa/client";
export default brisaElement(function MyComponent(props, {h, effect}) {
effect(() => props.foo.value ??= 'bar');
const bar = props.foo;
return h('div', {}, () => bar.value);
}, ['foo']);
`);

expect(output).toBe(expected);
});

it("should be possible to set default props inside code with ?? operator and some effect", () => {
const input = `
export default function MyComponent({foo}, {effect}) {
const bar = foo ?? 'bar';
effect(() => console.log(bar))
return <div>{bar}</div>
}
`;

const output = toInline(
transformJSXToReactive(input, "src/web-components/my-component.tsx")
);

const expected = toInline(`
import {brisaElement, _on, _off} from "brisa/client";
export default brisaElement(function MyComponent({foo}, {effect, h}) {
effect(() => foo.value ??= 'bar');
const bar = foo;
effect(() => console.log(bar.value));
return h('div', {}, () => bar.value);
}, ['foo']);
`);
Expand Down
10 changes: 7 additions & 3 deletions src/utils/transform-jsx-to-reactive/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ 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";
import { expect, it } from "bun:test";

const { parseCodeToAST, generateCodeFromAST } = AST("tsx");

Expand All @@ -16,7 +15,7 @@ export default function transformJSXToReactive(code: string, path: string) {

const ast = parseCodeToAST(code);
const astWithDirectExport = transformToDirectExport(ast);
const [astWithPropsDotValue, propNames] =
const [astWithPropsDotValue, propNames, isAddedDefaultProps] =
transformToReactiveProps(astWithDirectExport);
const reactiveAst = transformToReactiveArrays(astWithPropsDotValue);
const [componentBranch, index] = getWebComponentAst(reactiveAst) as [
Expand All @@ -31,13 +30,18 @@ export default function transformJSXToReactive(code: string, path: string) {
const componentVariableNames = getComponentVariableNames(componentBranch);
const allVariableNames = new Set([...propNames, ...componentVariableNames]);
let hyperScriptVarName = "h";
let effectVarName = isAddedDefaultProps ? "effect" : undefined;

while (allVariableNames.has(hyperScriptVarName)) hyperScriptVarName += "$";
if (effectVarName) {
while (allVariableNames.has(effectVarName)) effectVarName += "$";
}

const [importDeclaration, brisaElement] = defineBrisaElement(
componentBranch,
propNames,
hyperScriptVarName
hyperScriptVarName,
effectVarName
);

// Wrap the component with brisaElement
Expand Down
Loading

0 comments on commit 97f2758

Please sign in to comment.