@builder.io/react 8.0.12 → 8.1.0-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 (34) hide show
  1. package/CHANGELOG.md +20 -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 +1 -1
  15. package/dist/lib/src/blocks/Video.js +17 -25
  16. package/dist/lib/src/blocks/Video.js.map +1 -1
  17. package/dist/lib/src/blocks/raw/Img.js +14 -1
  18. package/dist/lib/src/blocks/raw/Img.js.map +1 -1
  19. package/dist/lib/src/functions/should-force-browser-runtime-in-node.test.js +59 -0
  20. package/dist/lib/src/functions/should-force-browser-runtime-in-node.test.js.map +1 -0
  21. package/dist/lib/src/functions/string-to-function.js +4 -1
  22. package/dist/lib/src/functions/string-to-function.js.map +1 -1
  23. package/dist/lib/src/functions/string-to-function.test.js +289 -0
  24. package/dist/lib/src/functions/string-to-function.test.js.map +1 -0
  25. package/dist/lib/src/sdk-version.js +1 -1
  26. package/dist/types/src/functions/should-force-browser-runtime-in-node.test.d.ts +1 -0
  27. package/dist/types/src/functions/string-to-function.test.d.ts +1 -0
  28. package/dist/types/src/sdk-version.d.ts +1 -1
  29. package/package.json +2 -2
  30. package/src/blocks/Video.tsx +22 -34
  31. package/src/blocks/raw/Img.tsx +18 -2
  32. package/src/functions/should-force-browser-runtime-in-node.test.ts +67 -0
  33. package/src/functions/string-to-function.test.ts +335 -0
  34. package/src/functions/string-to-function.ts +8 -0
@@ -2,7 +2,6 @@
2
2
  import { jsx } from '@emotion/core';
3
3
  import React, { PropsWithChildren } from 'react';
4
4
 
5
- import { throttle } from '../functions/throttle';
6
5
  import { withChildren } from '../functions/with-children';
7
6
  import { Builder } from '@builder.io/sdk';
8
7
  import { IMAGE_FILE_TYPES, VIDEO_FILE_TYPES } from 'src/constants/file-types.constant';
@@ -30,8 +29,7 @@ class VideoComponent extends React.Component<
30
29
  > {
31
30
  video: HTMLVideoElement | null = null;
32
31
  containerRef: HTMLElement | null = null;
33
-
34
- scrollListener: null | ((e: Event) => void) = null;
32
+ lazyVideoObserver: IntersectionObserver | null = null;
35
33
 
36
34
  get lazyLoad() {
37
35
  // Default is true, must be explicitly turned off to not have this behavior
@@ -72,43 +70,33 @@ class VideoComponent extends React.Component<
72
70
  this.updateVideo();
73
71
 
74
72
  if (this.lazyLoad && Builder.isBrowser) {
75
- // TODO: have a way to consolidate all listeners into one timer
76
- // to avoid excessive reflows
77
- const listener = throttle(
78
- (event: Event) => {
79
- if (this.containerRef) {
80
- const rect = this.containerRef.getBoundingClientRect();
81
- const buffer = window.innerHeight / 2;
82
- if (rect.top < window.innerHeight + buffer) {
83
- this.setState(state => ({
84
- ...state,
85
- load: true,
86
- }));
87
- window.removeEventListener('scroll', listener);
88
- this.scrollListener = null;
89
- }
90
- }
91
- },
92
- 400,
93
- {
94
- leading: false,
95
- trailing: true,
96
- }
97
- );
98
- this.scrollListener = listener;
73
+ const observer = new IntersectionObserver(entries => {
74
+ entries.forEach(entry => {
75
+ if (!entry.isIntersecting) return;
99
76
 
100
- window.addEventListener('scroll', listener, {
101
- capture: true,
102
- passive: true,
77
+ this.setState(state => ({
78
+ ...state,
79
+ load: true,
80
+ }));
81
+
82
+ if (this.lazyVideoObserver) {
83
+ this.lazyVideoObserver.disconnect();
84
+ this.lazyVideoObserver = null;
85
+ }
86
+ });
103
87
  });
104
- listener();
88
+
89
+ if (this.containerRef) {
90
+ observer.observe(this.containerRef);
91
+ this.lazyVideoObserver = observer;
92
+ }
105
93
  }
106
94
  }
107
95
 
108
96
  componentWillUnmount() {
109
- if (Builder.isBrowser && this.scrollListener) {
110
- window.removeEventListener('scroll', this.scrollListener);
111
- this.scrollListener = null;
97
+ if (this.lazyVideoObserver) {
98
+ this.lazyVideoObserver.disconnect();
99
+ this.lazyVideoObserver = null;
112
100
  }
113
101
  }
114
102
 
@@ -3,6 +3,7 @@ import React from 'react';
3
3
  import { BuilderElement } from '@builder.io/sdk';
4
4
  import { withBuilder } from '../../functions/with-builder';
5
5
  import { IMAGE_FILE_TYPES } from 'src/constants/file-types.constant';
6
+ import { getSrcSet } from '../Image';
6
7
 
7
8
  export interface ImgProps {
8
9
  attributes?: any;
@@ -13,14 +14,29 @@ export interface ImgProps {
13
14
  // TODO: srcset, alt text input, object size/position input, etc
14
15
 
15
16
  class ImgComponent extends React.Component<ImgProps> {
17
+ getSrcSet(): string | undefined {
18
+ const url = this.props.image;
19
+ if (!url || typeof url !== 'string') {
20
+ return;
21
+ }
22
+
23
+ // We can auto add srcset for cdn.builder.io images
24
+ if (!url.match(/builder\.io/)) {
25
+ return;
26
+ }
27
+
28
+ return getSrcSet(url);
29
+ }
30
+
16
31
  render() {
17
32
  const attributes = this.props.attributes || {};
33
+ const srcset = this.getSrcSet();
18
34
  return (
19
35
  <img
36
+ loading="lazy"
20
37
  {...this.props.attributes}
21
38
  src={this.props.image || attributes.src}
22
- // TODO: generate this
23
- // srcSet={this.props.image || attributes.srcSet || attributes.srcset}
39
+ srcSet={srcset}
24
40
  />
25
41
  );
26
42
  }
@@ -0,0 +1,67 @@
1
+ import { shouldForceBrowserRuntimeInNode } from './should-force-browser-runtime-in-node';
2
+
3
+ describe('shouldForceBrowserRuntimeInNode', () => {
4
+ const originalArch = process.arch;
5
+ const originalVersion = process.version;
6
+ const originalNodeOptions = process.env.NODE_OPTIONS;
7
+ const originalConsoleLog = console.log;
8
+
9
+ beforeEach(() => {
10
+ // Mock console.log to prevent actual logging during tests
11
+ console.log = jest.fn();
12
+ });
13
+
14
+ afterEach(() => {
15
+ // Restore original process properties
16
+ Object.defineProperty(process, 'arch', { value: originalArch });
17
+ Object.defineProperty(process, 'version', { value: originalVersion });
18
+ process.env.NODE_OPTIONS = originalNodeOptions;
19
+ console.log = originalConsoleLog;
20
+ });
21
+
22
+ it('should return false when not in Node runtime', () => {
23
+ // Save original process
24
+ const originalProcess = global.process;
25
+
26
+ try {
27
+ // Mock not being in Node runtime
28
+ // @ts-ignore - Intentionally modifying global.process for test
29
+ global.process = undefined;
30
+
31
+ expect(shouldForceBrowserRuntimeInNode()).toBe(false);
32
+ } finally {
33
+ // Restore original process
34
+ global.process = originalProcess;
35
+ }
36
+ });
37
+
38
+ it('should return false when not on arm64 architecture', () => {
39
+ Object.defineProperty(process, 'arch', { value: 'x64' });
40
+ Object.defineProperty(process, 'version', { value: 'v20.0.0' });
41
+ expect(shouldForceBrowserRuntimeInNode()).toBe(false);
42
+ });
43
+
44
+ it('should return false when not on Node 20', () => {
45
+ Object.defineProperty(process, 'arch', { value: 'arm64' });
46
+ Object.defineProperty(process, 'version', { value: 'v18.0.0' });
47
+ expect(shouldForceBrowserRuntimeInNode()).toBe(false);
48
+ });
49
+
50
+ it('should return false when on arm64 and Node 20 but has no-node-snapshot option', () => {
51
+ Object.defineProperty(process, 'arch', { value: 'arm64' });
52
+ Object.defineProperty(process, 'version', { value: 'v20.0.0' });
53
+ process.env.NODE_OPTIONS = '--no-node-snapshot';
54
+ expect(shouldForceBrowserRuntimeInNode()).toBe(false);
55
+ });
56
+
57
+ it('should return true and log warning when on arm64, Node 20, and no snapshot option flag', () => {
58
+ Object.defineProperty(process, 'arch', { value: 'arm64' });
59
+ Object.defineProperty(process, 'version', { value: 'v20.0.0' });
60
+ process.env.NODE_OPTIONS = '';
61
+
62
+ const result = shouldForceBrowserRuntimeInNode();
63
+
64
+ expect(result).toBe(true);
65
+ expect(console.log).toHaveBeenCalled();
66
+ });
67
+ });
@@ -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
  },