@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.
@@ -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 { terser } = require('rollup-plugin-terser');
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 QuickJSSyncContext;
18
+ let QuickJSModule;
18
19
  const loader = memoizePromiseFactory(() => newQuickJSWASMModule());
19
- const getContext = (opts) => loader().then((mod) => (QuickJSSyncContext = mod.newContext(opts)));
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 newQuickJSWASMModule();
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
- ${moduleLoaderCode()}
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
- bru.visualize(vm.dump(type), vm.dump(data));
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
- return marshallToVm(res(vm.dump(exprStr)), vm);
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
+ };