@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.
- package/CHANGELOG.md +20 -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 +1 -1
- package/dist/lib/src/blocks/Video.js +17 -25
- package/dist/lib/src/blocks/Video.js.map +1 -1
- package/dist/lib/src/blocks/raw/Img.js +14 -1
- package/dist/lib/src/blocks/raw/Img.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/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 +2 -2
- package/src/blocks/Video.tsx +22 -34
- package/src/blocks/raw/Img.tsx +18 -2
- 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
package/src/blocks/Video.tsx
CHANGED
|
@@ -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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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 (
|
|
110
|
-
|
|
111
|
-
this.
|
|
97
|
+
if (this.lazyVideoObserver) {
|
|
98
|
+
this.lazyVideoObserver.disconnect();
|
|
99
|
+
this.lazyVideoObserver = null;
|
|
112
100
|
}
|
|
113
101
|
}
|
|
114
102
|
|
package/src/blocks/raw/Img.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
},
|