@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.
Files changed (49) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/builder-react-lite.cjs.js +1 -1
  3. package/dist/builder-react-lite.cjs.js.map +1 -1
  4. package/dist/builder-react-lite.esm.js +1 -1
  5. package/dist/builder-react-lite.esm.js.map +1 -1
  6. package/dist/builder-react.browser.js +2 -2
  7. package/dist/builder-react.browser.js.map +1 -1
  8. package/dist/builder-react.cjs.js +1 -1
  9. package/dist/builder-react.cjs.js.map +1 -1
  10. package/dist/builder-react.es5.js +1 -1
  11. package/dist/builder-react.es5.js.map +1 -1
  12. package/dist/builder-react.unpkg.js +2 -2
  13. package/dist/builder-react.unpkg.js.map +1 -1
  14. package/dist/lib/package.json +4 -3
  15. package/dist/lib/src/blocks/PersonalizationContainer.js +3 -3
  16. package/dist/lib/src/blocks/PersonalizationContainer.js.map +1 -1
  17. package/dist/lib/src/blocks/Router.js +2 -2
  18. package/dist/lib/src/blocks/Router.js.map +1 -1
  19. package/dist/lib/src/blocks/Symbol.js +1 -1
  20. package/dist/lib/src/blocks/Symbol.js.map +1 -1
  21. package/dist/lib/src/components/builder-component.component.js +3 -3
  22. package/dist/lib/src/components/builder-component.component.js.map +1 -1
  23. package/dist/lib/src/components/builder-content.component.js +1 -1
  24. package/dist/lib/src/components/builder-content.component.js.map +1 -1
  25. package/dist/lib/src/components/variants-provider.component.js +2 -2
  26. package/dist/lib/src/components/variants-provider.component.js.map +1 -1
  27. package/dist/lib/src/functions/should-force-browser-runtime-in-node.test.js +59 -0
  28. package/dist/lib/src/functions/should-force-browser-runtime-in-node.test.js.map +1 -0
  29. package/dist/lib/src/functions/string-to-function.js +4 -1
  30. package/dist/lib/src/functions/string-to-function.js.map +1 -1
  31. package/dist/lib/src/functions/string-to-function.test.js +289 -0
  32. package/dist/lib/src/functions/string-to-function.test.js.map +1 -0
  33. package/dist/lib/src/sdk-version.js +1 -1
  34. package/dist/types/src/components/builder-component.component.d.ts +5 -0
  35. package/dist/types/src/components/builder-content.component.d.ts +4 -0
  36. package/dist/types/src/components/variants-provider.component.d.ts +2 -1
  37. package/dist/types/src/functions/should-force-browser-runtime-in-node.test.d.ts +1 -0
  38. package/dist/types/src/functions/string-to-function.test.d.ts +1 -0
  39. package/dist/types/src/sdk-version.d.ts +1 -1
  40. package/package.json +3 -2
  41. package/src/blocks/PersonalizationContainer.tsx +3 -0
  42. package/src/blocks/Router.tsx +2 -1
  43. package/src/blocks/Symbol.tsx +1 -0
  44. package/src/components/builder-component.component.tsx +8 -0
  45. package/src/components/builder-content.component.tsx +5 -1
  46. package/src/components/variants-provider.component.tsx +3 -1
  47. package/src/functions/should-force-browser-runtime-in-node.test.ts +67 -0
  48. package/src/functions/string-to-function.test.ts +335 -0
  49. 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
  },