@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
|
@@ -1,8 +1,9 @@
|
|
|
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
|
-
const
|
|
6
|
+
const terser = require('@rollup/plugin-terser').default;
|
|
6
7
|
|
|
7
8
|
const bundleLibraries = async () => {
|
|
8
9
|
const codeScript = `
|
|
@@ -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
|
},
|
|
@@ -5,6 +5,7 @@ const addBrunoResponseShimToContext = require('./shims/bruno-response');
|
|
|
5
5
|
const addTestShimToContext = require('./shims/test');
|
|
6
6
|
const addLibraryShimsToContext = require('./shims/lib');
|
|
7
7
|
const addLocalModuleLoaderShimToContext = require('./shims/local-module');
|
|
8
|
+
const { getRequireCode } = require('./shims/require');
|
|
8
9
|
const { newQuickJSWASMModule, memoizePromiseFactory } = require('quickjs-emscripten');
|
|
9
10
|
|
|
10
11
|
// execute `npm run sandbox:bundle-libraries` if the below file doesn't exist
|
|
@@ -14,10 +15,9 @@ const { marshallToVm } = require('./utils');
|
|
|
14
15
|
const addCryptoUtilsShimToContext = require('./shims/lib/crypto-utils');
|
|
15
16
|
const { wrapScriptInClosure, SANDBOX } = require('../../utils/sandbox');
|
|
16
17
|
|
|
17
|
-
let
|
|
18
|
+
let QuickJSModule;
|
|
18
19
|
const loader = memoizePromiseFactory(() => newQuickJSWASMModule());
|
|
19
|
-
|
|
20
|
-
getContext();
|
|
20
|
+
loader().then((mod) => (QuickJSModule = mod));
|
|
21
21
|
|
|
22
22
|
const toNumber = (value) => {
|
|
23
23
|
const num = Number(value);
|
|
@@ -57,9 +57,8 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc
|
|
|
57
57
|
externalScript = removeQuotes(externalScript);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
const vm = QuickJSSyncContext;
|
|
61
|
-
|
|
62
60
|
try {
|
|
61
|
+
const vm = QuickJSModule.newContext();
|
|
63
62
|
const { bru, req, res, ...variables } = externalContext;
|
|
64
63
|
|
|
65
64
|
bru && addBruShimToContext(vm, bru);
|
|
@@ -97,51 +96,18 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
|
|
|
97
96
|
externalScript = externalScript?.trim();
|
|
98
97
|
|
|
99
98
|
try {
|
|
100
|
-
const module = await
|
|
99
|
+
const module = await loader();
|
|
101
100
|
const vm = module.newContext();
|
|
102
101
|
|
|
103
102
|
// add crypto utilities required by the crypto-js library in bundledCode
|
|
104
103
|
await addCryptoUtilsShimToContext(vm);
|
|
105
104
|
|
|
106
105
|
const bundledCode = getBundledCode?.toString() || '';
|
|
107
|
-
const moduleLoaderCode = function () {
|
|
108
|
-
return `
|
|
109
|
-
globalThis.require = (mod) => {
|
|
110
|
-
let lib = globalThis.requireObject[mod];
|
|
111
|
-
let isModuleAPath = (module) => (module?.startsWith('.') || module?.startsWith?.(bru.cwd()))
|
|
112
|
-
if (lib) {
|
|
113
|
-
return lib;
|
|
114
|
-
}
|
|
115
|
-
else if (isModuleAPath(mod)) {
|
|
116
|
-
// fetch local module
|
|
117
|
-
let localModuleCode = globalThis.__brunoLoadLocalModule(mod);
|
|
118
|
-
|
|
119
|
-
// compile local module as iife
|
|
120
|
-
(function (){
|
|
121
|
-
const initModuleExportsCode = "const module = { exports: {} };"
|
|
122
|
-
const copyModuleExportsCode = "\\n;globalThis.requireObject[mod] = module.exports;";
|
|
123
|
-
const patchedRequire = ${`
|
|
124
|
-
"\\n;" +
|
|
125
|
-
"let require = (subModule) => isModuleAPath(subModule) ? globalThis.require(path.resolve(bru.cwd(), mod, '..', subModule)) : globalThis.require(subModule)" +
|
|
126
|
-
"\\n;"
|
|
127
|
-
`}
|
|
128
|
-
eval(initModuleExportsCode + patchedRequire + localModuleCode + copyModuleExportsCode);
|
|
129
|
-
})();
|
|
130
|
-
|
|
131
|
-
// resolve module
|
|
132
|
-
return globalThis.requireObject[mod];
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
throw new Error("Cannot find module " + mod);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
`;
|
|
139
|
-
};
|
|
140
106
|
|
|
141
107
|
vm.evalCode(
|
|
142
108
|
`
|
|
143
109
|
(${bundledCode})()
|
|
144
|
-
${
|
|
110
|
+
${getRequireCode()}
|
|
145
111
|
`
|
|
146
112
|
);
|
|
147
113
|
|
|
@@ -176,5 +142,6 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
|
|
|
176
142
|
|
|
177
143
|
module.exports = {
|
|
178
144
|
executeQuickJsVm,
|
|
179
|
-
executeQuickJsVmAsync
|
|
145
|
+
executeQuickJsVmAsync,
|
|
146
|
+
loader
|
|
180
147
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { cleanJson, cleanCircularJson } = require('../../../utils');
|
|
2
2
|
const { marshallToVm } = require('../utils');
|
|
3
|
+
const { createPropertyListBridge } = require('../utils/property-list-bridge');
|
|
3
4
|
|
|
4
5
|
const addBruShimToContext = (vm, bru) => {
|
|
5
6
|
const bruObject = vm.newObject();
|
|
@@ -229,11 +230,29 @@ const addBruShimToContext = (vm, bru) => {
|
|
|
229
230
|
bruRunnerIterationDataObject.dispose();
|
|
230
231
|
|
|
231
232
|
let visualize = vm.newFunction('visualize', function (type, data) {
|
|
232
|
-
|
|
233
|
+
try {
|
|
234
|
+
// Guard against missing handles: vm.dump() accesses .owner on the handle
|
|
235
|
+
// to verify its VM context. When called with no args, the handles are
|
|
236
|
+
// undefined, and vm.dump(undefined) throws a generic
|
|
237
|
+
// "Cannot read properties of undefined (reading 'owner')" instead of
|
|
238
|
+
// letting bru.visualize() throw a meaningful error message.
|
|
239
|
+
bru.visualize(
|
|
240
|
+
type ? vm.dump(type) : undefined,
|
|
241
|
+
data ? vm.dump(data) : undefined
|
|
242
|
+
);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
throw vm.newError(err.message || String(err));
|
|
245
|
+
}
|
|
233
246
|
});
|
|
234
247
|
vm.setProp(bruObject, 'visualize', visualize);
|
|
235
248
|
visualize.dispose();
|
|
236
249
|
|
|
250
|
+
let clearVisualizations = vm.newFunction('clearVisualizations', function () {
|
|
251
|
+
bru.clearVisualizations();
|
|
252
|
+
});
|
|
253
|
+
vm.setProp(bruObject, 'clearVisualizations', clearVisualizations);
|
|
254
|
+
clearVisualizations.dispose();
|
|
255
|
+
|
|
237
256
|
let getSecretVar = vm.newFunction('getSecretVar', function (key) {
|
|
238
257
|
return marshallToVm(bru.getSecretVar(vm.dump(key)), vm);
|
|
239
258
|
});
|
|
@@ -394,6 +413,13 @@ const addBruShimToContext = (vm, bru) => {
|
|
|
394
413
|
sleep.consume((handle) => vm.setProp(bruObject, 'sleep', handle));
|
|
395
414
|
|
|
396
415
|
let bruCookiesObject = vm.newObject();
|
|
416
|
+
const { evalCode: cookiesEvalCode } = createPropertyListBridge(vm, bru.cookies, bruCookiesObject, {
|
|
417
|
+
globalPath: 'globalThis.bru.cookies',
|
|
418
|
+
syncReadMethods: ['get', 'has', 'count', 'indexOf', 'toObject', 'toString'],
|
|
419
|
+
syncReadObjectMethods: ['one', 'all', 'idx', 'toJSON'],
|
|
420
|
+
asyncWriteMethods: ['add', 'upsert', 'remove', 'clear', 'delete'],
|
|
421
|
+
withIterators: true
|
|
422
|
+
});
|
|
397
423
|
|
|
398
424
|
const _jarFn = vm.newFunction('_jar', () => {
|
|
399
425
|
const nativeJar = bru.cookies.jar();
|
|
@@ -571,6 +597,10 @@ const addBruShimToContext = (vm, bru) => {
|
|
|
571
597
|
}
|
|
572
598
|
};
|
|
573
599
|
|
|
600
|
+
{
|
|
601
|
+
${cookiesEvalCode}
|
|
602
|
+
}
|
|
603
|
+
|
|
574
604
|
globalThis.bru.cookies.jar = () => {
|
|
575
605
|
const _jar = globalThis.bru.cookies._jar();
|
|
576
606
|
|
|
@@ -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,31 +1,77 @@
|
|
|
1
1
|
const { marshallToVm } = require('../utils');
|
|
2
|
+
const { createPropertyListBridge } = require('../utils/property-list-bridge');
|
|
3
|
+
|
|
4
|
+
// Marshal a QuickJS query argument to a host-compatible value.
|
|
5
|
+
// Function handles are wrapped as native callbacks; other values are dumped as-is.
|
|
6
|
+
// Safe because @usebruno/query's get() invokes filters synchronously,
|
|
7
|
+
// so the borrowed arg handle is still valid.
|
|
8
|
+
const toHostQueryArg = (vm, arg) => {
|
|
9
|
+
if (vm.typeof(arg) === 'function') {
|
|
10
|
+
return (item) => {
|
|
11
|
+
const itemHandle = marshallToVm(item, vm);
|
|
12
|
+
const result = vm.callFunction(arg, vm.undefined, itemHandle);
|
|
13
|
+
itemHandle.dispose();
|
|
14
|
+
|
|
15
|
+
if (result.error) {
|
|
16
|
+
const error = vm.dump(result.error);
|
|
17
|
+
result.error.dispose();
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const value = vm.dump(result.value);
|
|
22
|
+
result.value.dispose();
|
|
23
|
+
return value;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return vm.dump(arg);
|
|
28
|
+
};
|
|
2
29
|
|
|
3
30
|
const addBrunoResponseShimToContext = (vm, res) => {
|
|
4
|
-
let resFn = vm.newFunction('res', function (exprStr) {
|
|
5
|
-
|
|
31
|
+
let resFn = vm.newFunction('res', function (exprStr, ...queryArgs) {
|
|
32
|
+
const nativeArgs = queryArgs.map((arg) => toHostQueryArg(vm, arg));
|
|
33
|
+
return marshallToVm(res(vm.dump(exprStr), ...nativeArgs), vm);
|
|
6
34
|
});
|
|
7
35
|
|
|
8
36
|
const status = marshallToVm(res?.status, vm);
|
|
9
37
|
const statusText = marshallToVm(res?.statusText, vm);
|
|
10
|
-
const headers = marshallToVm(res?.headers, vm);
|
|
11
38
|
const body = marshallToVm(res?.body, vm);
|
|
12
39
|
const responseTime = marshallToVm(res?.responseTime, vm);
|
|
13
40
|
const url = marshallToVm(res?.url, vm);
|
|
14
41
|
|
|
15
42
|
vm.setProp(resFn, 'status', status);
|
|
16
43
|
vm.setProp(resFn, 'statusText', statusText);
|
|
17
|
-
vm.setProp(resFn, 'headers', headers);
|
|
18
44
|
vm.setProp(resFn, 'body', body);
|
|
19
45
|
vm.setProp(resFn, 'responseTime', responseTime);
|
|
20
46
|
vm.setProp(resFn, 'url', url);
|
|
21
47
|
|
|
22
48
|
status.dispose();
|
|
23
|
-
headers.dispose();
|
|
24
49
|
body.dispose();
|
|
25
50
|
responseTime.dispose();
|
|
26
51
|
url.dispose();
|
|
27
52
|
statusText.dispose();
|
|
28
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
|
+
|
|
29
75
|
let getStatusText = vm.newFunction('getStatusText', function () {
|
|
30
76
|
return marshallToVm(res.getStatusText(), vm);
|
|
31
77
|
});
|
|
@@ -82,6 +128,12 @@ const addBrunoResponseShimToContext = (vm, res) => {
|
|
|
82
128
|
|
|
83
129
|
vm.setProp(vm.global, 'res', resFn);
|
|
84
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
|
+
}
|
|
85
137
|
};
|
|
86
138
|
|
|
87
139
|
module.exports = addBrunoResponseShimToContext;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const { describe, it, expect, beforeAll, beforeEach, afterEach, afterAll } = require('@jest/globals');
|
|
2
|
+
const { newQuickJSWASMModule } = require('quickjs-emscripten');
|
|
3
|
+
const addBrunoResponseShimToContext = require('./bruno-response');
|
|
4
|
+
|
|
5
|
+
describe('bruno response shim', () => {
|
|
6
|
+
let vm, module;
|
|
7
|
+
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
module = await newQuickJSWASMModule();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vm = module.newContext();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
if (vm) {
|
|
18
|
+
try {
|
|
19
|
+
vm.dispose();
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error('Error disposing vm', err);
|
|
22
|
+
}
|
|
23
|
+
vm = null;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterAll(() => {
|
|
28
|
+
if (module) {
|
|
29
|
+
try {
|
|
30
|
+
module.dispose();
|
|
31
|
+
} catch (err) {
|
|
32
|
+
console.error('Error disposing module', err);
|
|
33
|
+
}
|
|
34
|
+
module = null;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('forwards response query filter callbacks in safe mode', () => {
|
|
39
|
+
const resources = [
|
|
40
|
+
{ id: 'test', value: 1 },
|
|
41
|
+
{ id: 'other', value: 2 }
|
|
42
|
+
];
|
|
43
|
+
const response = Object.assign(
|
|
44
|
+
(expr, filter) => {
|
|
45
|
+
expect(expr).toBe('resources[?].id');
|
|
46
|
+
return resources.filter(filter).map((item) => item.id);
|
|
47
|
+
},
|
|
48
|
+
{ status: 200, statusText: 'OK', headers: {}, body: { resources } }
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
addBrunoResponseShimToContext(vm, response);
|
|
52
|
+
|
|
53
|
+
const result = vm.evalCode(`
|
|
54
|
+
res('resources[?].id', r => r.id === 'test')
|
|
55
|
+
`);
|
|
56
|
+
|
|
57
|
+
const valueHandle = vm.unwrapResult(result);
|
|
58
|
+
const filtered = vm.dump(valueHandle);
|
|
59
|
+
valueHandle.dispose();
|
|
60
|
+
|
|
61
|
+
expect(filtered).toEqual(['test']);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('still supports object predicates in safe mode', () => {
|
|
65
|
+
const resources = [
|
|
66
|
+
{ id: 'test', value: 1 },
|
|
67
|
+
{ id: 'other', value: 2 }
|
|
68
|
+
];
|
|
69
|
+
const response = Object.assign(
|
|
70
|
+
(expr, predicate) => {
|
|
71
|
+
expect(expr).toBe('resources[?].id');
|
|
72
|
+
return resources.filter((item) =>
|
|
73
|
+
Object.entries(predicate).every(([key, value]) => item[key] === value)
|
|
74
|
+
).map((item) => item.id);
|
|
75
|
+
},
|
|
76
|
+
{ status: 200, statusText: 'OK', headers: {}, body: { resources } }
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
addBrunoResponseShimToContext(vm, response);
|
|
80
|
+
|
|
81
|
+
const result = vm.evalCode(`
|
|
82
|
+
res('resources[?].id', { id: 'other' })
|
|
83
|
+
`);
|
|
84
|
+
|
|
85
|
+
const valueHandle = vm.unwrapResult(result);
|
|
86
|
+
const filtered = vm.dump(valueHandle);
|
|
87
|
+
valueHandle.dispose();
|
|
88
|
+
|
|
89
|
+
expect(filtered).toEqual(['other']);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
const { describe, it, expect, beforeAll, beforeEach, afterEach, afterAll } = require('@jest/globals');
|
|
2
|
+
const { newQuickJSWASMModule } = require('quickjs-emscripten');
|
|
3
|
+
const { validate: uuidValidate, version: uuidVersion } = require('uuid');
|
|
4
|
+
const addUuidShimToContext = require('./uuid');
|
|
5
|
+
const { addRequireShimToContext } = require('../require');
|
|
6
|
+
const { createEvalHelper } = require('../../utils/test-helpers');
|
|
7
|
+
|
|
8
|
+
describe('uuid shim tests', () => {
|
|
9
|
+
let vm, module, evalAndDump;
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
module = await newQuickJSWASMModule();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
vm = module.newContext();
|
|
17
|
+
evalAndDump = createEvalHelper(vm);
|
|
18
|
+
await addUuidShimToContext(vm);
|
|
19
|
+
addRequireShimToContext(vm, { enableLocalModules: false });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
if (vm) {
|
|
24
|
+
try {
|
|
25
|
+
vm.dispose();
|
|
26
|
+
} catch (err) {
|
|
27
|
+
// Ignore disposal errors
|
|
28
|
+
}
|
|
29
|
+
vm = null;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterAll(() => {
|
|
34
|
+
if (module) {
|
|
35
|
+
try {
|
|
36
|
+
module.dispose();
|
|
37
|
+
} catch (err) {
|
|
38
|
+
// Ignore disposal errors
|
|
39
|
+
}
|
|
40
|
+
module = null;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validates that a string is a valid UUID of the expected version
|
|
46
|
+
*/
|
|
47
|
+
function expectUuidVersion(uuid, expectedVersion) {
|
|
48
|
+
expect(uuidValidate(uuid)).toBe(true);
|
|
49
|
+
expect(uuidVersion(uuid)).toBe(expectedVersion);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('uuid version functions', () => {
|
|
53
|
+
const versionTests = [
|
|
54
|
+
{ fn: 'v1', version: 1 },
|
|
55
|
+
{ fn: 'v4', version: 4 },
|
|
56
|
+
{ fn: 'v6', version: 6, note: 'RFC 9562' },
|
|
57
|
+
{ fn: 'v7', version: 7, note: 'RFC 9562' }
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
it.each(versionTests)('$fn should generate valid uuid and be accessible via require', ({ fn, version }) => {
|
|
61
|
+
// Test direct access
|
|
62
|
+
const directUuid = evalAndDump(`globalThis.uuid.${fn}()`);
|
|
63
|
+
expectUuidVersion(directUuid, version);
|
|
64
|
+
|
|
65
|
+
// Test require with destructuring
|
|
66
|
+
const requireUuid = evalAndDump(`const { ${fn} } = require('uuid'); ${fn}()`);
|
|
67
|
+
expectUuidVersion(requireUuid, version);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('v7 should be accessible with alias pattern (v7: uuidv7) - issue #7333', () => {
|
|
71
|
+
const uuid = evalAndDump(`
|
|
72
|
+
const { v7: uuidv7 } = require('uuid');
|
|
73
|
+
uuidv7();
|
|
74
|
+
`);
|
|
75
|
+
expectUuidVersion(uuid, 7);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('v7 should generate time-ordered uuids', () => {
|
|
79
|
+
const [uuid1, uuid2] = evalAndDump(`
|
|
80
|
+
const { v7 } = require('uuid');
|
|
81
|
+
[v7(), v7()];
|
|
82
|
+
`);
|
|
83
|
+
// v7 UUIDs are lexicographically sortable by design (timestamp in most significant bits)
|
|
84
|
+
expect(uuid1 <= uuid2).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('name-based uuid functions (v3, v5)', () => {
|
|
89
|
+
const DNS_NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
|
|
90
|
+
|
|
91
|
+
it.each([
|
|
92
|
+
{ fn: 'v3', version: 3 },
|
|
93
|
+
{ fn: 'v5', version: 5 }
|
|
94
|
+
])('$fn should generate valid uuid with namespace', ({ fn, version }) => {
|
|
95
|
+
const uuid = evalAndDump(`
|
|
96
|
+
const { ${fn} } = require('uuid');
|
|
97
|
+
${fn}('example.com', '${DNS_NAMESPACE}');
|
|
98
|
+
`);
|
|
99
|
+
expectUuidVersion(uuid, version);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('conversion functions', () => {
|
|
104
|
+
it('should convert between v1 and v6 using v1ToV6 and v6ToV1', () => {
|
|
105
|
+
const [v1Uuid, v6FromV1, v6Uuid, v1FromV6] = evalAndDump(`
|
|
106
|
+
const { v1, v6, v1ToV6, v6ToV1 } = require('uuid');
|
|
107
|
+
const v1Uuid = v1();
|
|
108
|
+
const v6Uuid = v6();
|
|
109
|
+
[v1Uuid, v1ToV6(v1Uuid), v6Uuid, v6ToV1(v6Uuid)];
|
|
110
|
+
`);
|
|
111
|
+
|
|
112
|
+
expectUuidVersion(v1Uuid, 1);
|
|
113
|
+
expectUuidVersion(v6FromV1, 6);
|
|
114
|
+
expectUuidVersion(v6Uuid, 6);
|
|
115
|
+
expectUuidVersion(v1FromV6, 1);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('utility functions', () => {
|
|
120
|
+
it('should validate uuids correctly', () => {
|
|
121
|
+
const [isValid, isInvalid] = evalAndDump(`
|
|
122
|
+
const { v4, validate } = require('uuid');
|
|
123
|
+
[validate(v4()), validate('not-a-uuid')];
|
|
124
|
+
`);
|
|
125
|
+
expect(isValid).toBe(true);
|
|
126
|
+
expect(isInvalid).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should detect uuid version correctly', () => {
|
|
130
|
+
const [v4Version, v7Version] = evalAndDump(`
|
|
131
|
+
const { v4, v7, version } = require('uuid');
|
|
132
|
+
[version(v4()), version(v7())];
|
|
133
|
+
`);
|
|
134
|
+
expect(v4Version).toBe(4);
|
|
135
|
+
expect(v7Version).toBe(7);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should parse and stringify uuid', () => {
|
|
139
|
+
const [original, stringified, isObject, length] = evalAndDump(`
|
|
140
|
+
const { v4, parse, stringify } = require('uuid');
|
|
141
|
+
const original = v4();
|
|
142
|
+
const parsed = parse(original);
|
|
143
|
+
[original, stringify(parsed), typeof parsed === 'object', Object.keys(parsed).length];
|
|
144
|
+
`);
|
|
145
|
+
expect(original).toBe(stringified);
|
|
146
|
+
expect(isObject).toBe(true);
|
|
147
|
+
expect(length).toBe(16);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('issue #7333 regression test', () => {
|
|
152
|
+
it('should work with the exact pattern from the bug report', () => {
|
|
153
|
+
const [typeOfFn, id1, id2] = evalAndDump(`
|
|
154
|
+
const { v7: uuidv7 } = require('uuid');
|
|
155
|
+
const id1 = uuidv7();
|
|
156
|
+
const id2 = uuidv7();
|
|
157
|
+
[typeof uuidv7, id1, id2];
|
|
158
|
+
`);
|
|
159
|
+
|
|
160
|
+
expect(typeOfFn).toBe('function');
|
|
161
|
+
expectUuidVersion(id1, 7);
|
|
162
|
+
expectUuidVersion(id2, 7);
|
|
163
|
+
expect(id1).not.toBe(id2);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns JavaScript code that sets up the require() function in the QuickJS VM.
|
|
3
|
+
* This module loader looks up modules from globalThis.requireObject and optionally
|
|
4
|
+
* supports loading local modules if the necessary context (bru.cwd, __brunoLoadLocalModule) is available.
|
|
5
|
+
*
|
|
6
|
+
* @param {Object} options
|
|
7
|
+
* @param {boolean} options.enableLocalModules - Whether to enable local module loading (requires bru context)
|
|
8
|
+
* @returns {string} JavaScript code to eval in the VM
|
|
9
|
+
*/
|
|
10
|
+
function getRequireCode() {
|
|
11
|
+
return `
|
|
12
|
+
globalThis.require = (mod) => {
|
|
13
|
+
let lib = globalThis.requireObject[mod];
|
|
14
|
+
let isModuleAPath = (module) => (module?.startsWith('.') || (typeof bru !== 'undefined' && module?.startsWith(bru.cwd())))
|
|
15
|
+
if (lib) {
|
|
16
|
+
return lib;
|
|
17
|
+
}
|
|
18
|
+
else if (isModuleAPath(mod)) {
|
|
19
|
+
// fetch local module
|
|
20
|
+
let localModuleCode = globalThis.__brunoLoadLocalModule(mod);
|
|
21
|
+
|
|
22
|
+
// compile local module as iife
|
|
23
|
+
(function (){
|
|
24
|
+
const initModuleExportsCode = "const module = { exports: {} };"
|
|
25
|
+
const copyModuleExportsCode = "\\n;globalThis.requireObject[mod] = module.exports;";
|
|
26
|
+
const patchedRequire = ${`
|
|
27
|
+
"\\n;" +
|
|
28
|
+
"let require = (subModule) => isModuleAPath(subModule) ? globalThis.require(path.resolve(bru.cwd(), mod, '..', subModule)) : globalThis.require(subModule)" +
|
|
29
|
+
"\\n;"
|
|
30
|
+
`}
|
|
31
|
+
eval(initModuleExportsCode + patchedRequire + localModuleCode + copyModuleExportsCode);
|
|
32
|
+
})();
|
|
33
|
+
|
|
34
|
+
// resolve module
|
|
35
|
+
return globalThis.requireObject[mod];
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
throw new Error("Cannot find module " + mod);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Adds the require() function to a QuickJS VM context
|
|
46
|
+
* @param {Object} vm - QuickJS VM context
|
|
47
|
+
* @param {Object} options - Options passed to getRequireCode
|
|
48
|
+
*/
|
|
49
|
+
function addRequireShimToContext(vm) {
|
|
50
|
+
vm.evalCode(getRequireCode());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
getRequireCode,
|
|
55
|
+
addRequireShimToContext
|
|
56
|
+
};
|