@usebruno/js 0.47.0 → 0.49.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 +6 -4
- package/src/bru.js +26 -3
- package/src/bruno-request.js +13 -7
- package/src/bruno-response.js +4 -0
- package/src/header-list.js +497 -0
- package/src/runtime/assert-runtime.js +156 -0
- package/src/runtime/script-runtime.js +11 -10
- package/src/runtime/scripted-entries.js +16 -0
- package/src/runtime/test-runtime.js +6 -5
- package/src/sandbox/bundle-browser-rollup.js +72 -66
- package/src/sandbox/bundle-libraries.js +9 -1
- package/src/sandbox/quickjs/index.js +27 -13
- package/src/sandbox/quickjs/shims/bru.js +16 -0
- package/src/sandbox/quickjs/shims/bruno-request.js +24 -3
- package/src/sandbox/quickjs/shims/bruno-response.js +28 -3
- package/src/sandbox/quickjs/shims/test.js +173 -0
- package/src/sandbox/quickjs/utils/index.js +131 -1
- package/src/sandbox/quickjs/utils/property-list-bridge.js +30 -5
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const rollup = require('rollup');
|
|
2
2
|
const { nodeResolve } = require('@rollup/plugin-node-resolve');
|
|
3
3
|
const commonjs = require('@rollup/plugin-commonjs');
|
|
4
|
+
const json = require('@rollup/plugin-json');
|
|
4
5
|
const fs = require('fs');
|
|
5
6
|
const terser = require('@rollup/plugin-terser').default;
|
|
6
7
|
|
|
@@ -13,6 +14,8 @@ const bundleLibraries = async () => {
|
|
|
13
14
|
import atob from "atob";
|
|
14
15
|
import * as cryptoJs from 'crypto-js';
|
|
15
16
|
import tv4 from "tv4";
|
|
17
|
+
import Ajv from "ajv";
|
|
18
|
+
import addFormats from "ajv-formats";
|
|
16
19
|
globalThis.expect = expect;
|
|
17
20
|
globalThis.assert = assert;
|
|
18
21
|
globalThis.moment = moment;
|
|
@@ -20,6 +23,8 @@ const bundleLibraries = async () => {
|
|
|
20
23
|
globalThis.atob = atob;
|
|
21
24
|
globalThis.Buffer = Buffer;
|
|
22
25
|
globalThis.tv4 = tv4;
|
|
26
|
+
globalThis.Ajv = Ajv;
|
|
27
|
+
globalThis.addFormats = addFormats;
|
|
23
28
|
globalThis.requireObject = {
|
|
24
29
|
...(globalThis.requireObject || {}),
|
|
25
30
|
'chai': { expect, assert },
|
|
@@ -28,7 +33,9 @@ const bundleLibraries = async () => {
|
|
|
28
33
|
'btoa': btoa,
|
|
29
34
|
'atob': atob,
|
|
30
35
|
'crypto-js': cryptoJs,
|
|
31
|
-
'tv4': tv4
|
|
36
|
+
'tv4': tv4,
|
|
37
|
+
'ajv': Ajv,
|
|
38
|
+
'ajv-formats': addFormats
|
|
32
39
|
};
|
|
33
40
|
`;
|
|
34
41
|
|
|
@@ -56,6 +63,7 @@ const bundleLibraries = async () => {
|
|
|
56
63
|
browser: false
|
|
57
64
|
}),
|
|
58
65
|
commonjs(),
|
|
66
|
+
json(),
|
|
59
67
|
terser()
|
|
60
68
|
]
|
|
61
69
|
},
|
|
@@ -11,14 +11,13 @@ const { newQuickJSWASMModule, memoizePromiseFactory } = require('quickjs-emscrip
|
|
|
11
11
|
// execute `npm run sandbox:bundle-libraries` if the below file doesn't exist
|
|
12
12
|
const getBundledCode = require('../bundle-browser-rollup');
|
|
13
13
|
const addPathShimToContext = require('./shims/lib/path');
|
|
14
|
-
const { marshallToVm } = require('./utils');
|
|
14
|
+
const { marshallToVm, createManagedQuickJsContext } = require('./utils');
|
|
15
15
|
const addCryptoUtilsShimToContext = require('./shims/lib/crypto-utils');
|
|
16
16
|
const { wrapScriptInClosure, SANDBOX } = require('../../utils/sandbox');
|
|
17
17
|
|
|
18
|
-
let
|
|
18
|
+
let QuickJSModule;
|
|
19
19
|
const loader = memoizePromiseFactory(() => newQuickJSWASMModule());
|
|
20
|
-
|
|
21
|
-
getContext();
|
|
20
|
+
loader().then((mod) => (QuickJSModule = mod));
|
|
22
21
|
|
|
23
22
|
const toNumber = (value) => {
|
|
24
23
|
const num = Number(value);
|
|
@@ -57,10 +56,10 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc
|
|
|
57
56
|
|
|
58
57
|
externalScript = removeQuotes(externalScript);
|
|
59
58
|
}
|
|
60
|
-
|
|
61
|
-
const vm = QuickJSSyncContext;
|
|
62
|
-
|
|
59
|
+
let managedQuickJsContext;
|
|
63
60
|
try {
|
|
61
|
+
managedQuickJsContext = createManagedQuickJsContext(QuickJSModule);
|
|
62
|
+
const vm = managedQuickJsContext.vm;
|
|
64
63
|
const { bru, req, res, ...variables } = externalContext;
|
|
65
64
|
|
|
66
65
|
bru && addBruShimToContext(vm, bru);
|
|
@@ -76,7 +75,7 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc
|
|
|
76
75
|
|
|
77
76
|
let scriptText = scriptType === 'template-literal' ? templateLiteralText : jsExpressionText;
|
|
78
77
|
|
|
79
|
-
const result = vm.
|
|
78
|
+
const result = vm.evalCodeRetained(scriptText);
|
|
80
79
|
if (result.error) {
|
|
81
80
|
let e = vm.dump(result.error);
|
|
82
81
|
result.error.dispose();
|
|
@@ -88,6 +87,8 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc
|
|
|
88
87
|
}
|
|
89
88
|
} catch (error) {
|
|
90
89
|
console.error('Error executing the script!', error);
|
|
90
|
+
} finally {
|
|
91
|
+
managedQuickJsContext?.dispose();
|
|
91
92
|
}
|
|
92
93
|
};
|
|
93
94
|
|
|
@@ -97,9 +98,11 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
|
|
|
97
98
|
}
|
|
98
99
|
externalScript = externalScript?.trim();
|
|
99
100
|
|
|
101
|
+
let managedQuickJsContext;
|
|
100
102
|
try {
|
|
101
|
-
const module = await
|
|
102
|
-
|
|
103
|
+
const module = await loader();
|
|
104
|
+
managedQuickJsContext = createManagedQuickJsContext(module);
|
|
105
|
+
const vm = managedQuickJsContext.vm;
|
|
103
106
|
|
|
104
107
|
// add crypto utilities required by the crypto-js library in bundledCode
|
|
105
108
|
await addCryptoUtilsShimToContext(vm);
|
|
@@ -128,21 +131,32 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
|
|
|
128
131
|
|
|
129
132
|
const script = wrapScriptInClosure(externalScript, SANDBOX.QUICKJS);
|
|
130
133
|
|
|
131
|
-
const result = vm.
|
|
134
|
+
const result = vm.evalCodeRetained(script, scriptPath);
|
|
132
135
|
const promiseHandle = vm.unwrapResult(result);
|
|
133
136
|
const resolvedResult = await vm.resolvePromise(promiseHandle);
|
|
134
137
|
promiseHandle.dispose();
|
|
135
138
|
const resolvedHandle = vm.unwrapResult(resolvedResult);
|
|
136
139
|
resolvedHandle.dispose();
|
|
137
|
-
// vm.dispose();
|
|
138
140
|
return;
|
|
139
141
|
} catch (error) {
|
|
140
142
|
error.__isQuickJS = true;
|
|
141
143
|
throw error;
|
|
144
|
+
} finally {
|
|
145
|
+
// Wait for any in-flight async work (sendRequest, axios, cookie jar, timers,
|
|
146
|
+
// un-awaited promises) to settle before tearing down the VM. Disposing while
|
|
147
|
+
// a deferred is still pending lets its later host callback touch a freed
|
|
148
|
+
// context, throwing `QuickJSUseAfterFree`.
|
|
149
|
+
try {
|
|
150
|
+
await managedQuickJsContext?.waitForPendingDeferreds?.();
|
|
151
|
+
managedQuickJsContext?.dispose();
|
|
152
|
+
} catch (teardownError) {
|
|
153
|
+
throw teardownError;
|
|
154
|
+
}
|
|
142
155
|
}
|
|
143
156
|
};
|
|
144
157
|
|
|
145
158
|
module.exports = {
|
|
146
159
|
executeQuickJsVm,
|
|
147
|
-
executeQuickJsVmAsync
|
|
160
|
+
executeQuickJsVmAsync,
|
|
161
|
+
loader
|
|
148
162
|
};
|
|
@@ -117,6 +117,12 @@ const addBruShimToContext = (vm, bru) => {
|
|
|
117
117
|
vm.setProp(bruObject, 'getAllGlobalEnvVars', getAllGlobalEnvVars);
|
|
118
118
|
getAllGlobalEnvVars.dispose();
|
|
119
119
|
|
|
120
|
+
let hasGlobalEnvVar = vm.newFunction('hasGlobalEnvVar', function (key) {
|
|
121
|
+
return marshallToVm(bru.hasGlobalEnvVar(vm.dump(key)), vm);
|
|
122
|
+
});
|
|
123
|
+
vm.setProp(bruObject, 'hasGlobalEnvVar', hasGlobalEnvVar);
|
|
124
|
+
hasGlobalEnvVar.dispose();
|
|
125
|
+
|
|
120
126
|
// TODO: deleteAllGlobalEnvVars works in the request lifecycle but does not update the UI.
|
|
121
127
|
// Re-enable once the UI sync issue is resolved.
|
|
122
128
|
// let deleteAllGlobalEnvVars = vm.newFunction('deleteAllGlobalEnvVars', function () {
|
|
@@ -401,10 +407,20 @@ const addBruShimToContext = (vm, bru) => {
|
|
|
401
407
|
});
|
|
402
408
|
sendRequestHandle.consume((handle) => vm.setProp(bruObject, '_sendRequest', handle));
|
|
403
409
|
|
|
410
|
+
// On vm.global, not bru, to stay off user-facing autocomplete.
|
|
411
|
+
let setScopeHandle = vm.newFunction('__bruSetScope', (scopeArg) => {
|
|
412
|
+
bru._currentScope = vm.dump(scopeArg) || null;
|
|
413
|
+
});
|
|
414
|
+
setScopeHandle.consume((handle) => vm.setProp(vm.global, '__bruSetScope', handle));
|
|
415
|
+
|
|
404
416
|
const sleep = vm.newFunction('sleep', (timer) => {
|
|
405
417
|
const t = vm.getString(timer);
|
|
406
418
|
const promise = vm.newPromise();
|
|
407
419
|
setTimeout(() => {
|
|
420
|
+
// The VM may have been disposed while this native timer was pending
|
|
421
|
+
// (e.g. a setTimeout/sleep whose promise the script never awaited).
|
|
422
|
+
// Touching the VM after teardown throws QuickJSUseAfterFree, so bail out.
|
|
423
|
+
if (!vm.alive) return;
|
|
408
424
|
promise.resolve(vm.newString('slept'));
|
|
409
425
|
}, t);
|
|
410
426
|
promise.settled.then(vm.runtime.executePendingJobs);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
const { marshallToVm } = require('../utils');
|
|
2
|
+
const { createPropertyListBridge } = require('../utils/property-list-bridge');
|
|
2
3
|
|
|
3
4
|
const addBrunoRequestShimToContext = (vm, req) => {
|
|
4
5
|
const reqObject = vm.newObject();
|
|
5
6
|
|
|
6
7
|
const url = marshallToVm(req.getUrl(), vm);
|
|
7
8
|
const method = marshallToVm(req.getMethod(), vm);
|
|
8
|
-
const headers = marshallToVm(req.getHeaders(), vm);
|
|
9
9
|
const body = marshallToVm(req.getBody(), vm);
|
|
10
10
|
const timeout = marshallToVm(req.getTimeout(), vm);
|
|
11
11
|
const name = marshallToVm(req.getName(), vm);
|
|
@@ -14,7 +14,6 @@ const addBrunoRequestShimToContext = (vm, req) => {
|
|
|
14
14
|
|
|
15
15
|
vm.setProp(reqObject, 'url', url);
|
|
16
16
|
vm.setProp(reqObject, 'method', method);
|
|
17
|
-
vm.setProp(reqObject, 'headers', headers);
|
|
18
17
|
vm.setProp(reqObject, 'body', body);
|
|
19
18
|
vm.setProp(reqObject, 'timeout', timeout);
|
|
20
19
|
vm.setProp(reqObject, 'name', name);
|
|
@@ -23,13 +22,29 @@ const addBrunoRequestShimToContext = (vm, req) => {
|
|
|
23
22
|
|
|
24
23
|
url.dispose();
|
|
25
24
|
method.dispose();
|
|
26
|
-
headers.dispose();
|
|
27
25
|
body.dispose();
|
|
28
26
|
timeout.dispose();
|
|
29
27
|
name.dispose();
|
|
30
28
|
pathParams.dispose();
|
|
31
29
|
tags.dispose();
|
|
32
30
|
|
|
31
|
+
// req.headers — plain headers object for backward-compatible bracket access
|
|
32
|
+
const headersVal = marshallToVm(req.getHeaders(), vm);
|
|
33
|
+
vm.setProp(reqObject, 'headers', headersVal);
|
|
34
|
+
headersVal.dispose();
|
|
35
|
+
|
|
36
|
+
// req.headerList — PropertyList bridge for structured header operations
|
|
37
|
+
const headerListObj = vm.newObject();
|
|
38
|
+
const { evalCode: headersEvalCode } = createPropertyListBridge(vm, req.headerList, headerListObj, {
|
|
39
|
+
globalPath: 'globalThis.req.headerList',
|
|
40
|
+
syncReadMethods: ['get', 'has', 'count', 'indexOf', 'toObject', 'toString'],
|
|
41
|
+
syncReadObjectMethods: ['one', 'all', 'toJSON'],
|
|
42
|
+
syncWriteMethods: ['add', 'upsert', 'remove', 'clear', 'populate', 'repopulate', 'assimilate'],
|
|
43
|
+
withIterators: true
|
|
44
|
+
});
|
|
45
|
+
vm.setProp(reqObject, 'headerList', headerListObj);
|
|
46
|
+
headerListObj.dispose();
|
|
47
|
+
|
|
33
48
|
let getUrl = vm.newFunction('getUrl', function () {
|
|
34
49
|
return marshallToVm(req.getUrl(), vm);
|
|
35
50
|
});
|
|
@@ -177,6 +192,12 @@ const addBrunoRequestShimToContext = (vm, req) => {
|
|
|
177
192
|
|
|
178
193
|
vm.setProp(vm.global, 'req', reqObject);
|
|
179
194
|
reqObject.dispose();
|
|
195
|
+
|
|
196
|
+
// Evaluate iterator code after req is on global (iterators reference globalThis.req.headerList)
|
|
197
|
+
// Wrapped in a block to avoid const redeclaration conflicts with other evalCode blocks
|
|
198
|
+
if (headersEvalCode) {
|
|
199
|
+
vm.evalCode(`{ ${headersEvalCode} }`);
|
|
200
|
+
}
|
|
180
201
|
};
|
|
181
202
|
|
|
182
203
|
module.exports = addBrunoRequestShimToContext;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { marshallToVm } = require('../utils');
|
|
2
|
+
const { createPropertyListBridge } = require('../utils/property-list-bridge');
|
|
2
3
|
|
|
3
4
|
// Marshal a QuickJS query argument to a host-compatible value.
|
|
4
5
|
// Function handles are wrapped as native callbacks; other values are dumped as-is.
|
|
@@ -34,25 +35,43 @@ const addBrunoResponseShimToContext = (vm, res) => {
|
|
|
34
35
|
|
|
35
36
|
const status = marshallToVm(res?.status, vm);
|
|
36
37
|
const statusText = marshallToVm(res?.statusText, vm);
|
|
37
|
-
const headers = marshallToVm(res?.headers, vm);
|
|
38
38
|
const body = marshallToVm(res?.body, vm);
|
|
39
39
|
const responseTime = marshallToVm(res?.responseTime, vm);
|
|
40
40
|
const url = marshallToVm(res?.url, vm);
|
|
41
41
|
|
|
42
42
|
vm.setProp(resFn, 'status', status);
|
|
43
43
|
vm.setProp(resFn, 'statusText', statusText);
|
|
44
|
-
vm.setProp(resFn, 'headers', headers);
|
|
45
44
|
vm.setProp(resFn, 'body', body);
|
|
46
45
|
vm.setProp(resFn, 'responseTime', responseTime);
|
|
47
46
|
vm.setProp(resFn, 'url', url);
|
|
48
47
|
|
|
49
48
|
status.dispose();
|
|
50
|
-
headers.dispose();
|
|
51
49
|
body.dispose();
|
|
52
50
|
responseTime.dispose();
|
|
53
51
|
url.dispose();
|
|
54
52
|
statusText.dispose();
|
|
55
53
|
|
|
54
|
+
// res.headers — plain headers object for backward-compatible bracket access
|
|
55
|
+
const headersVal = marshallToVm(res?.headers || {}, vm);
|
|
56
|
+
vm.setProp(resFn, 'headers', headersVal);
|
|
57
|
+
headersVal.dispose();
|
|
58
|
+
|
|
59
|
+
// res.headerList — read-only PropertyList bridge for structured header operations
|
|
60
|
+
let resHeadersEvalCode = '';
|
|
61
|
+
if (res?.headerList) {
|
|
62
|
+
const headerListObj = vm.newObject();
|
|
63
|
+
const bridge = createPropertyListBridge(vm, res.headerList, headerListObj, {
|
|
64
|
+
globalPath: 'globalThis.res.headerList',
|
|
65
|
+
syncReadMethods: ['get', 'has', 'count', 'indexOf', 'toObject', 'toString'],
|
|
66
|
+
syncReadObjectMethods: ['one', 'all', 'toJSON'],
|
|
67
|
+
syncWriteMethods: ['add', 'upsert', 'remove', 'clear', 'populate', 'repopulate', 'assimilate'],
|
|
68
|
+
withIterators: true
|
|
69
|
+
});
|
|
70
|
+
resHeadersEvalCode = bridge.evalCode;
|
|
71
|
+
vm.setProp(resFn, 'headerList', headerListObj);
|
|
72
|
+
headerListObj.dispose();
|
|
73
|
+
}
|
|
74
|
+
|
|
56
75
|
let getStatusText = vm.newFunction('getStatusText', function () {
|
|
57
76
|
return marshallToVm(res.getStatusText(), vm);
|
|
58
77
|
});
|
|
@@ -109,6 +128,12 @@ const addBrunoResponseShimToContext = (vm, res) => {
|
|
|
109
128
|
|
|
110
129
|
vm.setProp(vm.global, 'res', resFn);
|
|
111
130
|
resFn.dispose();
|
|
131
|
+
|
|
132
|
+
// Evaluate iterator code after res is on global (iterators reference globalThis.res.headerList)
|
|
133
|
+
// Wrapped in a block to avoid const redeclaration conflicts with req.headerList's evalCode
|
|
134
|
+
if (resHeadersEvalCode) {
|
|
135
|
+
vm.evalCode(`{ ${resHeadersEvalCode} }`);
|
|
136
|
+
}
|
|
112
137
|
};
|
|
113
138
|
|
|
114
139
|
module.exports = addBrunoResponseShimToContext;
|
|
@@ -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;
|
|
@@ -1,3 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a QuickJS context with centralized lifecycle management:
|
|
3
|
+
* - vm.evalCode() auto-disposes result handles (for shim setup code)
|
|
4
|
+
* - vm.evalCodeRetained() returns the raw result (for user script execution)
|
|
5
|
+
* - all newObject/newFunction/newArray handles are tracked and disposed on teardown
|
|
6
|
+
*/
|
|
7
|
+
const createManagedQuickJsContext = (module) => {
|
|
8
|
+
const vm = module.newContext();
|
|
9
|
+
const disposeTracked = trackQuickJsContext(vm);
|
|
10
|
+
const evalCodeRetained = vm.evalCode.bind(vm);
|
|
11
|
+
const waitForPendingDeferreds = trackPendingDeferreds(vm);
|
|
12
|
+
|
|
13
|
+
vm.evalCode = (code, filename = 'eval.js') => {
|
|
14
|
+
const result = evalCodeRetained(code, filename);
|
|
15
|
+
if (result.error) {
|
|
16
|
+
const error = vm.dump(result.error);
|
|
17
|
+
result.error.dispose();
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
result.value.dispose();
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
vm.evalCodeRetained = evalCodeRetained;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
vm,
|
|
27
|
+
waitForPendingDeferreds,
|
|
28
|
+
dispose: () => disposeQuickJsContext(vm, disposeTracked)
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Track every deferred created by the async shims (sendRequest, axios, cookie
|
|
34
|
+
* jar, sleep, ...) so teardown can wait for them to settle. A user script that
|
|
35
|
+
* fires-and-forgets async work (e.g. an un-awaited setTimeout) resolves the
|
|
36
|
+
* wrapping closure immediately; without this, the VM is disposed before the
|
|
37
|
+
* deferred's host callback runs, and touching the freed context throws
|
|
38
|
+
* `QuickJSUseAfterFree`. Each `.settled` resolves once the deferred is
|
|
39
|
+
* resolved/rejected, so awaiting them keeps the context alive long enough.
|
|
40
|
+
*
|
|
41
|
+
* The hook is installed now (at context creation) so it captures promises as
|
|
42
|
+
* the script runs. Returns a function that drains the captured deferreds at
|
|
43
|
+
* teardown; new deferreds can be created while we wait (a chained timer), so it
|
|
44
|
+
* drains in place until none remain.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
const trackPendingDeferreds = (vm) => {
|
|
48
|
+
const pendingDeferreds = [];
|
|
49
|
+
const originalNewPromise = vm.newPromise.bind(vm);
|
|
50
|
+
vm.newPromise = (...args) => {
|
|
51
|
+
const deferred = originalNewPromise(...args);
|
|
52
|
+
pendingDeferreds.push(deferred.settled.catch(() => { }));
|
|
53
|
+
return deferred;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return async () => {
|
|
57
|
+
while (pendingDeferreds.length) {
|
|
58
|
+
const batch = pendingDeferreds.splice(0);
|
|
59
|
+
await Promise.all(batch);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Tracks handles created via newObject/newFunction/newArray so they can all be
|
|
66
|
+
* disposed before the context. quickjs-emscripten requires every heap handle to
|
|
67
|
+
* be disposed individually; shims attach then drop their ref via .dispose().
|
|
68
|
+
*/
|
|
69
|
+
const trackQuickJsContext = (vm) => {
|
|
70
|
+
const handles = [];
|
|
71
|
+
|
|
72
|
+
const track = (handle) => {
|
|
73
|
+
handles.push(handle);
|
|
74
|
+
return handle;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Replace an allocator with a wrapper that records every handle it returns,
|
|
78
|
+
// so teardown can dispose them all. Behaviour is otherwise identical.
|
|
79
|
+
const trackAllocations = (method) => {
|
|
80
|
+
const original = vm[method]?.bind(vm);
|
|
81
|
+
if (!original) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
vm[method] = (...args) => track(original(...args));
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
['newObject', 'newFunction', 'newArray'].forEach(trackAllocations);
|
|
89
|
+
|
|
90
|
+
// Dispose newest-first: later handles may reference earlier ones.
|
|
91
|
+
return () => {
|
|
92
|
+
for (const handle of handles.reverse()) {
|
|
93
|
+
if (handle?.alive) {
|
|
94
|
+
handle.dispose();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Clears shim globals, drains pending QuickJS jobs, and disposes the context.
|
|
102
|
+
* Pass disposeTracked from trackQuickJsContext() to free shim handles first.
|
|
103
|
+
*/
|
|
104
|
+
const disposeQuickJsContext = (vm, disposeTracked) => {
|
|
105
|
+
if (!vm?.alive) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (typeof disposeTracked === 'function') {
|
|
110
|
+
disposeTracked();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Drain the runtime's pending job queue (resolved/rejected promise callbacks)
|
|
114
|
+
// before disposing. Executing a job can schedule more jobs (chained `.then()`s),
|
|
115
|
+
// so we keep going until `hasPendingJob()` reports the queue is empty or a job
|
|
116
|
+
// throws.
|
|
117
|
+
while (vm.runtime?.hasPendingJob?.()) {
|
|
118
|
+
const result = vm.runtime.executePendingJobs();
|
|
119
|
+
// On error, dispose the error handle and stop draining.
|
|
120
|
+
if (result.error) {
|
|
121
|
+
result.error.dispose();
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
vm.dispose();
|
|
126
|
+
};
|
|
127
|
+
|
|
1
128
|
const marshallToVm = (value, vm) => {
|
|
2
129
|
if (value === undefined) {
|
|
3
130
|
return vm.undefined;
|
|
@@ -79,5 +206,8 @@ async function invokeFunction(vm, quickFn, args = []) {
|
|
|
79
206
|
|
|
80
207
|
module.exports = {
|
|
81
208
|
marshallToVm,
|
|
82
|
-
invokeFunction
|
|
209
|
+
invokeFunction,
|
|
210
|
+
createManagedQuickJsContext,
|
|
211
|
+
disposeQuickJsContext,
|
|
212
|
+
trackQuickJsContext
|
|
83
213
|
};
|
|
@@ -81,6 +81,7 @@ const createPropertyListBridge = (vm, nativeList, targetObj, options) => {
|
|
|
81
81
|
globalPath,
|
|
82
82
|
syncReadMethods = [],
|
|
83
83
|
syncReadObjectMethods = [],
|
|
84
|
+
syncWriteMethods = [],
|
|
84
85
|
asyncWriteMethods = [],
|
|
85
86
|
withIterators = false
|
|
86
87
|
} = options;
|
|
@@ -103,6 +104,16 @@ const createPropertyListBridge = (vm, nativeList, targetObj, options) => {
|
|
|
103
104
|
fn.consume((handle) => vm.setProp(targetObj, methodName, handle));
|
|
104
105
|
}
|
|
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
|
+
|
|
106
117
|
// Async write methods — two-phase setup:
|
|
107
118
|
// Phase 1 (native): Register `_prefixed` bridge functions (e.g. `_add`, `_remove`) via
|
|
108
119
|
// createAsyncBridge. These are QuickJS promise-based wrappers that call the native method's
|
|
@@ -149,11 +160,25 @@ const createPropertyListBridge = (vm, nativeList, targetObj, options) => {
|
|
|
149
160
|
// operation inside the VM where the callback lives. Requires `all` in `syncReadObjectMethods`.
|
|
150
161
|
if (withIterators) {
|
|
151
162
|
evalCode += `const _allNative = ${globalPath}.all;
|
|
152
|
-
${globalPath}.each = (fn) => { _allNative().forEach(
|
|
153
|
-
${globalPath}.filter = (fn) => _allNative().filter(
|
|
154
|
-
${globalPath}.find = (fn) => _allNative().find(
|
|
155
|
-
${globalPath}.map = (fn) => _allNative().map(
|
|
156
|
-
${globalPath}.reduce = (fn, ...rest) => rest.length ? _allNative().reduce(
|
|
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`;
|
|
157
182
|
}
|
|
158
183
|
|
|
159
184
|
return { evalCode };
|