@usebruno/js 0.46.1 → 0.48.0
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/package.json +9 -7
- package/src/bru.js +173 -75
- package/src/bruno-request.js +15 -7
- package/src/bruno-response.js +4 -0
- package/src/cookie-list.js +272 -0
- package/src/header-list.js +497 -0
- package/src/index.js +27 -2
- package/src/property-list.js +184 -0
- package/src/readonly-property-list.js +227 -0
- package/src/runtime/assert-runtime.js +164 -10
- package/src/runtime/script-runtime.js +55 -6
- package/src/runtime/test-runtime.js +19 -1
- package/src/runtime/vars-runtime.js +20 -3
- package/src/sandbox/bundle-browser-rollup.js +72 -66
- package/src/sandbox/bundle-libraries.js +10 -2
- package/src/sandbox/quickjs/index.js +8 -41
- package/src/sandbox/quickjs/shims/bru.js +31 -1
- package/src/sandbox/quickjs/shims/bruno-request.js +24 -3
- package/src/sandbox/quickjs/shims/bruno-response.js +57 -5
- package/src/sandbox/quickjs/shims/bruno-response.spec.js +91 -0
- package/src/sandbox/quickjs/shims/lib/uuid.spec.js +166 -0
- package/src/sandbox/quickjs/shims/require.js +56 -0
- package/src/sandbox/quickjs/shims/require.spec.js +154 -0
- package/src/sandbox/quickjs/shims/test.js +175 -2
- package/src/sandbox/quickjs/utils/property-list-bridge.js +190 -0
- package/src/sandbox/quickjs/utils/test-helpers.js +31 -0
- package/src/utils/error-formatter.js +345 -20
- package/src/utils/error-formatter.spec.js +683 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const { describe, it, expect, beforeAll, beforeEach, afterEach, afterAll } = require('@jest/globals');
|
|
2
|
+
const { newQuickJSWASMModule } = require('quickjs-emscripten');
|
|
3
|
+
const { addRequireShimToContext, getRequireCode } = require('./require');
|
|
4
|
+
const { createEvalHelper } = require('../utils/test-helpers');
|
|
5
|
+
|
|
6
|
+
describe('require shim tests', () => {
|
|
7
|
+
let vm, module, evalAndDump;
|
|
8
|
+
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
module = await newQuickJSWASMModule();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vm = module.newContext();
|
|
15
|
+
evalAndDump = createEvalHelper(vm);
|
|
16
|
+
// Initialize empty requireObject
|
|
17
|
+
vm.evalCode('globalThis.requireObject = {}');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
if (vm) {
|
|
22
|
+
try {
|
|
23
|
+
vm.dispose();
|
|
24
|
+
} catch (err) {
|
|
25
|
+
// Ignore disposal errors
|
|
26
|
+
}
|
|
27
|
+
vm = null;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterAll(() => {
|
|
32
|
+
if (module) {
|
|
33
|
+
try {
|
|
34
|
+
module.dispose();
|
|
35
|
+
} catch (err) {
|
|
36
|
+
// Ignore disposal errors
|
|
37
|
+
}
|
|
38
|
+
module = null;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('getRequireCode', () => {
|
|
43
|
+
it('should return a string', () => {
|
|
44
|
+
expect(typeof getRequireCode()).toBe('string');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should contain require function definition', () => {
|
|
48
|
+
const code = getRequireCode();
|
|
49
|
+
expect(code).toContain('globalThis.require');
|
|
50
|
+
expect(code).toContain('requireObject');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('addRequireShimToContext', () => {
|
|
55
|
+
it('should add require function to the VM context', () => {
|
|
56
|
+
addRequireShimToContext(vm);
|
|
57
|
+
const typeOfRequire = evalAndDump('typeof globalThis.require');
|
|
58
|
+
expect(typeOfRequire).toBe('function');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should return module from requireObject', () => {
|
|
62
|
+
addRequireShimToContext(vm);
|
|
63
|
+
|
|
64
|
+
// Register a mock module
|
|
65
|
+
vm.evalCode(`
|
|
66
|
+
globalThis.requireObject['test-module'] = { foo: 'bar', answer: 42 };
|
|
67
|
+
`);
|
|
68
|
+
|
|
69
|
+
const result = evalAndDump(`
|
|
70
|
+
const mod = require('test-module');
|
|
71
|
+
[mod.foo, mod.answer];
|
|
72
|
+
`);
|
|
73
|
+
|
|
74
|
+
expect(result).toEqual(['bar', 42]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should support destructuring from required modules', () => {
|
|
78
|
+
addRequireShimToContext(vm, { enableLocalModules: false });
|
|
79
|
+
|
|
80
|
+
vm.evalCode(`
|
|
81
|
+
globalThis.requireObject['my-lib'] = {
|
|
82
|
+
greet: (name) => 'Hello, ' + name,
|
|
83
|
+
VERSION: '1.0.0'
|
|
84
|
+
};
|
|
85
|
+
`);
|
|
86
|
+
|
|
87
|
+
const [greeting, version] = evalAndDump(`
|
|
88
|
+
const { greet, VERSION } = require('my-lib');
|
|
89
|
+
[greet('World'), VERSION];
|
|
90
|
+
`);
|
|
91
|
+
|
|
92
|
+
expect(greeting).toBe('Hello, World');
|
|
93
|
+
expect(version).toBe('1.0.0');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should support aliased destructuring', () => {
|
|
97
|
+
addRequireShimToContext(vm);
|
|
98
|
+
|
|
99
|
+
vm.evalCode(`
|
|
100
|
+
globalThis.requireObject['utils'] = { v1: () => 'version-1' };
|
|
101
|
+
`);
|
|
102
|
+
|
|
103
|
+
const result = evalAndDump(`
|
|
104
|
+
const { v1: getVersion } = require('utils');
|
|
105
|
+
getVersion();
|
|
106
|
+
`);
|
|
107
|
+
|
|
108
|
+
expect(result).toBe('version-1');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should throw error for unknown modules', () => {
|
|
112
|
+
addRequireShimToContext(vm);
|
|
113
|
+
|
|
114
|
+
const result = vm.evalCode(`
|
|
115
|
+
try {
|
|
116
|
+
require('non-existent-module');
|
|
117
|
+
'no error';
|
|
118
|
+
} catch (e) {
|
|
119
|
+
e.message;
|
|
120
|
+
}
|
|
121
|
+
`);
|
|
122
|
+
const handle = vm.unwrapResult(result);
|
|
123
|
+
const errorMessage = vm.dump(handle);
|
|
124
|
+
handle.dispose();
|
|
125
|
+
|
|
126
|
+
expect(errorMessage).toContain('Cannot find module');
|
|
127
|
+
expect(errorMessage).toContain('non-existent-module');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should allow requiring the same module multiple times', () => {
|
|
131
|
+
addRequireShimToContext(vm);
|
|
132
|
+
|
|
133
|
+
vm.evalCode(`
|
|
134
|
+
globalThis.requireObject['counter'] = { count: 0 };
|
|
135
|
+
`);
|
|
136
|
+
|
|
137
|
+
const result = evalAndDump(`
|
|
138
|
+
const mod1 = require('counter');
|
|
139
|
+
const mod2 = require('counter');
|
|
140
|
+
mod1 === mod2;
|
|
141
|
+
`);
|
|
142
|
+
|
|
143
|
+
expect(result).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('enableLocalModules option', () => {
|
|
148
|
+
it('should include local module loading code when enabled', () => {
|
|
149
|
+
const code = getRequireCode();
|
|
150
|
+
expect(code).toContain('isModuleAPath');
|
|
151
|
+
expect(code).toContain('__brunoLoadLocalModule');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -69,8 +69,8 @@ const addBruShimToContext = (vm, __brunoTestResults) => {
|
|
|
69
69
|
Object.defineProperty(proto, 'json', {
|
|
70
70
|
get: function () {
|
|
71
71
|
var obj = this._obj;
|
|
72
|
-
var isJson = typeof obj === 'object' && obj !== null &&
|
|
73
|
-
Object.prototype.toString.call(obj) === '[object Object]';
|
|
72
|
+
var isJson = typeof obj === 'object' && obj !== null &&
|
|
73
|
+
(Array.isArray(obj) || Object.prototype.toString.call(obj) === '[object Object]');
|
|
74
74
|
this.assert(isJson, 'expected #{this} to be JSON', 'expected #{this} not to be JSON');
|
|
75
75
|
return this;
|
|
76
76
|
},
|
|
@@ -79,6 +79,179 @@ const addBruShimToContext = (vm, __brunoTestResults) => {
|
|
|
79
79
|
})();
|
|
80
80
|
`
|
|
81
81
|
);
|
|
82
|
+
// Register custom chai assertion for jsonSchema (expect(...).to.have.jsonSchema(schema, options))
|
|
83
|
+
vm.evalCode(
|
|
84
|
+
`
|
|
85
|
+
(function() {
|
|
86
|
+
var Ajv = require('ajv');
|
|
87
|
+
var addFormats = require('ajv-formats');
|
|
88
|
+
var defaultAjv = new Ajv({ allErrors: true });
|
|
89
|
+
addFormats(defaultAjv);
|
|
90
|
+
var SUPPORTED_SCHEMA_VERSIONS = [
|
|
91
|
+
'http://json-schema.org/draft-07/schema#',
|
|
92
|
+
'http://json-schema.org/draft-07/schema'
|
|
93
|
+
];
|
|
94
|
+
var proto = Object.getPrototypeOf(expect(null));
|
|
95
|
+
proto.jsonSchema = function(schema, ajvOptions) {
|
|
96
|
+
if (schema && schema.$schema && !SUPPORTED_SCHEMA_VERSIONS.includes(schema.$schema)) {
|
|
97
|
+
this.assert(
|
|
98
|
+
false,
|
|
99
|
+
'Unsupported JSON Schema version: "' + schema.$schema + '". Bruno currently only supports Draft-07 (http://json-schema.org/draft-07/schema#). Please update your schema to be Draft-07 compatible and remove the $schema property.',
|
|
100
|
+
'Unsupported JSON Schema version: "' + schema.$schema + '".'
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
var ajv;
|
|
104
|
+
if (ajvOptions) {
|
|
105
|
+
ajv = new Ajv(Object.assign({ allErrors: true }, ajvOptions));
|
|
106
|
+
addFormats(ajv);
|
|
107
|
+
} else {
|
|
108
|
+
ajv = defaultAjv;
|
|
109
|
+
}
|
|
110
|
+
var validate;
|
|
111
|
+
try {
|
|
112
|
+
validate = ajv.compile(schema);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
this.assert(false, 'JSON schema compile error: ' + e.message, 'JSON schema compile error: ' + e.message);
|
|
115
|
+
}
|
|
116
|
+
var data = this._obj;
|
|
117
|
+
var isValid = validate(data);
|
|
118
|
+
|
|
119
|
+
var dataStr;
|
|
120
|
+
try { dataStr = JSON.stringify(data); } catch (e) { dataStr = '[unserializable value]'; }
|
|
121
|
+
this.assert(
|
|
122
|
+
isValid,
|
|
123
|
+
'expected ' + dataStr + ' to match JSON schema, validation errors: ' + (validate.errors ? JSON.stringify(validate.errors) : 'none'),
|
|
124
|
+
'expected ' + dataStr + ' to not match JSON schema'
|
|
125
|
+
);
|
|
126
|
+
return this;
|
|
127
|
+
};
|
|
128
|
+
})();
|
|
129
|
+
`
|
|
130
|
+
);
|
|
131
|
+
// Register custom chai assertion for jsonBody (Postman parity)
|
|
132
|
+
vm.evalCode(
|
|
133
|
+
`
|
|
134
|
+
(function() {
|
|
135
|
+
var proto = Object.getPrototypeOf(expect(null));
|
|
136
|
+
|
|
137
|
+
// Parse a property path into an array of keys.
|
|
138
|
+
// Handles: dot notation (a.b), numeric brackets (a[0]), quoted brackets (a["b.c"], a['key']),
|
|
139
|
+
// and combinations like data[0]["a.b"].name
|
|
140
|
+
//
|
|
141
|
+
// Examples:
|
|
142
|
+
// "a.b.c" -> ["a", "b", "c"]
|
|
143
|
+
// "items[0].name" -> ["items", "0", "name"]
|
|
144
|
+
// 'data["a.b"]' -> ["data", "a.b"]
|
|
145
|
+
// "matrix[0][1]" -> ["matrix", "0", "1"]
|
|
146
|
+
// 'nested["x.y"].z' -> ["nested", "x.y", "z"]
|
|
147
|
+
// '["say \\"hi\\""]' -> ["say \\"hi\\""]
|
|
148
|
+
function parsePath(path) {
|
|
149
|
+
var keys = [];
|
|
150
|
+
var i = 0;
|
|
151
|
+
while (i < path.length) {
|
|
152
|
+
if (path[i] === '.') {
|
|
153
|
+
i++;
|
|
154
|
+
} else if (path[i] === '[') {
|
|
155
|
+
i++;
|
|
156
|
+
if (i < path.length && (path[i] === "'" || path[i] === '"')) {
|
|
157
|
+
var quote = path[i];
|
|
158
|
+
i++;
|
|
159
|
+
var key = '';
|
|
160
|
+
while (i < path.length && path[i] !== quote) {
|
|
161
|
+
if (path[i] === '\\\\' && i + 1 < path.length && path[i + 1] === quote) {
|
|
162
|
+
key += quote;
|
|
163
|
+
i += 2;
|
|
164
|
+
} else {
|
|
165
|
+
key += path[i];
|
|
166
|
+
i++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
i++; // skip closing quote
|
|
170
|
+
i++; // skip ']'
|
|
171
|
+
keys.push(key);
|
|
172
|
+
} else {
|
|
173
|
+
var key = '';
|
|
174
|
+
while (i < path.length && path[i] !== ']') {
|
|
175
|
+
key += path[i];
|
|
176
|
+
i++;
|
|
177
|
+
}
|
|
178
|
+
i++; // skip ']'
|
|
179
|
+
keys.push(key);
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
var key = '';
|
|
183
|
+
while (i < path.length && path[i] !== '.' && path[i] !== '[') {
|
|
184
|
+
key += path[i];
|
|
185
|
+
i++;
|
|
186
|
+
}
|
|
187
|
+
keys.push(key);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return keys;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function getNestedValue(obj, path) {
|
|
194
|
+
var keys = parsePath(path);
|
|
195
|
+
var current = obj;
|
|
196
|
+
for (var i = 0; i < keys.length; i++) {
|
|
197
|
+
var key = keys[i];
|
|
198
|
+
if (current === null || current === undefined || !Object.prototype.hasOwnProperty.call(Object(current), key)) {
|
|
199
|
+
return { found: false };
|
|
200
|
+
}
|
|
201
|
+
current = current[key];
|
|
202
|
+
}
|
|
203
|
+
return { found: true, value: current };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function deepEqual(a, b) {
|
|
207
|
+
if (a === b) return true;
|
|
208
|
+
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false;
|
|
209
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
210
|
+
var keysA = Object.keys(a);
|
|
211
|
+
var keysB = Object.keys(b);
|
|
212
|
+
if (keysA.length !== keysB.length) return false;
|
|
213
|
+
for (var i = 0; i < keysA.length; i++) {
|
|
214
|
+
if (!Object.prototype.hasOwnProperty.call(b, keysA[i]) || !deepEqual(a[keysA[i]], b[keysA[i]])) return false;
|
|
215
|
+
}
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
proto.jsonBody = function() {
|
|
220
|
+
var obj = this._obj;
|
|
221
|
+
var args = Array.prototype.slice.call(arguments);
|
|
222
|
+
|
|
223
|
+
if (args.length === 0) {
|
|
224
|
+
this.assert(
|
|
225
|
+
typeof obj === 'object' && obj !== null,
|
|
226
|
+
'expected value to be a JSON body (object or array)',
|
|
227
|
+
'expected value not to be a JSON body'
|
|
228
|
+
);
|
|
229
|
+
} else if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
|
|
230
|
+
this.assert(
|
|
231
|
+
deepEqual(obj, args[0]),
|
|
232
|
+
'expected body to deeply equal given object',
|
|
233
|
+
'expected body to not deeply equal given object'
|
|
234
|
+
);
|
|
235
|
+
} else if (args.length === 1) {
|
|
236
|
+
var result = getNestedValue(obj, String(args[0]));
|
|
237
|
+
this.assert(
|
|
238
|
+
result.found,
|
|
239
|
+
"expected body to have nested property '" + args[0] + "'",
|
|
240
|
+
"expected body to not have nested property '" + args[0] + "'"
|
|
241
|
+
);
|
|
242
|
+
} else {
|
|
243
|
+
var result = getNestedValue(obj, String(args[0]));
|
|
244
|
+
this.assert(
|
|
245
|
+
result.found && deepEqual(result.value, args[1]),
|
|
246
|
+
"expected body to have nested property '" + args[0] + "' equal to given value",
|
|
247
|
+
"expected body to not have nested property '" + args[0] + "' equal to given value"
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return this;
|
|
251
|
+
};
|
|
252
|
+
})();
|
|
253
|
+
`
|
|
254
|
+
);
|
|
82
255
|
};
|
|
83
256
|
|
|
84
257
|
module.exports = addBruShimToContext;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
const { cleanJson, cleanCircularJson } = require('../../../utils');
|
|
2
|
+
const { marshallToVm } = require('../utils');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates an async bridge that resolves with `undefined` (write-only).
|
|
6
|
+
* Do NOT reuse this for read methods that need to return values —
|
|
7
|
+
* those require resolving with the callback's result argument instead.
|
|
8
|
+
*/
|
|
9
|
+
const createAsyncBridge = (vm, targetObj, propName, nativeMethod) => {
|
|
10
|
+
const fn = vm.newFunction(propName, (...vmArgs) => {
|
|
11
|
+
const promise = vm.newPromise();
|
|
12
|
+
const args = vmArgs.map((a) => vm.dump(a));
|
|
13
|
+
nativeMethod(...args, (err) => {
|
|
14
|
+
if (err) {
|
|
15
|
+
promise.reject(marshallToVm(cleanJson(err), vm));
|
|
16
|
+
} else {
|
|
17
|
+
promise.resolve(vm.undefined);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
promise.settled.then(vm.runtime.executePendingJobs);
|
|
21
|
+
return promise.handle;
|
|
22
|
+
});
|
|
23
|
+
fn.consume((handle) => vm.setProp(targetObj, propName, handle));
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Factory that auto-wires PropertyList methods onto a QuickJS VM object.
|
|
28
|
+
*
|
|
29
|
+
* Generates:
|
|
30
|
+
* - Sync read methods: `vm.newFunction` → `marshallToVm(nativeList.method(...args), vm)`
|
|
31
|
+
* - Sync read object methods: same but wrapped with `cleanCircularJson()`
|
|
32
|
+
* - Async write methods: `_prefix` bridge pattern (native callback → QuickJS promise)
|
|
33
|
+
* - Returns `{ evalCode }` string containing `callWithCallback` helper + async wrappers + iterators
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* In shims/bru.js, wiring up bru.cookies takes a single call:
|
|
37
|
+
*
|
|
38
|
+
* const { evalCode: cookiesEvalCode } = createPropertyListBridge(vm, bru.cookies, bruCookiesObject, {
|
|
39
|
+
* globalPath: 'globalThis.bru.cookies',
|
|
40
|
+
* syncReadMethods: ['get', 'has', 'count', 'indexOf', 'toObject', 'toString'],
|
|
41
|
+
* syncReadObjectMethods: ['one', 'all', 'idx', 'toJSON'],
|
|
42
|
+
* asyncWriteMethods: ['add', 'upsert', 'remove', 'clear', 'delete'],
|
|
43
|
+
* withIterators: true
|
|
44
|
+
* });
|
|
45
|
+
*
|
|
46
|
+
* Without this factory, each method would require manual boilerplate like the
|
|
47
|
+
* hand-written jar() bridge in bru.js (~100 lines), where every method needs:
|
|
48
|
+
*
|
|
49
|
+
* const _fn = vm.newFunction('_method', (...vmArgs) => {
|
|
50
|
+
* const promise = vm.newPromise();
|
|
51
|
+
* nativeObj.method(vm.dump(vmArgs[0]), (err, result) => {
|
|
52
|
+
* if (err) {
|
|
53
|
+
* promise.reject(marshallToVm(cleanJson(err), vm));
|
|
54
|
+
* } else {
|
|
55
|
+
* promise.resolve(marshallToVm(cleanCircularJson(result), vm));
|
|
56
|
+
* }
|
|
57
|
+
* });
|
|
58
|
+
* promise.settled.then(vm.runtime.executePendingJobs);
|
|
59
|
+
* return promise.handle;
|
|
60
|
+
* });
|
|
61
|
+
* _fn.consume((handle) => vm.setProp(obj, '_method', handle));
|
|
62
|
+
*
|
|
63
|
+
* …repeated for every method, plus separate evalCode for async wrappers.
|
|
64
|
+
*
|
|
65
|
+
* To wire up a new PropertyList-backed object, add one createPropertyListBridge
|
|
66
|
+
* call instead of duplicating all that boilerplate.
|
|
67
|
+
*
|
|
68
|
+
* @param {Object} vm - QuickJS VM instance
|
|
69
|
+
* @param {Object} nativeList - Native PropertyList instance
|
|
70
|
+
* @param {Object} targetObj - QuickJS object handle to attach methods to
|
|
71
|
+
* @param {Object} options
|
|
72
|
+
* @param {string} options.globalPath - Global path in QuickJS (e.g. 'globalThis.bru.cookies')
|
|
73
|
+
* @param {string[]} [options.syncReadMethods] - Methods that return primitive values
|
|
74
|
+
* @param {string[]} [options.syncReadObjectMethods] - Methods that return objects (need cleanCircularJson)
|
|
75
|
+
* @param {string[]} [options.asyncWriteMethods] - Async write methods (use _prefix bridge)
|
|
76
|
+
* @param {boolean} [options.withIterators] - Whether to add each/find/filter/map/reduce
|
|
77
|
+
* @returns {{ evalCode: string }} - JavaScript code to eval in the VM for async wrappers and iterators
|
|
78
|
+
*/
|
|
79
|
+
const createPropertyListBridge = (vm, nativeList, targetObj, options) => {
|
|
80
|
+
const {
|
|
81
|
+
globalPath,
|
|
82
|
+
syncReadMethods = [],
|
|
83
|
+
syncReadObjectMethods = [],
|
|
84
|
+
syncWriteMethods = [],
|
|
85
|
+
asyncWriteMethods = [],
|
|
86
|
+
withIterators = false
|
|
87
|
+
} = options;
|
|
88
|
+
|
|
89
|
+
// Sync read methods — return primitive values
|
|
90
|
+
for (const methodName of syncReadMethods) {
|
|
91
|
+
const fn = vm.newFunction(methodName, (...vmArgs) => {
|
|
92
|
+
const args = vmArgs.map((a) => vm.dump(a));
|
|
93
|
+
return marshallToVm(nativeList[methodName](...args), vm);
|
|
94
|
+
});
|
|
95
|
+
fn.consume((handle) => vm.setProp(targetObj, methodName, handle));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Sync read object methods — need cleanCircularJson
|
|
99
|
+
for (const methodName of syncReadObjectMethods) {
|
|
100
|
+
const fn = vm.newFunction(methodName, (...vmArgs) => {
|
|
101
|
+
const args = vmArgs.map((a) => vm.dump(a));
|
|
102
|
+
return marshallToVm(cleanCircularJson(nativeList[methodName](...args)), vm);
|
|
103
|
+
});
|
|
104
|
+
fn.consume((handle) => vm.setProp(targetObj, methodName, handle));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Sync write methods — void return, just call and discard
|
|
108
|
+
for (const methodName of syncWriteMethods) {
|
|
109
|
+
const fn = vm.newFunction(methodName, (...vmArgs) => {
|
|
110
|
+
const args = vmArgs.map((a) => vm.dump(a));
|
|
111
|
+
nativeList[methodName](...args);
|
|
112
|
+
return vm.undefined;
|
|
113
|
+
});
|
|
114
|
+
fn.consume((handle) => vm.setProp(targetObj, methodName, handle));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Async write methods — two-phase setup:
|
|
118
|
+
// Phase 1 (native): Register `_prefixed` bridge functions (e.g. `_add`, `_remove`) via
|
|
119
|
+
// createAsyncBridge. These are QuickJS promise-based wrappers that call the native method's
|
|
120
|
+
// callback API and resolve with `undefined` (write-only).
|
|
121
|
+
// Phase 2 (evalCode): Generates JS code eval'd in the VM that:
|
|
122
|
+
// 1. Defines a `callWithCallback` helper supporting both `await method(args)` and
|
|
123
|
+
// `method(args, callback)` calling styles.
|
|
124
|
+
// 2. Captures `_prefixed` direct references, then overwrites the public method name with
|
|
125
|
+
// a wrapper that auto-detects whether the last argument is a callback.
|
|
126
|
+
for (const methodName of asyncWriteMethods) {
|
|
127
|
+
createAsyncBridge(vm, targetObj, `_${methodName}`, (...a) => nativeList[methodName](...a));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let evalCode = '';
|
|
131
|
+
|
|
132
|
+
if (asyncWriteMethods.length > 0) {
|
|
133
|
+
evalCode += `const callWithCallback = async (promiseFn, callback) => {
|
|
134
|
+
if (!callback) return await promiseFn();
|
|
135
|
+
try {
|
|
136
|
+
const result = await promiseFn();
|
|
137
|
+
try { await callback(null, result); } catch(cbErr) { return Promise.reject(cbErr); }
|
|
138
|
+
} catch(err) {
|
|
139
|
+
try { await callback(err, null); } catch(cbErr) { return Promise.reject(cbErr); }
|
|
140
|
+
}
|
|
141
|
+
};\n`;
|
|
142
|
+
|
|
143
|
+
// Capture _prefixed direct references before overwriting
|
|
144
|
+
for (const methodName of asyncWriteMethods) {
|
|
145
|
+
evalCode += `const _${methodName}Direct = ${globalPath}._${methodName};\n`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Generate wrapper functions: method(...args, cb?) => callWithCallback(() => _direct(...args), cb)
|
|
149
|
+
for (const methodName of asyncWriteMethods) {
|
|
150
|
+
evalCode += `${globalPath}.${methodName} = (...args) => {
|
|
151
|
+
const cb = typeof args[args.length - 1] === 'function' ? args.pop() : undefined;
|
|
152
|
+
return callWithCallback(() => _${methodName}Direct(...args), cb);
|
|
153
|
+
};\n`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Iterators — these can't be bridged as syncReadObjectMethods because they take a callback
|
|
158
|
+
// function as an argument, and functions can't cross the native↔VM boundary (vm.dump() can't
|
|
159
|
+
// serialize them). Instead, we pull the data into the VM via `all()`, then run the array
|
|
160
|
+
// operation inside the VM where the callback lives. Requires `all` in `syncReadObjectMethods`.
|
|
161
|
+
if (withIterators) {
|
|
162
|
+
evalCode += `const _allNative = ${globalPath}.all;
|
|
163
|
+
${globalPath}.each = (fn, ctx) => { const b = ctx !== undefined ? fn.bind(ctx) : fn; _allNative().forEach(b); };
|
|
164
|
+
${globalPath}.filter = (fn, ctx) => { const b = ctx !== undefined ? fn.bind(ctx) : fn; return _allNative().filter(b); };
|
|
165
|
+
${globalPath}.find = (fn, ctx) => { const b = ctx !== undefined ? fn.bind(ctx) : fn; return _allNative().find(b); };
|
|
166
|
+
${globalPath}.map = (fn, ctx) => { const b = ctx !== undefined ? fn.bind(ctx) : fn; return _allNative().map(b); };
|
|
167
|
+
${globalPath}.reduce = (fn, ...rest) => { const ctx = rest.length > 1 ? rest[1] : undefined; const b = ctx !== undefined ? fn.bind(ctx) : fn; return rest.length > 0 ? _allNative().reduce(b, rest[0]) : _allNative().reduce(b); };\n`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Override `remove` when it's a syncWriteMethod so function predicates work in-VM.
|
|
171
|
+
// The native bridge can't serialize function handles (vm.dump fails on functions).
|
|
172
|
+
// Instead: pull items via all(), run the predicate in-VM, call native remove(key) per match.
|
|
173
|
+
if (withIterators && syncWriteMethods.includes('remove')) {
|
|
174
|
+
evalCode += `const _removeNative = ${globalPath}.remove;
|
|
175
|
+
${globalPath}.remove = (predicate) => {
|
|
176
|
+
if (typeof predicate === 'function') {
|
|
177
|
+
_allNative().filter(predicate).forEach(item => _removeNative(item.key));
|
|
178
|
+
} else {
|
|
179
|
+
_removeNative(predicate);
|
|
180
|
+
}
|
|
181
|
+
};\n`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { evalCode };
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
createPropertyListBridge,
|
|
189
|
+
createAsyncBridge
|
|
190
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evaluates code in a QuickJS VM and returns the dumped result.
|
|
3
|
+
* Handles unwrapping and disposing of handles automatically.
|
|
4
|
+
*
|
|
5
|
+
* @param {Object} vm - QuickJS VM context
|
|
6
|
+
* @param {string} code - JavaScript code to evaluate
|
|
7
|
+
* @returns {*} The evaluated and dumped result
|
|
8
|
+
*/
|
|
9
|
+
function evalAndDump(vm, code) {
|
|
10
|
+
const result = vm.evalCode(code);
|
|
11
|
+
const handle = vm.unwrapResult(result);
|
|
12
|
+
const value = vm.dump(handle);
|
|
13
|
+
handle.dispose();
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates a helper function bound to a specific VM instance.
|
|
19
|
+
* Useful in beforeEach to create a test-scoped helper.
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} vm - QuickJS VM context
|
|
22
|
+
* @returns {Function} evalAndDump function bound to the VM
|
|
23
|
+
*/
|
|
24
|
+
function createEvalHelper(vm) {
|
|
25
|
+
return (code) => evalAndDump(vm, code);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
evalAndDump,
|
|
30
|
+
createEvalHelper
|
|
31
|
+
};
|