@usebruno/js 0.46.1 → 0.47.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,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,8 +1,35 @@
1
1
  const { marshallToVm } = require('../utils');
2
2
 
3
+ // Marshal a QuickJS query argument to a host-compatible value.
4
+ // Function handles are wrapped as native callbacks; other values are dumped as-is.
5
+ // Safe because @usebruno/query's get() invokes filters synchronously,
6
+ // so the borrowed arg handle is still valid.
7
+ const toHostQueryArg = (vm, arg) => {
8
+ if (vm.typeof(arg) === 'function') {
9
+ return (item) => {
10
+ const itemHandle = marshallToVm(item, vm);
11
+ const result = vm.callFunction(arg, vm.undefined, itemHandle);
12
+ itemHandle.dispose();
13
+
14
+ if (result.error) {
15
+ const error = vm.dump(result.error);
16
+ result.error.dispose();
17
+ throw error;
18
+ }
19
+
20
+ const value = vm.dump(result.value);
21
+ result.value.dispose();
22
+ return value;
23
+ };
24
+ }
25
+
26
+ return vm.dump(arg);
27
+ };
28
+
3
29
  const addBrunoResponseShimToContext = (vm, res) => {
4
- let resFn = vm.newFunction('res', function (exprStr) {
5
- return marshallToVm(res(vm.dump(exprStr)), vm);
30
+ let resFn = vm.newFunction('res', function (exprStr, ...queryArgs) {
31
+ const nativeArgs = queryArgs.map((arg) => toHostQueryArg(vm, arg));
32
+ return marshallToVm(res(vm.dump(exprStr), ...nativeArgs), vm);
6
33
  });
7
34
 
8
35
  const status = marshallToVm(res?.status, vm);
@@ -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
+ };
@@ -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 && !Array.isArray(obj) &&
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
  },