@usebruno/js 0.47.0 → 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,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
  },
@@ -15,10 +15,9 @@ const { marshallToVm } = require('./utils');
15
15
  const addCryptoUtilsShimToContext = require('./shims/lib/crypto-utils');
16
16
  const { wrapScriptInClosure, SANDBOX } = require('../../utils/sandbox');
17
17
 
18
- let QuickJSSyncContext;
18
+ let QuickJSModule;
19
19
  const loader = memoizePromiseFactory(() => newQuickJSWASMModule());
20
- const getContext = (opts) => loader().then((mod) => (QuickJSSyncContext = mod.newContext(opts)));
21
- getContext();
20
+ loader().then((mod) => (QuickJSModule = mod));
22
21
 
23
22
  const toNumber = (value) => {
24
23
  const num = Number(value);
@@ -58,9 +57,8 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc
58
57
  externalScript = removeQuotes(externalScript);
59
58
  }
60
59
 
61
- const vm = QuickJSSyncContext;
62
-
63
60
  try {
61
+ const vm = QuickJSModule.newContext();
64
62
  const { bru, req, res, ...variables } = externalContext;
65
63
 
66
64
  bru && addBruShimToContext(vm, bru);
@@ -98,7 +96,7 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
98
96
  externalScript = externalScript?.trim();
99
97
 
100
98
  try {
101
- const module = await newQuickJSWASMModule();
99
+ const module = await loader();
102
100
  const vm = module.newContext();
103
101
 
104
102
  // add crypto utilities required by the crypto-js library in bundledCode
@@ -144,5 +142,6 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
144
142
 
145
143
  module.exports = {
146
144
  executeQuickJsVm,
147
- executeQuickJsVmAsync
145
+ executeQuickJsVmAsync,
146
+ loader
148
147
  };
@@ -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;
@@ -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(fn); };
153
- ${globalPath}.filter = (fn) => _allNative().filter(fn);
154
- ${globalPath}.find = (fn) => _allNative().find(fn);
155
- ${globalPath}.map = (fn) => _allNative().map(fn);
156
- ${globalPath}.reduce = (fn, ...rest) => rest.length ? _allNative().reduce(fn, rest[0]) : _allNative().reduce(fn);\n`;
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 };