Skip to content

Commit 314b396

Browse files
fix: improve binding validation (#1739)
* fix: improve binding validation * changeset * test * some fixes * fix tests * snapshots * I cannot spell * fix: tests * Revert "fix: tests" This reverts commit 6a9b42c. * fix: test --------- Co-authored-by: Midhun A Darvin <[email protected]>
1 parent f442a8c commit 314b396

File tree

3 files changed

+222
-54
lines changed

3 files changed

+222
-54
lines changed

.changeset/clean-ads-lay.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@builder.io/mitosis': patch
3+
'@builder.io/mitosis-cli': patch
4+
---
5+
6+
Builder: improve accuracy of invalid binding detection

packages/core/src/__tests__/builder/invalid-jsx-flag.test.ts

Lines changed: 163 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,6 @@ describe('Builder Invalid JSX Flag', () => {
2323
const builderToMitosis = builderContentToMitosisComponent(builderJson, {
2424
escapeInvalidCode: true,
2525
});
26-
27-
expect(builderToMitosis.children[0].bindings.style).toMatchInlineSnapshot(`
28-
{
29-
"bindingType": "expression",
30-
"code": "{ marginTop: \`state.isDropdownOpen ? window.innerWidth <= 640 ? \\"25
31-
0px\\" : \\"100px\\" : \\"0\\" [INVALID CODE]\`, \\"@media (max-width: 991px)\\": { marginTop: \`state.isDropdownOpen ? window.innerWidth <= 640 ? \\"25
32-
0px\\" : \\"100px\\" : \\"0\\" [INVALID CODE]\` }, }",
33-
"type": "single",
34-
}
35-
`);
36-
3726
const mitosis = componentToMitosis({})({
3827
component: builderToMitosis,
3928
});
@@ -56,7 +45,7 @@ describe('Builder Invalid JSX Flag', () => {
5645
`);
5746
});
5847

59-
test('escaping invalid binding does not crash jsx generator', () => {
48+
test('escaping invalid binding does not crash jsx generator on element', () => {
6049
const builderJson = {
6150
data: {
6251
blocks: [
@@ -73,22 +62,6 @@ describe('Builder Invalid JSX Flag', () => {
7362
const builderToMitosis = builderContentToMitosisComponent(builderJson, {
7463
escapeInvalidCode: true,
7564
});
76-
77-
expect(builderToMitosis.children[0].bindings).toMatchInlineSnapshot(`
78-
{
79-
"foo": {
80-
"bindingType": "expression",
81-
"code": "\`bar + [INVALID CODE]\`",
82-
"type": "single",
83-
},
84-
"onClick": {
85-
"bindingType": "expression",
86-
"code": "\`state. [INVALID CODE]\`",
87-
"type": "single",
88-
},
89-
}
90-
`);
91-
9265
const mitosis = componentToMitosis({})({
9366
component: builderToMitosis,
9467
});
@@ -104,6 +77,48 @@ describe('Builder Invalid JSX Flag', () => {
10477
"
10578
`);
10679
});
80+
81+
// Text components have a different code path for bindings than other components
82+
test('escaping invalid binding does not crash jsx generator on Text component', () => {
83+
const builderJson = {
84+
data: {
85+
blocks: [
86+
{
87+
'@type': '@builder.io/sdk:Element' as const,
88+
bindings: {
89+
onClick: 'state.',
90+
foo: 'bar + ',
91+
},
92+
component: {
93+
name: 'Text',
94+
options: {
95+
text: 'Text',
96+
},
97+
},
98+
},
99+
],
100+
},
101+
};
102+
const builderToMitosis = builderContentToMitosisComponent(builderJson, {
103+
escapeInvalidCode: true,
104+
});
105+
const mitosis = componentToMitosis({})({
106+
component: builderToMitosis,
107+
});
108+
expect(mitosis).toMatchInlineSnapshot(`
109+
"export default function MyComponent(props) {
110+
return (
111+
<div
112+
onClick={(event) => \`state. [INVALID CODE]\`}
113+
foo={\`bar + [INVALID CODE]\`}
114+
>
115+
Text
116+
</div>
117+
);
118+
}
119+
"
120+
`);
121+
});
107122
});
108123

109124
describe('escapeInvalidCode: false', () => {
@@ -123,15 +138,6 @@ describe('Builder Invalid JSX Flag', () => {
123138
},
124139
};
125140
const builderToMitosis = builderContentToMitosisComponent(builderJson);
126-
127-
expect(builderToMitosis.children[0].bindings.style).toMatchInlineSnapshot(`
128-
{
129-
"bindingType": "expression",
130-
"code": "{ \\"@media (max-width: 991px)\\": { marginTop: state.marginTop }, }",
131-
"type": "single",
132-
}
133-
`);
134-
135141
const mitosis = componentToMitosis({})({
136142
component: builderToMitosis,
137143
});
@@ -151,7 +157,7 @@ describe('Builder Invalid JSX Flag', () => {
151157
`);
152158
});
153159

154-
test('invalid binding is dropped', () => {
160+
test('invalid binding is dropped on element', () => {
155161
const builderJson = {
156162
data: {
157163
blocks: [
@@ -166,26 +172,136 @@ describe('Builder Invalid JSX Flag', () => {
166172
},
167173
};
168174
const builderToMitosis = builderContentToMitosisComponent(builderJson);
169-
170-
expect(builderToMitosis.children[0].bindings).toMatchInlineSnapshot(`
171-
{
172-
"foo": {
173-
"bindingType": "expression",
174-
"code": "bar",
175-
"type": "single",
176-
},
175+
const mitosis = componentToMitosis({})({
176+
component: builderToMitosis,
177+
});
178+
expect(mitosis).toMatchInlineSnapshot(`
179+
"export default function MyComponent(props) {
180+
return <div foo={bar} />;
177181
}
182+
"
178183
`);
184+
});
179185

186+
// Text components have a different code path for bindings than other components
187+
test('invalid binding is dropped on Text component', () => {
188+
const builderJson = {
189+
data: {
190+
blocks: [
191+
{
192+
'@type': '@builder.io/sdk:Element' as const,
193+
bindings: {
194+
onClick: 'state.',
195+
foo: 'bar',
196+
},
197+
component: {
198+
name: 'Text',
199+
options: {
200+
text: 'Text',
201+
},
202+
},
203+
},
204+
],
205+
},
206+
};
207+
const builderToMitosis = builderContentToMitosisComponent(builderJson);
180208
const mitosis = componentToMitosis({})({
181209
component: builderToMitosis,
182210
});
183211
expect(mitosis).toMatchInlineSnapshot(`
184212
"export default function MyComponent(props) {
185-
return <div foo={bar} />;
213+
return <div foo={bar}>Text</div>;
186214
}
187215
"
188216
`);
189217
});
190218
});
191219
});
220+
221+
// https://github.com/BuilderIO/builder-internal/blob/39d18b50928f8c843255637a7c07c41d4277127c/packages/app/functions/transpile.worker.ts#L26-L42
222+
describe('export default transpiling', () => {
223+
test('convert on element', () => {
224+
const builderJson = {
225+
data: {
226+
blocks: [
227+
{
228+
'@type': '@builder.io/sdk:Element' as const,
229+
bindings: {
230+
foo: 'export default bar',
231+
},
232+
code: {
233+
bindings: {
234+
foo: 'export default bar',
235+
},
236+
},
237+
},
238+
],
239+
},
240+
};
241+
const builderToMitosis = builderContentToMitosisComponent(builderJson, {
242+
escapeInvalidCode: true,
243+
});
244+
const mitosis = componentToMitosis({})({
245+
component: builderToMitosis,
246+
});
247+
expect(mitosis).toMatchInlineSnapshot(`
248+
"export default function MyComponent(props) {
249+
return (
250+
<div
251+
foo={function () {
252+
return bar;
253+
}}
254+
/>
255+
);
256+
}
257+
"
258+
`);
259+
});
260+
261+
/// Text components have a different code path for bindings than other components
262+
test('convert on Text component', () => {
263+
const builderJson = {
264+
data: {
265+
blocks: [
266+
{
267+
'@type': '@builder.io/sdk:Element' as const,
268+
bindings: {
269+
foo: 'export default bar',
270+
},
271+
code: {
272+
bindings: {
273+
foo: 'export default bar',
274+
},
275+
},
276+
component: {
277+
name: 'Text',
278+
options: {
279+
text: 'Text',
280+
},
281+
},
282+
},
283+
],
284+
},
285+
};
286+
const builderToMitosis = builderContentToMitosisComponent(builderJson, {
287+
escapeInvalidCode: true,
288+
});
289+
const mitosis = componentToMitosis({})({
290+
component: builderToMitosis,
291+
});
292+
expect(mitosis).toMatchInlineSnapshot(`
293+
"export default function MyComponent(props) {
294+
return (
295+
<div
296+
foo={function () {
297+
return bar;
298+
}}
299+
>
300+
Text
301+
</div>
302+
);
303+
}
304+
"
305+
`);
306+
});
307+
});

packages/core/src/parsers/builder/builder.ts

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { hashCodeAsString } from '@/symbols/symbol-processor';
33
import { MitosisComponent, MitosisState } from '@/types/mitosis-component';
44
import * as babel from '@babel/core';
55
import generate from '@babel/generator';
6+
import babelTraverse from '@babel/traverse';
7+
import * as t from '@babel/types';
68
import { BuilderContent, BuilderElement } from '@builder.io/sdk';
79
import json5 from 'json5';
810
import { mapKeys, merge, omit, omitBy, sortBy, upperFirst } from 'lodash';
@@ -122,8 +124,11 @@ export const getStyleStringFromBlock = (
122124
}
123125

124126
let code = block.code?.bindings?.[key] || block.bindings[key];
127+
125128
const verifyCode = verifyIsValid(code);
126-
if (!verifyCode.valid) {
129+
if (verifyCode.valid) {
130+
code = processBoundLogic(code);
131+
} else {
127132
if (options.escapeInvalidCode) {
128133
code = '`' + code + ' [INVALID CODE]`';
129134
} else {
@@ -493,8 +498,8 @@ const componentMappers: {
493498
const localizedValues: MitosisNode['localizedValues'] = {};
494499

495500
const blockBindings: MitosisNode['bindings'] = {
496-
...mapBuilderBindingsToMitosisBindingWithCode(block.bindings),
497-
...mapBuilderBindingsToMitosisBindingWithCode(block.code?.bindings),
501+
...mapBuilderBindingsToMitosisBindingWithCode(block.bindings, options),
502+
...mapBuilderBindingsToMitosisBindingWithCode(block.code?.bindings, options),
498503
};
499504

500505
const bindings: any = {
@@ -669,6 +674,28 @@ type BuilderToMitosisOptions = {
669674
*/
670675
enableBlocksSlots?: boolean;
671676
};
677+
const processBoundLogic = (code: string) => {
678+
const ast = babel.parse(code);
679+
if (!ast) return code;
680+
681+
let replacedWithReturn = false;
682+
babelTraverse(ast, {
683+
ExportDefaultDeclaration(path) {
684+
const exportedNode = path.node.declaration;
685+
if (t.isExpression(exportedNode)) {
686+
const returnStatement = t.returnStatement(exportedNode);
687+
path.replaceWith(returnStatement);
688+
replacedWithReturn = true;
689+
}
690+
},
691+
});
692+
693+
if (replacedWithReturn) {
694+
return generate(ast).code;
695+
}
696+
697+
return code;
698+
};
672699

673700
export const builderElementToMitosisNode = (
674701
block: BuilderElement,
@@ -780,12 +807,15 @@ export const builderElementToMitosisNode = (
780807
if (key === 'css') {
781808
continue;
782809
}
810+
783811
const useKey = key.replace(/^(component\.)?options\./, '');
784812
if (!useKey.includes('.')) {
785813
let code = (blockBindings[key] as any).code || blockBindings[key];
786-
787814
const verifyCode = verifyIsValid(code);
788-
if (!verifyCode.valid) {
815+
816+
if (verifyCode.valid) {
817+
code = processBoundLogic(code);
818+
} else {
789819
if (options.escapeInvalidCode) {
790820
code = '`' + code + ' [INVALID CODE]`';
791821
} else {
@@ -1367,18 +1397,34 @@ export const builderContentToMitosisComponent = (
13671397

13681398
function mapBuilderBindingsToMitosisBindingWithCode(
13691399
bindings: { [key: string]: string } | undefined,
1400+
options?: BuilderToMitosisOptions,
13701401
): MitosisNode['bindings'] {
13711402
const result: MitosisNode['bindings'] = {};
13721403
bindings &&
13731404
Object.keys(bindings).forEach((key) => {
13741405
const value: string | { code: string } = bindings[key] as any;
1406+
let code = '';
13751407
if (typeof value === 'string') {
1376-
result[key] = createSingleBinding({ code: value });
1408+
code = value;
13771409
} else if (value && typeof value === 'object' && value.code) {
1378-
result[key] = createSingleBinding({ code: value.code });
1410+
code = value.code;
13791411
} else {
13801412
throw new Error('Unexpected binding value: ' + JSON.stringify(value));
13811413
}
1414+
1415+
const verifyCode = verifyIsValid(code);
1416+
if (verifyCode.valid) {
1417+
code = processBoundLogic(code);
1418+
} else {
1419+
if (options?.escapeInvalidCode) {
1420+
code = '`' + code + ' [INVALID CODE]`';
1421+
} else {
1422+
console.warn(`Dropping binding "${key}" due to invalid code: ${code}`);
1423+
return;
1424+
}
1425+
}
1426+
1427+
result[key] = createSingleBinding({ code });
13821428
});
13831429
return result;
13841430
}

0 commit comments

Comments
 (0)