@builder.io/react 8.2.1 → 8.2.2-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/builder-react-lite.cjs.js +1 -1
- package/dist/builder-react-lite.cjs.js.map +1 -1
- package/dist/builder-react-lite.esm.js +1 -1
- package/dist/builder-react-lite.esm.js.map +1 -1
- package/dist/builder-react.browser.js +2 -2
- package/dist/builder-react.browser.js.map +1 -1
- package/dist/builder-react.cjs.js +1 -1
- package/dist/builder-react.cjs.js.map +1 -1
- package/dist/builder-react.es5.js +1 -1
- package/dist/builder-react.es5.js.map +1 -1
- package/dist/builder-react.unpkg.js +2 -2
- package/dist/builder-react.unpkg.js.map +1 -1
- package/dist/lib/package.json +4 -3
- package/dist/lib/src/blocks/PersonalizationContainer.js +3 -3
- package/dist/lib/src/blocks/PersonalizationContainer.js.map +1 -1
- package/dist/lib/src/blocks/Router.js +2 -2
- package/dist/lib/src/blocks/Router.js.map +1 -1
- package/dist/lib/src/blocks/Symbol.js +1 -1
- package/dist/lib/src/blocks/Symbol.js.map +1 -1
- package/dist/lib/src/components/builder-component.component.js +3 -3
- package/dist/lib/src/components/builder-component.component.js.map +1 -1
- package/dist/lib/src/components/builder-content.component.js +1 -1
- package/dist/lib/src/components/builder-content.component.js.map +1 -1
- package/dist/lib/src/components/variants-provider.component.js +2 -2
- package/dist/lib/src/components/variants-provider.component.js.map +1 -1
- package/dist/lib/src/functions/should-force-browser-runtime-in-node.test.js +59 -0
- package/dist/lib/src/functions/should-force-browser-runtime-in-node.test.js.map +1 -0
- package/dist/lib/src/functions/string-to-function.js +4 -1
- package/dist/lib/src/functions/string-to-function.js.map +1 -1
- package/dist/lib/src/functions/string-to-function.test.js +289 -0
- package/dist/lib/src/functions/string-to-function.test.js.map +1 -0
- package/dist/lib/src/sdk-version.js +1 -1
- package/dist/types/src/components/builder-component.component.d.ts +5 -0
- package/dist/types/src/components/builder-content.component.d.ts +4 -0
- package/dist/types/src/components/variants-provider.component.d.ts +2 -1
- package/dist/types/src/functions/should-force-browser-runtime-in-node.test.d.ts +1 -0
- package/dist/types/src/functions/string-to-function.test.d.ts +1 -0
- package/dist/types/src/sdk-version.d.ts +1 -1
- package/package.json +3 -2
- package/src/blocks/PersonalizationContainer.tsx +3 -0
- package/src/blocks/Router.tsx +2 -1
- package/src/blocks/Symbol.tsx +1 -0
- package/src/components/builder-component.component.tsx +8 -0
- package/src/components/builder-content.component.tsx +5 -1
- package/src/components/variants-provider.component.tsx +3 -1
- package/src/functions/should-force-browser-runtime-in-node.test.ts +67 -0
- package/src/functions/string-to-function.test.ts +335 -0
- package/src/functions/string-to-function.ts +8 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { Builder } from '@builder.io/sdk';
|
|
2
|
+
import { stringToFunction, makeFn, getIsolateContext } from './string-to-function';
|
|
3
|
+
import * as shouldForceModule from './should-force-browser-runtime-in-node';
|
|
4
|
+
import { builder } from '@builder.io/sdk';
|
|
5
|
+
|
|
6
|
+
jest.mock('./is-debug', () => ({
|
|
7
|
+
isDebug: jest.fn().mockReturnValue(true),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
// Mock for isolated-vm module
|
|
11
|
+
interface MockReference {
|
|
12
|
+
value: any;
|
|
13
|
+
copySync?: () => any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const mockEvalClosureSync = jest.fn().mockReturnValue('"test"');
|
|
17
|
+
|
|
18
|
+
jest.mock('./safe-dynamic-require', () => ({
|
|
19
|
+
safeDynamicRequire: jest.fn().mockImplementation(() => ({
|
|
20
|
+
Isolate: class {
|
|
21
|
+
constructor() {}
|
|
22
|
+
createContextSync() {
|
|
23
|
+
return {
|
|
24
|
+
global: {
|
|
25
|
+
setSync: jest.fn(),
|
|
26
|
+
derefInto: jest.fn(),
|
|
27
|
+
},
|
|
28
|
+
evalClosureSync: mockEvalClosureSync,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
Reference: class implements MockReference {
|
|
33
|
+
value: any;
|
|
34
|
+
constructor(val: any) {
|
|
35
|
+
this.value = val;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
})),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
describe('makeFn', () => {
|
|
42
|
+
it('should create a function string with default arguments', () => {
|
|
43
|
+
const result = makeFn('state.value', true);
|
|
44
|
+
expect(result).toContain('var state = refToProxy($0);');
|
|
45
|
+
expect(result).toContain('var event = refToProxy($1);');
|
|
46
|
+
expect(result).toContain('var block = refToProxy($2);');
|
|
47
|
+
expect(result).toContain('var builder = refToProxy($3);');
|
|
48
|
+
expect(result).toContain('var Device = refToProxy($4);');
|
|
49
|
+
expect(result).toContain('var update = refToProxy($5);');
|
|
50
|
+
expect(result).toContain('var Builder = refToProxy($6);');
|
|
51
|
+
expect(result).toContain('var context = refToProxy($7);');
|
|
52
|
+
expect(result).toContain('var ctx = context;');
|
|
53
|
+
expect(result).toContain('return (state.value);');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should create a function string with custom arguments', () => {
|
|
57
|
+
const result = makeFn('custom.value', true, ['custom']);
|
|
58
|
+
expect(result).toContain('var custom = refToProxy($0);');
|
|
59
|
+
expect(result).not.toContain('var state = refToProxy($0);');
|
|
60
|
+
expect(result).toContain('return (custom.value);');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should handle non-return expressions', () => {
|
|
64
|
+
const result = makeFn('state.value', false);
|
|
65
|
+
expect(result).toContain('state.value');
|
|
66
|
+
expect(result).not.toContain('return (state.value);');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should include refToProxy function definition', () => {
|
|
70
|
+
const result = makeFn('state.value', true);
|
|
71
|
+
expect(result).toContain('var refToProxy = (obj) => {');
|
|
72
|
+
expect(result).toContain("if (typeof obj !== 'object' || obj === null) {");
|
|
73
|
+
expect(result).toContain('return obj;');
|
|
74
|
+
expect(result).toContain('return new Proxy({}, {');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should include stringify function definition', () => {
|
|
78
|
+
const result = makeFn('state.value', true);
|
|
79
|
+
expect(result).toContain('var stringify = (val) => {');
|
|
80
|
+
expect(result).toContain("if (typeof val === 'object' && val !== null) {");
|
|
81
|
+
expect(result).toContain('return JSON.stringify(val.copySync ? val.copySync() : val);');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should handle context alias correctly', () => {
|
|
85
|
+
const result = makeFn('ctx.value', true, ['state', 'context']);
|
|
86
|
+
expect(result).toContain('var ctx = context;');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should not include context alias when context is not in arguments', () => {
|
|
90
|
+
const result = makeFn('state.value', true, ['state']);
|
|
91
|
+
expect(result).not.toContain('var ctx = context;');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should properly wrap the code in endResult function', () => {
|
|
95
|
+
const result = makeFn('state.value', true);
|
|
96
|
+
expect(result).toContain('var endResult = function() {');
|
|
97
|
+
expect(result).toContain('return stringify(endResult());');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('getIsolateContext', () => {
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
Builder.serverContext = undefined;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should create a new context if none exists', () => {
|
|
107
|
+
const context = getIsolateContext();
|
|
108
|
+
expect(context).toBeDefined();
|
|
109
|
+
expect(Builder.serverContext).toBe(context);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should reuse existing context', () => {
|
|
113
|
+
const firstContext = getIsolateContext();
|
|
114
|
+
const secondContext = getIsolateContext();
|
|
115
|
+
expect(secondContext).toBe(firstContext);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('stringToFunction', () => {
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
// Reset Builder.isBrowser before each test
|
|
122
|
+
(Builder as any).isBrowser = true;
|
|
123
|
+
jest.clearAllMocks();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should return undefined for empty string', () => {
|
|
127
|
+
const fn = stringToFunction('');
|
|
128
|
+
expect(fn({})).toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should handle basic expressions', () => {
|
|
132
|
+
const fn = stringToFunction('state.value + 1');
|
|
133
|
+
expect(fn({ value: 1 })).toBe(2);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should handle statements', () => {
|
|
137
|
+
const fn = stringToFunction('let x = state.value; return x + 1;');
|
|
138
|
+
expect(fn({ value: 1 })).toBe(2);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should handle return statements', () => {
|
|
142
|
+
const fn = stringToFunction('return state.value + 1;');
|
|
143
|
+
expect(fn({ value: 1 })).toBe(2);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should handle functions that start with builder.run', () => {
|
|
147
|
+
const mockBuilderObj = {
|
|
148
|
+
getUserAttributes: jest.fn(),
|
|
149
|
+
run: jest.fn().mockReturnValue('ran'),
|
|
150
|
+
} as unknown as Builder;
|
|
151
|
+
const fn = stringToFunction('builder.run()');
|
|
152
|
+
expect(fn({}, undefined, undefined, mockBuilderObj)).toBe('ran');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should handle event parameter', () => {
|
|
156
|
+
const fn = stringToFunction('event.target.value');
|
|
157
|
+
const mockEvent = { target: { value: 'test' } } as unknown as Event;
|
|
158
|
+
expect(fn({}, mockEvent)).toBe('test');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should handle builder parameter', () => {
|
|
162
|
+
const fn = stringToFunction('builder.getUserAttributes()');
|
|
163
|
+
const mockBuilder = { getUserAttributes: () => ({ name: 'test' }) } as unknown as Builder;
|
|
164
|
+
expect(fn({}, undefined, undefined, mockBuilder)).toEqual({ name: 'test' });
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should handle context parameter', () => {
|
|
168
|
+
const fn = stringToFunction('ctx.value');
|
|
169
|
+
expect(
|
|
170
|
+
fn({}, undefined, undefined, undefined, undefined, undefined, undefined, { value: 'test' })
|
|
171
|
+
).toBe('test');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should cache function results', () => {
|
|
175
|
+
const str = 'state.value + 1';
|
|
176
|
+
const fn1 = stringToFunction(str);
|
|
177
|
+
const fn2 = stringToFunction(str);
|
|
178
|
+
expect(fn1).toBe(fn2);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should handle errors gracefully', () => {
|
|
182
|
+
const errors: Error[] = [];
|
|
183
|
+
const fn = stringToFunction('invalid code', true, errors);
|
|
184
|
+
fn({});
|
|
185
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should push error messages to logs array', () => {
|
|
189
|
+
const logs: string[] = [];
|
|
190
|
+
const errors: Error[] = [];
|
|
191
|
+
// Creating a runtime error by accessing an undefined property
|
|
192
|
+
const fn = stringToFunction('state.undefinedProp.accessSomething', true, errors, logs);
|
|
193
|
+
fn({});
|
|
194
|
+
expect(logs.length).toBeGreaterThan(0);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should handle compilation errors', () => {
|
|
198
|
+
const errors: Error[] = [];
|
|
199
|
+
// Invalid JavaScript that will cause a compilation error
|
|
200
|
+
const fn = stringToFunction('for() {}', true, errors);
|
|
201
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should handle functions in contentData', () => {
|
|
205
|
+
const fn = stringToFunction('state.contentData.exampleFunction()');
|
|
206
|
+
expect(
|
|
207
|
+
fn({
|
|
208
|
+
contentData: {
|
|
209
|
+
someString: 'test',
|
|
210
|
+
exampleFunction: () => 'exampleFunctionInvoked',
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
).toBe('exampleFunctionInvoked');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should pass all parameters correctly to the function', () => {
|
|
217
|
+
const fn = stringToFunction(
|
|
218
|
+
'state.value + (event ? 1 : 0) + (block ? 1 : 0) + (builder ? 1 : 0) + (Device ? 1 : 0) + (update ? 1 : 0) + (Builder ? 1 : 0) + (context ? 1 : 0)'
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const mockUpdate = jest.fn();
|
|
222
|
+
const mockDevice = { isMobile: true };
|
|
223
|
+
const mockBlock = { id: 'test-block' };
|
|
224
|
+
const mockEvent = { type: 'click' } as unknown as Event;
|
|
225
|
+
const mockContext = { foo: 'bar' };
|
|
226
|
+
|
|
227
|
+
const result = fn(
|
|
228
|
+
{ value: 1 },
|
|
229
|
+
mockEvent,
|
|
230
|
+
mockBlock,
|
|
231
|
+
{} as Builder,
|
|
232
|
+
mockDevice,
|
|
233
|
+
mockUpdate,
|
|
234
|
+
Builder,
|
|
235
|
+
mockContext
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// All parameters present = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 8
|
|
239
|
+
expect(result).toBe(8);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should handle the getIsolateContext with existing context', () => {
|
|
243
|
+
// Setup a fake serverContext
|
|
244
|
+
const mockContext = {
|
|
245
|
+
global: {
|
|
246
|
+
setSync: jest.fn(),
|
|
247
|
+
derefInto: jest.fn(),
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
Builder.serverContext = mockContext as any;
|
|
252
|
+
|
|
253
|
+
// Get the context
|
|
254
|
+
const context = getIsolateContext();
|
|
255
|
+
|
|
256
|
+
// Should return the existing context
|
|
257
|
+
expect(context).toBe(mockContext);
|
|
258
|
+
|
|
259
|
+
// Reset the context
|
|
260
|
+
Builder.serverContext = undefined;
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should handle complex isolated VM execution', () => {
|
|
264
|
+
// Setup a customized mock for evalClosureSync
|
|
265
|
+
mockEvalClosureSync.mockImplementationOnce((code, args) => {
|
|
266
|
+
// Verify that makeFn was called with correct parameters
|
|
267
|
+
expect(code).toContain('refToProxy');
|
|
268
|
+
expect(args.length).toBeGreaterThan(0);
|
|
269
|
+
|
|
270
|
+
// Return a valid JSON string to test the JSON.parse path
|
|
271
|
+
return '{"value":"test"}';
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const fn = stringToFunction('state');
|
|
275
|
+
const result = fn({ value: 'test' });
|
|
276
|
+
|
|
277
|
+
expect(result).toEqual({ value: 'test' });
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('server-side execution', () => {
|
|
281
|
+
beforeEach(() => {
|
|
282
|
+
(Builder as any).isBrowser = false;
|
|
283
|
+
jest.spyOn(shouldForceModule, 'shouldForceBrowserRuntimeInNode').mockReturnValue(false);
|
|
284
|
+
mockEvalClosureSync.mockReset();
|
|
285
|
+
mockEvalClosureSync.mockReturnValue('"test"');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
afterEach(() => {
|
|
289
|
+
jest.restoreAllMocks();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should use isolated VM when not in browser', () => {
|
|
293
|
+
const fn = stringToFunction('state.value');
|
|
294
|
+
expect(fn({ value: 'test' })).toBe('test');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should handle JSON parse errors in server context', () => {
|
|
298
|
+
mockEvalClosureSync.mockReturnValue('not valid json');
|
|
299
|
+
const fn = stringToFunction('state.value');
|
|
300
|
+
expect(fn({ value: 'test' })).toBe('not valid json');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should handle error in server-side execution', () => {
|
|
304
|
+
// Mock the evalClosureSync to throw an error
|
|
305
|
+
const testError = new Error('Server error');
|
|
306
|
+
mockEvalClosureSync.mockImplementation(() => {
|
|
307
|
+
throw testError;
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const consoleSpy = jest.spyOn(console, 'debug').mockImplementation(() => {});
|
|
311
|
+
const errors: Error[] = [];
|
|
312
|
+
|
|
313
|
+
const fn = stringToFunction('state.value', true, errors);
|
|
314
|
+
const result = fn({ value: 'test' });
|
|
315
|
+
|
|
316
|
+
expect(result).toBeNull();
|
|
317
|
+
expect(errors).toContain(testError);
|
|
318
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
319
|
+
|
|
320
|
+
consoleSpy.mockReset();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should use browser runtime when shouldForceBrowserRuntimeInNode returns true', () => {
|
|
324
|
+
// Instead of testing the warn functionality which is hard to mock properly,
|
|
325
|
+
// let's verify the code path by checking that the browser runtime path works
|
|
326
|
+
// when shouldForceBrowserRuntimeInNode returns true
|
|
327
|
+
jest.spyOn(shouldForceModule, 'shouldForceBrowserRuntimeInNode').mockReturnValue(true);
|
|
328
|
+
(Builder as any).isBrowser = false; // Ensure we're in "server" mode
|
|
329
|
+
|
|
330
|
+
// Simple expression that will work in browser mode
|
|
331
|
+
const fn = stringToFunction('state.value + 1');
|
|
332
|
+
expect(fn({ value: 1 })).toBe(2);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
});
|
|
@@ -169,6 +169,9 @@ export function stringToFunction(
|
|
|
169
169
|
if (errors) {
|
|
170
170
|
errors.push(error);
|
|
171
171
|
}
|
|
172
|
+
if (logs && error.message && typeof error.message === 'string') {
|
|
173
|
+
logs.push(error.message);
|
|
174
|
+
}
|
|
172
175
|
return null;
|
|
173
176
|
}
|
|
174
177
|
};
|
|
@@ -209,7 +212,12 @@ export const makeFn = (code: string, useReturn: boolean, args?: string[]) => {
|
|
|
209
212
|
}
|
|
210
213
|
const val = obj.getSync(key);
|
|
211
214
|
if (typeof val?.copySync === 'function') {
|
|
215
|
+
try {
|
|
212
216
|
return JSON.parse(stringify(val));
|
|
217
|
+
} catch (e) {
|
|
218
|
+
log('Error:', e);
|
|
219
|
+
return refToProxy(val);
|
|
220
|
+
}
|
|
213
221
|
}
|
|
214
222
|
return val;
|
|
215
223
|
},
|