diff --git a/.changeset/clean-ads-lay.md b/.changeset/clean-ads-lay.md
new file mode 100644
index 0000000000..f14b9dc2d5
--- /dev/null
+++ b/.changeset/clean-ads-lay.md
@@ -0,0 +1,6 @@
+---
+'@builder.io/mitosis': patch
+'@builder.io/mitosis-cli': patch
+---
+
+Builder: improve accuracy of invalid binding detection
diff --git a/packages/core/src/__tests__/builder/invalid-jsx-flag.test.ts b/packages/core/src/__tests__/builder/invalid-jsx-flag.test.ts
index 716ae140bc..11f61339b6 100644
--- a/packages/core/src/__tests__/builder/invalid-jsx-flag.test.ts
+++ b/packages/core/src/__tests__/builder/invalid-jsx-flag.test.ts
@@ -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,
});
@@ -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: [
@@ -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,
});
@@ -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 (
+
\`state. [INVALID CODE]\`}
+ foo={\`bar + [INVALID CODE]\`}
+ >
+ Text
+
+ );
+ }
+ "
+ `);
+ });
});
describe('escapeInvalidCode: false', () => {
@@ -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,
});
@@ -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: [
@@ -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 ;
}
+ "
`);
+ });
+ // 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 ;
+ return Text
;
}
"
`);
});
});
});
+
+// 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 (
+
+ );
+ }
+ "
+ `);
+ });
+
+ /// 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 (
+
+ Text
+
+ );
+ }
+ "
+ `);
+ });
+});
diff --git a/packages/core/src/parsers/builder/builder.ts b/packages/core/src/parsers/builder/builder.ts
index 470e9c80e9..1f2850fab2 100644
--- a/packages/core/src/parsers/builder/builder.ts
+++ b/packages/core/src/parsers/builder/builder.ts
@@ -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';
@@ -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 {
@@ -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 = {
@@ -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,
@@ -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 {
@@ -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;
}