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
6 changes: 6 additions & 0 deletions .changeset/clean-ads-lay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@builder.io/mitosis': patch
'@builder.io/mitosis-cli': patch
---

Builder: improve accuracy of invalid binding detection
210 changes: 163 additions & 47 deletions packages/core/src/__tests__/builder/invalid-jsx-flag.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,6 @@ describe('Builder Invalid JSX Flag', () => {
const builderToMitosis = builderContentToMitosisComponent(builderJson, {
escapeInvalidCode: true,
});

expect(builderToMitosis.children[0].bindings.style).toMatchInlineSnapshot(`
{
"bindingType": "expression",
"code": "{ marginTop: \`state.isDropdownOpen ? window.innerWidth <= 640 ? \\"25
0px\\" : \\"100px\\" : \\"0\\" [INVALID CODE]\`, \\"@media (max-width: 991px)\\": { marginTop: \`state.isDropdownOpen ? window.innerWidth <= 640 ? \\"25
0px\\" : \\"100px\\" : \\"0\\" [INVALID CODE]\` }, }",
"type": "single",
}
`);

const mitosis = componentToMitosis({})({
component: builderToMitosis,
});
Expand All @@ -56,7 +45,7 @@ describe('Builder Invalid JSX Flag', () => {
`);
});

test('escaping invalid binding does not crash jsx generator', () => {
test('escaping invalid binding does not crash jsx generator on element', () => {
const builderJson = {
data: {
blocks: [
Expand All @@ -73,22 +62,6 @@ describe('Builder Invalid JSX Flag', () => {
const builderToMitosis = builderContentToMitosisComponent(builderJson, {
escapeInvalidCode: true,
});

expect(builderToMitosis.children[0].bindings).toMatchInlineSnapshot(`
{
"foo": {
"bindingType": "expression",
"code": "\`bar + [INVALID CODE]\`",
"type": "single",
},
"onClick": {
"bindingType": "expression",
"code": "\`state. [INVALID CODE]\`",
"type": "single",
},
}
`);

const mitosis = componentToMitosis({})({
component: builderToMitosis,
});
Expand All @@ -104,6 +77,48 @@ describe('Builder Invalid JSX Flag', () => {
"
`);
});

// Text components have a different code path for bindings than other components
test('escaping invalid binding does not crash jsx generator on Text component', () => {
const builderJson = {
data: {
blocks: [
{
'@type': '@builder.io/sdk:Element' as const,
bindings: {
onClick: 'state.',
foo: 'bar + ',
},
component: {
name: 'Text',
options: {
text: 'Text',
},
},
},
],
},
};
const builderToMitosis = builderContentToMitosisComponent(builderJson, {
escapeInvalidCode: true,
});
const mitosis = componentToMitosis({})({
component: builderToMitosis,
});
expect(mitosis).toMatchInlineSnapshot(`
"export default function MyComponent(props) {
return (
<div
onClick={(event) => \`state. [INVALID CODE]\`}
foo={\`bar + [INVALID CODE]\`}
>
Text
</div>
);
}
"
`);
});
});

describe('escapeInvalidCode: false', () => {
Expand All @@ -123,15 +138,6 @@ describe('Builder Invalid JSX Flag', () => {
},
};
const builderToMitosis = builderContentToMitosisComponent(builderJson);

expect(builderToMitosis.children[0].bindings.style).toMatchInlineSnapshot(`
{
"bindingType": "expression",
"code": "{ \\"@media (max-width: 991px)\\": { marginTop: state.marginTop }, }",
"type": "single",
}
`);

const mitosis = componentToMitosis({})({
component: builderToMitosis,
});
Expand All @@ -151,7 +157,7 @@ describe('Builder Invalid JSX Flag', () => {
`);
});

test('invalid binding is dropped', () => {
test('invalid binding is dropped on element', () => {
const builderJson = {
data: {
blocks: [
Expand All @@ -166,26 +172,136 @@ describe('Builder Invalid JSX Flag', () => {
},
};
const builderToMitosis = builderContentToMitosisComponent(builderJson);

expect(builderToMitosis.children[0].bindings).toMatchInlineSnapshot(`
{
"foo": {
"bindingType": "expression",
"code": "bar",
"type": "single",
},
const mitosis = componentToMitosis({})({
component: builderToMitosis,
});
expect(mitosis).toMatchInlineSnapshot(`
"export default function MyComponent(props) {
return <div foo={bar} />;
}
"
`);
});

// Text components have a different code path for bindings than other components
test('invalid binding is dropped on Text component', () => {
const builderJson = {
data: {
blocks: [
{
'@type': '@builder.io/sdk:Element' as const,
bindings: {
onClick: 'state.',
foo: 'bar',
},
component: {
name: 'Text',
options: {
text: 'Text',
},
},
},
],
},
};
const builderToMitosis = builderContentToMitosisComponent(builderJson);
const mitosis = componentToMitosis({})({
component: builderToMitosis,
});
expect(mitosis).toMatchInlineSnapshot(`
"export default function MyComponent(props) {
return <div foo={bar} />;
return <div foo={bar}>Text</div>;
}
"
`);
});
});
});

// https://github.com/BuilderIO/builder-internal/blob/39d18b50928f8c843255637a7c07c41d4277127c/packages/app/functions/transpile.worker.ts#L26-L42
describe('export default transpiling', () => {
test('convert on element', () => {
const builderJson = {
data: {
blocks: [
{
'@type': '@builder.io/sdk:Element' as const,
bindings: {
foo: 'export default bar',
},
code: {
bindings: {
foo: 'export default bar',
},
},
},
],
},
};
const builderToMitosis = builderContentToMitosisComponent(builderJson, {
escapeInvalidCode: true,
});
const mitosis = componentToMitosis({})({
component: builderToMitosis,
});
expect(mitosis).toMatchInlineSnapshot(`
"export default function MyComponent(props) {
return (
<div
foo={function () {
return bar;
}}
/>
);
}
"
`);
});

/// Text components have a different code path for bindings than other components
test('convert on Text component', () => {
const builderJson = {
data: {
blocks: [
{
'@type': '@builder.io/sdk:Element' as const,
bindings: {
foo: 'export default bar',
},
code: {
bindings: {
foo: 'export default bar',
},
},
component: {
name: 'Text',
options: {
text: 'Text',
},
},
},
],
},
};
const builderToMitosis = builderContentToMitosisComponent(builderJson, {
escapeInvalidCode: true,
});
const mitosis = componentToMitosis({})({
component: builderToMitosis,
});
expect(mitosis).toMatchInlineSnapshot(`
"export default function MyComponent(props) {
return (
<div
foo={function () {
return bar;
}}
>
Text
</div>
);
}
"
`);
});
});
60 changes: 53 additions & 7 deletions packages/core/src/parsers/builder/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { hashCodeAsString } from '@/symbols/symbol-processor';
import { MitosisComponent, MitosisState } from '@/types/mitosis-component';
import * as babel from '@babel/core';
import generate from '@babel/generator';
import babelTraverse from '@babel/traverse';
import * as t from '@babel/types';
import { BuilderContent, BuilderElement } from '@builder.io/sdk';
import json5 from 'json5';
import { mapKeys, merge, omit, omitBy, sortBy, upperFirst } from 'lodash';
Expand Down Expand Up @@ -122,8 +124,11 @@ export const getStyleStringFromBlock = (
}

let code = block.code?.bindings?.[key] || block.bindings[key];

const verifyCode = verifyIsValid(code);
if (!verifyCode.valid) {
if (verifyCode.valid) {
code = processBoundLogic(code);
} else {
if (options.escapeInvalidCode) {
code = '`' + code + ' [INVALID CODE]`';
} else {
Expand Down Expand Up @@ -493,8 +498,8 @@ const componentMappers: {
const localizedValues: MitosisNode['localizedValues'] = {};

const blockBindings: MitosisNode['bindings'] = {
...mapBuilderBindingsToMitosisBindingWithCode(block.bindings),
...mapBuilderBindingsToMitosisBindingWithCode(block.code?.bindings),
...mapBuilderBindingsToMitosisBindingWithCode(block.bindings, options),
...mapBuilderBindingsToMitosisBindingWithCode(block.code?.bindings, options),
};

const bindings: any = {
Expand Down Expand Up @@ -669,6 +674,28 @@ type BuilderToMitosisOptions = {
*/
enableBlocksSlots?: boolean;
};
const processBoundLogic = (code: string) => {
const ast = babel.parse(code);
if (!ast) return code;

let replacedWithReturn = false;
babelTraverse(ast, {
ExportDefaultDeclaration(path) {
const exportedNode = path.node.declaration;
if (t.isExpression(exportedNode)) {
const returnStatement = t.returnStatement(exportedNode);
path.replaceWith(returnStatement);
replacedWithReturn = true;
}
},
});

if (replacedWithReturn) {
return generate(ast).code;
}

return code;
};

export const builderElementToMitosisNode = (
block: BuilderElement,
Expand Down Expand Up @@ -780,12 +807,15 @@ export const builderElementToMitosisNode = (
if (key === 'css') {
continue;
}

const useKey = key.replace(/^(component\.)?options\./, '');
if (!useKey.includes('.')) {
let code = (blockBindings[key] as any).code || blockBindings[key];

const verifyCode = verifyIsValid(code);
if (!verifyCode.valid) {

if (verifyCode.valid) {
code = processBoundLogic(code);
} else {
if (options.escapeInvalidCode) {
code = '`' + code + ' [INVALID CODE]`';
} else {
Expand Down Expand Up @@ -1367,18 +1397,34 @@ export const builderContentToMitosisComponent = (

function mapBuilderBindingsToMitosisBindingWithCode(
bindings: { [key: string]: string } | undefined,
options?: BuilderToMitosisOptions,
): MitosisNode['bindings'] {
const result: MitosisNode['bindings'] = {};
bindings &&
Object.keys(bindings).forEach((key) => {
const value: string | { code: string } = bindings[key] as any;
let code = '';
if (typeof value === 'string') {
result[key] = createSingleBinding({ code: value });
code = value;
} else if (value && typeof value === 'object' && value.code) {
result[key] = createSingleBinding({ code: value.code });
code = value.code;
} else {
throw new Error('Unexpected binding value: ' + JSON.stringify(value));
}

const verifyCode = verifyIsValid(code);
if (verifyCode.valid) {
code = processBoundLogic(code);
} else {
if (options?.escapeInvalidCode) {
code = '`' + code + ' [INVALID CODE]`';
} else {
console.warn(`Dropping binding "${key}" due to invalid code: ${code}`);
return;
}
}

result[key] = createSingleBinding({ code });
});
return result;
}
Expand Down