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; }