@usebruno/js 0.49.0 → 0.51.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usebruno/js",
3
- "version": "0.49.0",
3
+ "version": "0.51.0",
4
4
  "license": "MIT",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -14,7 +14,7 @@
14
14
  "prepack": "npm run test"
15
15
  },
16
16
  "dependencies": {
17
- "@usebruno/common": "0.23.0",
17
+ "@usebruno/common": "0.25.0",
18
18
  "@usebruno/query": "0.2.2",
19
19
  "ajv": "^8.12.0",
20
20
  "ajv-formats": "^2.1.1",
@@ -28,7 +28,7 @@
28
28
  "handlebars": "^4.7.9",
29
29
  "json-query": "^2.2.2",
30
30
  "jsonwebtoken": "^9.0.3",
31
- "lodash": "^4.17.21",
31
+ "lodash": "4.18.1",
32
32
  "moment": "^2.29.4",
33
33
  "nanoid": "3.3.8",
34
34
  "node-fetch": "^2.7.0",
package/src/bru.js CHANGED
@@ -1,4 +1,4 @@
1
- const { cloneDeep } = require('lodash');
1
+ const { cloneDeep, isEqual } = require('lodash');
2
2
  const { uuid } = require('./utils');
3
3
  const xmlFormat = require('xml-formatter');
4
4
  const { interpolate: _interpolate } = require('@usebruno/common');
@@ -121,8 +121,11 @@ class Bru {
121
121
  createCookieJar,
122
122
  getCookiesForUrl
123
123
  });
124
- // Holds variables that are marked as persistent by scripts
125
- this.persistentEnvVariables = {};
124
+ // Dirty flags set by mutators so runtimes can skip IPC/disk writes for unchanged scopes
125
+ this._envDirty = false;
126
+ this._globalEnvDirty = false;
127
+ this._collVarsDirty = false;
128
+ this._runtimeVarsDirty = false;
126
129
  // Holds credential IDs to be reset after script execution
127
130
  this.oauth2CredentialsToReset = [];
128
131
  this.runner = {
@@ -240,7 +243,7 @@ class Bru {
240
243
  return this.interpolate(this.envVariables[key]);
241
244
  }
242
245
 
243
- setEnvVar(key, value, options = {}) {
246
+ setEnvVar(key, value) {
244
247
  if (!key) {
245
248
  throw new Error('Creating a env variable without specifying a name is not allowed.');
246
249
  }
@@ -251,24 +254,21 @@ class Bru {
251
254
  );
252
255
  }
253
256
 
254
- // When persist is true, only string values are allowed
255
- if (options?.persist && typeof value !== 'string') {
256
- throw new Error(`Persistent environment variables must be strings. Received ${typeof value} for key "${key}".`);
257
- }
258
-
259
- this.envVariables[key] = value;
260
-
261
- if (options?.persist) {
262
- this.persistentEnvVariables[key] = value;
263
- } else {
264
- if (this.persistentEnvVariables[key]) {
265
- delete this.persistentEnvVariables[key];
266
- }
257
+ // Deep-equal compare so object/array writes that mutate in place
258
+ // (e.g. `const c = bru.getEnvVar('cfg'); c.port = 4000; bru.setEnvVar('cfg', c);`)
259
+ // still flip the dirty flag strict `!==` returned false for same-reference writes.
260
+ if (!Object.hasOwn(this.envVariables, key) || !isEqual(this.envVariables[key], value)) {
261
+ this.envVariables[key] = value;
262
+ this._envDirty = true;
267
263
  }
268
264
  }
269
265
 
270
266
  deleteEnvVar(key) {
271
- delete this.envVariables[key];
267
+ if (key === '__name__') return;
268
+ if (Object.hasOwn(this.envVariables, key)) {
269
+ delete this.envVariables[key];
270
+ this._envDirty = true;
271
+ }
272
272
  }
273
273
 
274
274
  getAllEnvVars() {
@@ -278,15 +278,15 @@ class Bru {
278
278
  }
279
279
 
280
280
  deleteAllEnvVars() {
281
- const envName = this.envVariables.__name__;
282
- for (let key in this.envVariables) {
283
- if (this.envVariables.hasOwnProperty(key)) {
284
- delete this.envVariables[key];
285
- }
286
- }
287
- if (envName !== undefined) {
288
- this.envVariables.__name__ = envName;
281
+ // Iterate via Object.keys (own enumerable) so a user-set `hasOwnProperty` var
282
+ // can't shadow Object.prototype.hasOwnProperty and crash the loop.
283
+ let removed = false;
284
+ for (const key of Object.keys(this.envVariables)) {
285
+ if (key === '__name__') continue;
286
+ delete this.envVariables[key];
287
+ removed = true;
289
288
  }
289
+ if (removed) this._envDirty = true;
290
290
  }
291
291
 
292
292
  hasGlobalEnvVar(key) {
@@ -302,28 +302,31 @@ class Bru {
302
302
  throw new Error('Creating a env variable without specifying a name is not allowed.');
303
303
  }
304
304
 
305
- this.globalEnvironmentVariables[key] = value;
305
+ if (!Object.hasOwn(this.globalEnvironmentVariables, key) || !isEqual(this.globalEnvironmentVariables[key], value)) {
306
+ this.globalEnvironmentVariables[key] = value;
307
+ this._globalEnvDirty = true;
308
+ }
306
309
  }
307
310
 
308
- // TODO: deleteGlobalEnvVar works in the request lifecycle but does not update the UI.
309
- // Re-enable once the UI sync issue is resolved.
310
- // deleteGlobalEnvVar(key) {
311
- // delete this.globalEnvironmentVariables[key];
312
- // }
311
+ deleteGlobalEnvVar(key) {
312
+ if (Object.hasOwn(this.globalEnvironmentVariables, key)) {
313
+ delete this.globalEnvironmentVariables[key];
314
+ this._globalEnvDirty = true;
315
+ }
316
+ }
313
317
 
314
318
  getAllGlobalEnvVars() {
315
319
  return Object.assign({}, this.globalEnvironmentVariables);
316
320
  }
317
321
 
318
- // TODO: deleteAllGlobalEnvVars works in the request lifecycle but does not update the UI.
319
- // Re-enable once the UI sync issue is resolved.
320
- // deleteAllGlobalEnvVars() {
321
- // for (let key in this.globalEnvironmentVariables) {
322
- // if (this.globalEnvironmentVariables.hasOwnProperty(key)) {
323
- // delete this.globalEnvironmentVariables[key];
324
- // }
325
- // }
326
- // }
322
+ deleteAllGlobalEnvVars() {
323
+ const keys = Object.keys(this.globalEnvironmentVariables);
324
+ if (!keys.length) return;
325
+ for (const key of keys) {
326
+ delete this.globalEnvironmentVariables[key];
327
+ }
328
+ this._globalEnvDirty = true;
329
+ }
327
330
 
328
331
  getOauth2CredentialVar(key) {
329
332
  return this.interpolate(this.oauth2CredentialVariables[key]);
@@ -363,7 +366,10 @@ class Bru {
363
366
  );
364
367
  }
365
368
 
366
- this.runtimeVariables[key] = value;
369
+ if (!Object.hasOwn(this.runtimeVariables, key) || !isEqual(this.runtimeVariables[key], value)) {
370
+ this.runtimeVariables[key] = value;
371
+ this._runtimeVarsDirty = true;
372
+ }
367
373
  }
368
374
 
369
375
  getVar(key) {
@@ -378,15 +384,19 @@ class Bru {
378
384
  }
379
385
 
380
386
  deleteVar(key) {
381
- delete this.runtimeVariables[key];
387
+ if (Object.hasOwn(this.runtimeVariables, key)) {
388
+ delete this.runtimeVariables[key];
389
+ this._runtimeVarsDirty = true;
390
+ }
382
391
  }
383
392
 
384
393
  deleteAllVars() {
385
- for (let key in this.runtimeVariables) {
386
- if (this.runtimeVariables.hasOwnProperty(key)) {
387
- delete this.runtimeVariables[key];
388
- }
394
+ const keys = Object.keys(this.runtimeVariables);
395
+ if (!keys.length) return;
396
+ for (const key of keys) {
397
+ delete this.runtimeVariables[key];
389
398
  }
399
+ this._runtimeVarsDirty = true;
390
400
  }
391
401
 
392
402
  getAllVars() {
@@ -397,48 +407,47 @@ class Bru {
397
407
  return this.interpolate(this.collectionVariables[key]);
398
408
  }
399
409
 
400
- // TODO: setCollectionVar works in the request lifecycle but does not update the UI.
401
- // Re-enable once the UI sync issue is resolved.
402
- // setCollectionVar(key, value) {
403
- // if (!key) {
404
- // throw new Error('Creating a variable without specifying a name is not allowed.');
405
- // }
406
- //
407
- // if (variableNameRegex.test(key) === false) {
408
- // throw new Error(
409
- // `Variable name: "${key}" contains invalid characters!`
410
- // + ' Names must only contain alpha-numeric characters, "-", "_", "."'
411
- // );
412
- // }
413
- //
414
- // this.collectionVariables[key] = value;
415
- // }
410
+ setCollectionVar(key, value) {
411
+ if (!key) {
412
+ throw new Error('Creating a variable without specifying a name is not allowed.');
413
+ }
414
+
415
+ if (variableNameRegex.test(key) === false) {
416
+ throw new Error(
417
+ `Variable name: "${key}" contains invalid characters!`
418
+ + ' Names must only contain alpha-numeric characters, "-", "_", "."'
419
+ );
420
+ }
421
+
422
+ if (!Object.hasOwn(this.collectionVariables, key) || !isEqual(this.collectionVariables[key], value)) {
423
+ this.collectionVariables[key] = value;
424
+ this._collVarsDirty = true;
425
+ }
426
+ }
416
427
 
417
428
  hasCollectionVar(key) {
418
429
  return Object.hasOwn(this.collectionVariables, key);
419
430
  }
420
431
 
421
- // TODO: deleteCollectionVar works in the request lifecycle but does not update the UI.
422
- // Re-enable once the UI sync issue is resolved.
423
- // deleteCollectionVar(key) {
424
- // delete this.collectionVariables[key];
425
- // }
426
-
427
- // TODO: deleteAllCollectionVars works in the request lifecycle but does not update the UI.
428
- // Re-enable once the UI sync issue is resolved.
429
- // deleteAllCollectionVars() {
430
- // for (let key in this.collectionVariables) {
431
- // if (this.collectionVariables.hasOwnProperty(key)) {
432
- // delete this.collectionVariables[key];
433
- // }
434
- // }
435
- // }
436
-
437
- // TODO: getAllCollectionVars works in the request lifecycle but does not update the UI.
438
- // Re-enable once the UI sync issue is resolved.
439
- // getAllCollectionVars() {
440
- // return Object.assign({}, this.collectionVariables);
441
- // }
432
+ deleteCollectionVar(key) {
433
+ if (Object.hasOwn(this.collectionVariables, key)) {
434
+ delete this.collectionVariables[key];
435
+ this._collVarsDirty = true;
436
+ }
437
+ }
438
+
439
+ deleteAllCollectionVars() {
440
+ const keys = Object.keys(this.collectionVariables);
441
+ if (!keys.length) return;
442
+ for (const key of keys) {
443
+ delete this.collectionVariables[key];
444
+ }
445
+ this._collVarsDirty = true;
446
+ }
447
+
448
+ getAllCollectionVars() {
449
+ return Object.assign({}, this.collectionVariables);
450
+ }
442
451
 
443
452
  getFolderVar(key) {
444
453
  return this.interpolate(this.folderVariables[key]);
@@ -491,13 +500,22 @@ class Bru {
491
500
  }
492
501
  if (type === 'table') {
493
502
  if (data?.provider === 'ag-grid') {
494
- if (!data?.props?.columnDefinitions) {
495
- throw new Error(`columns definitions are required`);
503
+ const { columnDefinitions, rowData } = data?.props || {};
504
+ let error;
505
+ if (rowData == null) {
506
+ error = 'Row data is required. Please provide an array of objects for table rendering.';
507
+ } else if (!Array.isArray(rowData)) {
508
+ error = `Invalid row data: expected an array of objects, received ${typeof rowData}.`;
509
+ } else if (columnDefinitions == null) {
510
+ error = 'Column definitions are required.';
511
+ } else if (!Array.isArray(columnDefinitions)) {
512
+ error = `Invalid column definitions: expected an array, received ${typeof columnDefinitions}.`;
496
513
  }
497
- if (!data?.props?.rowData) {
498
- throw new Error(`row data is required`);
514
+ if (error) {
515
+ this.setVisualizations({ uid: uuid(), type, data: { ...data, error } });
516
+ } else {
517
+ this.setVisualizations({ uid: uuid(), type, data });
499
518
  }
500
- this.setVisualizations({ uid: uuid(), type, data });
501
519
  } else if (data?.provider === 'react-table') {
502
520
  this.setVisualizations({ uid: uuid(), type, data });
503
521
  }
@@ -70,7 +70,13 @@ class BrunoRequest {
70
70
  if (segment.startsWith(':')) {
71
71
  const paramName = segment.slice(1);
72
72
  const pathParam = this.req.pathParams.find((param) => param.name === paramName);
73
- if (pathParam && pathParam.value) {
73
+ if (
74
+ pathParam
75
+ && pathParam.enabled !== false
76
+ && pathParam.value !== null
77
+ && pathParam.value !== undefined
78
+ && (typeof pathParam.value !== 'string' || pathParam.value.trim() !== '')
79
+ ) {
74
80
  return pathParam.value;
75
81
  }
76
82
  }
@@ -32,7 +32,10 @@ class BrunoResponse {
32
32
  }
33
33
 
34
34
  getHeader(name) {
35
- return this.res && this.res.headers ? this.res.headers[name] : null;
35
+ if (typeof name !== 'string' || !this.res?.headers) {
36
+ return null;
37
+ }
38
+ return this.res.headers[name.toLowerCase()];
36
39
  }
37
40
 
38
41
  getHeaders() {
@@ -32,7 +32,7 @@ class ScriptRuntime {
32
32
  collectionName
33
33
  ) {
34
34
  let visualizations = [];
35
- let setVisualizations = (data) => {
35
+ const setVisualizations = (data) => {
36
36
  if (data.type === VISUALIZATION_CLEAR) {
37
37
  visualizations = [];
38
38
  } else {
@@ -106,11 +106,11 @@ class ScriptRuntime {
106
106
  // Extracted to avoid duplication across runtime branches
107
107
  const buildRequestScriptResult = () => ({
108
108
  request,
109
- envVariables: cleanJson(envVariables),
110
- runtimeVariables: cleanJson(runtimeVariables),
109
+ envVariables: bru._envDirty ? cleanJson(envVariables) : null,
110
+ runtimeVariables: bru._runtimeVarsDirty ? cleanJson(runtimeVariables) : null,
111
+ collectionVariables: bru._collVarsDirty ? cleanJson(collectionVariables) : null,
112
+ globalEnvironmentVariables: bru._globalEnvDirty ? cleanJson(globalEnvironmentVariables) : null,
111
113
  visualizations,
112
- persistentEnvVariables: bru.persistentEnvVariables,
113
- globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
114
114
  oauth2CredentialsToReset: bru.oauth2CredentialsToReset,
115
115
  results: cleanJson(__brunoTestResults.getResults()),
116
116
  nextRequestName: bru.nextRequest,
@@ -183,7 +183,7 @@ class ScriptRuntime {
183
183
  collectionName
184
184
  ) {
185
185
  let visualizations = [];
186
- let setVisualizations = (data) => {
186
+ const setVisualizations = (data) => {
187
187
  if (data.type === VISUALIZATION_CLEAR) {
188
188
  visualizations = [];
189
189
  } else {
@@ -259,11 +259,11 @@ class ScriptRuntime {
259
259
  // Extracted to avoid duplication across runtime branches
260
260
  const buildResponseScriptResult = () => ({
261
261
  response,
262
- envVariables: cleanJson(envVariables),
263
- persistentEnvVariables: cleanJson(bru.persistentEnvVariables),
264
- runtimeVariables: cleanJson(runtimeVariables),
262
+ envVariables: bru._envDirty ? cleanJson(envVariables) : null,
263
+ runtimeVariables: bru._runtimeVarsDirty ? cleanJson(runtimeVariables) : null,
264
+ collectionVariables: bru._collVarsDirty ? cleanJson(collectionVariables) : null,
265
+ globalEnvironmentVariables: bru._globalEnvDirty ? cleanJson(globalEnvironmentVariables) : null,
265
266
  visualizations,
266
- globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
267
267
  oauth2CredentialsToReset: bru.oauth2CredentialsToReset,
268
268
  results: cleanJson(__brunoTestResults.getResults()),
269
269
  nextRequestName: bru.nextRequest,
@@ -69,11 +69,13 @@ class TestRuntime {
69
69
  if (!testsFile || !testsFile.length) {
70
70
  return {
71
71
  request,
72
- envVariables,
73
- runtimeVariables,
74
- globalEnvironmentVariables,
72
+ envVariables: null,
73
+ runtimeVariables: null,
74
+ collectionVariables: null,
75
+ globalEnvironmentVariables: null,
75
76
  results: __brunoTestResults.getResults(),
76
- nextRequestName: bru.nextRequest
77
+ nextRequestName: bru.nextRequest,
78
+ stopExecution: bru.stopExecution
77
79
  };
78
80
  }
79
81
 
@@ -141,13 +143,14 @@ class TestRuntime {
141
143
 
142
144
  const result = {
143
145
  request,
144
- envVariables: cleanJson(envVariables),
145
- runtimeVariables: cleanJson(runtimeVariables),
146
- globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
147
- persistentEnvVariables: cleanJson(bru.persistentEnvVariables),
146
+ envVariables: bru._envDirty ? cleanJson(envVariables) : null,
147
+ runtimeVariables: bru._runtimeVarsDirty ? cleanJson(runtimeVariables) : null,
148
+ collectionVariables: bru._collVarsDirty ? cleanJson(collectionVariables) : null,
149
+ globalEnvironmentVariables: bru._globalEnvDirty ? cleanJson(globalEnvironmentVariables) : null,
148
150
  oauth2CredentialsToReset: bru.oauth2CredentialsToReset,
149
151
  results: cleanJson(__brunoTestResults.getResults()),
150
152
  nextRequestName: bru.nextRequest,
153
+ stopExecution: bru.stopExecution,
151
154
  scriptedRequestEntries: cleanJson(bru.scriptedRequestEntries || [])
152
155
  };
153
156
 
@@ -92,10 +92,10 @@ class VarsRuntime {
92
92
  }
93
93
 
94
94
  return {
95
- envVariables,
96
- runtimeVariables,
97
- globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
98
- persistentEnvVariables: cleanJson(bru.persistentEnvVariables),
95
+ envVariables: bru._envDirty ? cleanJson(envVariables) : null,
96
+ runtimeVariables: bru._runtimeVarsDirty ? cleanJson(runtimeVariables) : null,
97
+ collectionVariables: bru._collVarsDirty ? cleanJson(collectionVariables) : null,
98
+ globalEnvironmentVariables: bru._globalEnvDirty ? cleanJson(globalEnvironmentVariables) : null,
99
99
  error
100
100
  };
101
101
  }
@@ -66,7 +66,7 @@ function resolveLocalModulePath(fromDir, moduleName) {
66
66
  * @param {Object} options.isolatedContext - The VM isolated context created with vm.createContext()
67
67
  * @param {string} options.currentModuleDir - Current module directory for resolving relative paths
68
68
  * @param {Map} options.localModuleCache - Cache for loaded modules
69
- * @param {string[]} options.additionalContextRootsAbsolute - Additional allowed root paths
69
+ * @param {string[]} options.additionalContextRootsAbsolute - Allowed roots for local file imports
70
70
  * @returns {Function} Custom require function
71
71
  */
72
72
  function createCustomRequire({
@@ -115,6 +115,7 @@ function createCustomRequire({
115
115
  return loadNpmModule({
116
116
  moduleName,
117
117
  collectionPath,
118
+ currentModuleDir,
118
119
  isolatedContext,
119
120
  localModuleCache
120
121
  });
@@ -269,7 +270,13 @@ function executeModuleInVmContext({
269
270
  }
270
271
 
271
272
  /**
272
- * Loads an npm module into the vm context
273
+ * Loads an npm module into the vm context.
274
+ *
275
+ * Resolution order matches standard Node.js walk-up:
276
+ * 1. currentModuleDir/node_modules → walk up parent dirs
277
+ * 2. collectionPath/node_modules
278
+ * 3. Bruno's bundled node_modules (final fallback for chai/ajv/axios/etc.)
279
+ *
273
280
  * @param {Object} options - Configuration options
274
281
  * @returns {*} The exported content of the loaded module
275
282
  * @throws {Error} When module cannot be resolved or loaded
@@ -277,19 +284,22 @@ function executeModuleInVmContext({
277
284
  function loadNpmModule({
278
285
  moduleName,
279
286
  collectionPath,
287
+ currentModuleDir,
280
288
  isolatedContext,
281
289
  localModuleCache
282
290
  }) {
283
291
  let resolvedPath;
284
292
 
285
- // Module resolution order:
286
- // 1. Collection's node_modules (user-installed packages for their collection)
287
- // 2. Bruno's node_modules (fallback for built-in dependencies)
288
- //
289
- // This order ensures user packages take precedence, allowing users to:
290
- // - Override Bruno's bundled package versions
291
- // - Install collection-specific dependencies
292
- if (collectionPath) {
293
+ if (currentModuleDir) {
294
+ try {
295
+ const callerRequire = nodeModule.createRequire(path.join(currentModuleDir, 'package.json'));
296
+ resolvedPath = callerRequire.resolve(moduleName);
297
+ } catch {
298
+ // Not found via walk-up, continue to fallbacks
299
+ }
300
+ }
301
+
302
+ if (!resolvedPath && collectionPath) {
293
303
  try {
294
304
  const collectionRequire = nodeModule.createRequire(path.join(collectionPath, 'package.json'));
295
305
  resolvedPath = collectionRequire.resolve(moduleName);
@@ -298,7 +308,7 @@ function loadNpmModule({
298
308
  }
299
309
  }
300
310
 
301
- // Fall back to Bruno's node_modules
311
+ // Fall back to Bruno's bundled node_modules
302
312
  if (!resolvedPath) {
303
313
  try {
304
314
  resolvedPath = require.resolve(moduleName, { paths: module.paths });
@@ -320,7 +330,11 @@ function loadNpmModule({
320
330
  }
321
331
 
322
332
  /**
323
- * Creates require function for npm module dependencies
333
+ * Creates the require function handed to a loaded npm module. Resolution is
334
+ * plain Node.js walk-up from the module's own directory — internal relative
335
+ * requires, sibling packages, and npm-linked / file: dependencies all resolve
336
+ * the way native `require` would from that location.
337
+ *
324
338
  * @param {Object} options - Configuration options
325
339
  * @returns {Function} Custom require function for npm module dependencies
326
340
  */
@@ -4,6 +4,36 @@ const path = require('path');
4
4
  const os = require('os');
5
5
  const { runScriptInNodeVm } = require('./index');
6
6
 
7
+ // Windows denies symlink creation without developer mode / admin. Probe once at
8
+ // module load so the dependent tests can be marked skipped in the reporter
9
+ // instead of silently no-oping mid-test.
10
+ const symlinksSupported = (() => {
11
+ const target = fs.mkdtempSync(path.join(os.tmpdir(), 'bruno-symlink-probe-'));
12
+ const link = target + '-link';
13
+ try {
14
+ fs.symlinkSync(target, link, 'dir');
15
+ fs.unlinkSync(link);
16
+ return true;
17
+ } catch (e) {
18
+ if (e.code === 'EPERM' || e.code === 'ENOTSUP') return false;
19
+ throw e;
20
+ } finally {
21
+ fs.rmSync(target, { recursive: true, force: true });
22
+ }
23
+ })();
24
+ const itIfSymlinks = symlinksSupported ? it : it.skip;
25
+
26
+ const makePkg = (parentDir, pkgName, files) => {
27
+ const pkgDir = path.join(parentDir, pkgName);
28
+ fs.mkdirSync(pkgDir, { recursive: true });
29
+ for (const [relPath, content] of Object.entries(files)) {
30
+ const filePath = path.join(pkgDir, relPath);
31
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
32
+ fs.writeFileSync(filePath, content);
33
+ }
34
+ return pkgDir;
35
+ };
36
+
7
37
  describe('node-vm sandbox', () => {
8
38
  let testDir;
9
39
  let collectionPath;
@@ -240,6 +270,256 @@ describe('node-vm sandbox', () => {
240
270
  // Nested module should successfully access the additional root
241
271
  expect(context.bru.setVar).toHaveBeenCalledWith('result', true);
242
272
  });
273
+
274
+ it('should not cross-resolve npm package from a sibling additionalContextRoot when required by a collection script', async () => {
275
+ // Package lives only in additionalRoot/node_modules — the collection has
276
+ // no dependency declared for it.
277
+ const additionalRoot = path.join(testDir, 'shared');
278
+ makePkg(path.join(additionalRoot, 'node_modules'), 'shared-package', {
279
+ 'index.js': 'module.exports = { fromShared: true };'
280
+ });
281
+
282
+ // A COLLECTION script directly requiring `shared-package` must fail:
283
+ // native Node walk-up from the collection never reaches a sibling
284
+ // additional root's node_modules, and cross-root discovery for bare-name
285
+ // resolution is intentionally not implemented. The supported patterns
286
+ // are (a) require the package from a shared script that itself lives
287
+ // inside additionalRoot (see the next test), or (b) declare the dep in
288
+ // the collection's own package.json.
289
+ const script = `require('shared-package');`;
290
+
291
+ const context = {
292
+ bru: { setVar: jest.fn() },
293
+ console: console
294
+ };
295
+
296
+ const scriptingConfig = {
297
+ additionalContextRoots: [additionalRoot]
298
+ };
299
+
300
+ await expect(
301
+ runScriptInNodeVm({ script, context, collectionPath, scriptingConfig })
302
+ ).rejects.toThrow(/Could not resolve module "shared-package"/);
303
+ });
304
+
305
+ it('should resolve npm module required by a shared script in additionalContextRoots', async () => {
306
+ const additionalRoot = path.join(testDir, 'shared');
307
+ makePkg(path.join(additionalRoot, 'node_modules'), 'shared-util', {
308
+ 'index.js': 'module.exports = { parse: function(s) { return JSON.parse(s); } };'
309
+ });
310
+ fs.writeFileSync(
311
+ path.join(additionalRoot, 'parser.js'),
312
+ 'const sharedUtil = require("shared-util"); module.exports = { parse: sharedUtil.parse };'
313
+ );
314
+
315
+ // Collection script requires the shared local script, which internally
316
+ // requires an npm package from the shared root's node_modules
317
+ const script = `
318
+ const parser = require('../shared/parser');
319
+ const result = parser.parse('{"ok":true}');
320
+ bru.setVar('result', result.ok);
321
+ `;
322
+
323
+ const context = {
324
+ bru: { setVar: jest.fn() },
325
+ console: console
326
+ };
327
+
328
+ const scriptingConfig = {
329
+ additionalContextRoots: [additionalRoot]
330
+ };
331
+
332
+ await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });
333
+
334
+ expect(context.bru.setVar).toHaveBeenCalledWith('result', true);
335
+ });
336
+
337
+ it('should walk up from a nested shared script to find its npm dependency', async () => {
338
+ // Structure:
339
+ // shared/
340
+ // node_modules/deep-dep/index.js ← package hoisted at shared root
341
+ // deep/nested/parser.js ← requires 'deep-dep'
342
+ const additionalRoot = path.join(testDir, 'shared');
343
+ const nestedDir = path.join(additionalRoot, 'deep', 'nested');
344
+ fs.mkdirSync(nestedDir, { recursive: true });
345
+
346
+ makePkg(path.join(additionalRoot, 'node_modules'), 'deep-dep', {
347
+ 'index.js': 'module.exports = { walkedUp: true };'
348
+ });
349
+
350
+ fs.writeFileSync(
351
+ path.join(nestedDir, 'parser.js'),
352
+ 'const dep = require("deep-dep"); module.exports = { ok: dep.walkedUp };'
353
+ );
354
+
355
+ const script = `
356
+ const parser = require('../shared/deep/nested/parser');
357
+ bru.setVar('result', parser.ok);
358
+ `;
359
+
360
+ const context = {
361
+ bru: { setVar: jest.fn() },
362
+ console: console
363
+ };
364
+
365
+ const scriptingConfig = {
366
+ additionalContextRoots: [additionalRoot]
367
+ };
368
+
369
+ await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });
370
+
371
+ expect(context.bru.setVar).toHaveBeenCalledWith('result', true);
372
+ });
373
+
374
+ itIfSymlinks('should resolve npm modules when additionalContextRoots points at a symlink', async () => {
375
+ // Physical location of the shared root
376
+ const realShared = path.join(testDir, 'real-shared');
377
+ makePkg(path.join(realShared, 'node_modules'), 'symlinked-lib', {
378
+ 'index.js': 'module.exports = { via: "symlink" };'
379
+ });
380
+
381
+ // Shared script inside the real location that requires the npm package.
382
+ // Loaded through the symlink below.
383
+ fs.writeFileSync(
384
+ path.join(realShared, 'helper.js'),
385
+ 'const pkg = require("symlinked-lib"); module.exports = { via: pkg.via };'
386
+ );
387
+
388
+ // User-facing symlink that Bruno is told to treat as the shared root.
389
+ const linkedShared = path.join(testDir, 'linked-shared');
390
+ fs.symlinkSync(realShared, linkedShared, 'dir');
391
+
392
+ const script = `
393
+ const helper = require('../linked-shared/helper');
394
+ bru.setVar('via', helper.via);
395
+ `;
396
+
397
+ const context = {
398
+ bru: { setVar: jest.fn() },
399
+ console: console
400
+ };
401
+
402
+ const scriptingConfig = {
403
+ additionalContextRoots: [linkedShared]
404
+ };
405
+
406
+ await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });
407
+
408
+ expect(context.bru.setVar).toHaveBeenCalledWith('via', 'symlink');
409
+ });
410
+
411
+ itIfSymlinks('should allow subpath imports into an npm-linked package', async () => {
412
+ // Physical location of a multi-file package outside every declared root.
413
+ // Subpath file utils.js — require('subpath-pkg/utils') maps to utils.js
414
+ // (a file, not a directory-with-index.js).
415
+ const externalPkg = makePkg(testDir, 'external-subpath-pkg', {
416
+ 'package.json': JSON.stringify({ name: 'subpath-pkg', main: 'index.js' }),
417
+ 'index.js': 'module.exports = { root: true };',
418
+ 'utils.js': 'module.exports = { greet: () => "sub-hello" };'
419
+ });
420
+
421
+ const nodeModulesDir = path.join(collectionPath, 'node_modules');
422
+ fs.mkdirSync(nodeModulesDir, { recursive: true });
423
+ fs.symlinkSync(externalPkg, path.join(nodeModulesDir, 'subpath-pkg'), 'dir');
424
+
425
+ const script = `
426
+ const utils = require('subpath-pkg/utils');
427
+ bru.setVar('result', utils.greet());
428
+ `;
429
+
430
+ const context = {
431
+ bru: { setVar: jest.fn() },
432
+ console: console
433
+ };
434
+
435
+ await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
436
+
437
+ expect(context.bru.setVar).toHaveBeenCalledWith('result', 'sub-hello');
438
+ });
439
+
440
+ itIfSymlinks('should allow internal relative requires inside an npm-linked package', async () => {
441
+ // Physical location of a multi-file package outside every declared root.
442
+ const externalPkg = makePkg(testDir, 'external-pkg', {
443
+ 'package.json': JSON.stringify({ name: 'linked-pkg', main: 'index.js' }),
444
+ 'index.js': 'const util = require("./util"); module.exports = { greet: util.greet };',
445
+ 'util.js': 'module.exports = { greet: () => "hello" };'
446
+ });
447
+
448
+ // npm-link style: collection has node_modules/<pkg> as a symlink to the
449
+ // physical location that lives outside the collection.
450
+ const nodeModulesDir = path.join(collectionPath, 'node_modules');
451
+ fs.mkdirSync(nodeModulesDir, { recursive: true });
452
+ fs.symlinkSync(externalPkg, path.join(nodeModulesDir, 'linked-pkg'), 'dir');
453
+
454
+ const script = `
455
+ const pkg = require('linked-pkg');
456
+ bru.setVar('result', pkg.greet());
457
+ `;
458
+
459
+ const context = {
460
+ bru: { setVar: jest.fn() },
461
+ console: console
462
+ };
463
+
464
+ await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
465
+
466
+ expect(context.bru.setVar).toHaveBeenCalledWith('result', 'hello');
467
+ });
468
+
469
+ it('should allow a package in collection node_modules to require a sibling package', async () => {
470
+ // Two packages installed side-by-side in the collection's node_modules —
471
+ // pkg-a transitively requires pkg-b.
472
+ const nodeModulesDir = path.join(collectionPath, 'node_modules');
473
+ makePkg(nodeModulesDir, 'pkg-a', {
474
+ 'package.json': JSON.stringify({ name: 'pkg-a', main: 'index.js' }),
475
+ 'index.js': 'const b = require("pkg-b"); module.exports = { value: b.value + 1 };'
476
+ });
477
+ makePkg(nodeModulesDir, 'pkg-b', {
478
+ 'package.json': JSON.stringify({ name: 'pkg-b', main: 'index.js' }),
479
+ 'index.js': 'module.exports = { value: 41 };'
480
+ });
481
+
482
+ const script = `
483
+ const a = require('pkg-a');
484
+ bru.setVar('result', a.value);
485
+ `;
486
+
487
+ const context = {
488
+ bru: { setVar: jest.fn() },
489
+ console: console
490
+ };
491
+
492
+ await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
493
+
494
+ expect(context.bru.setVar).toHaveBeenCalledWith('result', 42);
495
+ });
496
+
497
+ itIfSymlinks('should allow a scoped npm-linked package', async () => {
498
+ // Physical location of a scoped multi-file package outside every declared root.
499
+ const externalPkg = makePkg(testDir, 'external-scoped-pkg', {
500
+ 'package.json': JSON.stringify({ name: '@bruno/scoped-pkg', main: 'index.js' }),
501
+ 'index.js': 'const util = require("./util"); module.exports = { greet: util.greet };',
502
+ 'util.js': 'module.exports = { greet: () => "scoped-hello" };'
503
+ });
504
+
505
+ const scopeDir = path.join(collectionPath, 'node_modules', '@bruno');
506
+ fs.mkdirSync(scopeDir, { recursive: true });
507
+ fs.symlinkSync(externalPkg, path.join(scopeDir, 'scoped-pkg'), 'dir');
508
+
509
+ const script = `
510
+ const pkg = require('@bruno/scoped-pkg');
511
+ bru.setVar('result', pkg.greet());
512
+ `;
513
+
514
+ const context = {
515
+ bru: { setVar: jest.fn() },
516
+ console: console
517
+ };
518
+
519
+ await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
520
+
521
+ expect(context.bru.setVar).toHaveBeenCalledWith('result', 'scoped-hello');
522
+ });
243
523
  });
244
524
 
245
525
  describe('createCustomRequire - npm modules', () => {
@@ -7,211 +7,207 @@ const addBruShimToContext = (vm, bru) => {
7
7
  const bruRunnerObject = vm.newObject();
8
8
  const bruRunnerIterationDataObject = vm.newObject();
9
9
 
10
- let cwd = vm.newFunction('cwd', function () {
10
+ const cwd = vm.newFunction('cwd', function () {
11
11
  return marshallToVm(bru.cwd(), vm);
12
12
  });
13
13
  vm.setProp(bruObject, 'cwd', cwd);
14
14
  cwd.dispose();
15
15
 
16
- let getEnvName = vm.newFunction('getEnvName', function () {
16
+ const getEnvName = vm.newFunction('getEnvName', function () {
17
17
  return marshallToVm(bru.getEnvName(), vm);
18
18
  });
19
19
  vm.setProp(bruObject, 'getEnvName', getEnvName);
20
20
  getEnvName.dispose();
21
21
 
22
- let getCollectionName = vm.newFunction('getCollectionName', function () {
22
+ const getCollectionName = vm.newFunction('getCollectionName', function () {
23
23
  return marshallToVm(bru.getCollectionName(), vm);
24
24
  });
25
25
  vm.setProp(bruObject, 'getCollectionName', getCollectionName);
26
26
  getCollectionName.dispose();
27
27
 
28
- let isSafeMode = vm.newFunction('isSafeMode', function () {
28
+ const isSafeMode = vm.newFunction('isSafeMode', function () {
29
29
  return marshallToVm(bru.isSafeMode(), vm);
30
30
  });
31
31
  vm.setProp(bruObject, 'isSafeMode', isSafeMode);
32
32
  isSafeMode.dispose();
33
33
 
34
- let getProcessEnv = vm.newFunction('getProcessEnv', function (key) {
34
+ const getProcessEnv = vm.newFunction('getProcessEnv', function (key) {
35
35
  return marshallToVm(bru.getProcessEnv(vm.dump(key)), vm);
36
36
  });
37
37
  vm.setProp(bruObject, 'getProcessEnv', getProcessEnv);
38
38
  getProcessEnv.dispose();
39
39
 
40
- let interpolate = vm.newFunction('interpolate', function (str) {
40
+ const interpolate = vm.newFunction('interpolate', function (str) {
41
41
  return marshallToVm(bru.interpolate(vm.dump(str)), vm);
42
42
  });
43
43
  vm.setProp(bruObject, 'interpolate', interpolate);
44
44
  interpolate.dispose();
45
45
 
46
- let hasEnvVar = vm.newFunction('hasEnvVar', function (key) {
46
+ const hasEnvVar = vm.newFunction('hasEnvVar', function (key) {
47
47
  return marshallToVm(bru.hasEnvVar(vm.dump(key)), vm);
48
48
  });
49
49
  vm.setProp(bruObject, 'hasEnvVar', hasEnvVar);
50
50
  hasEnvVar.dispose();
51
51
 
52
- let getEnvVar = vm.newFunction('getEnvVar', function (key) {
52
+ const getEnvVar = vm.newFunction('getEnvVar', function (key) {
53
53
  return marshallToVm(bru.getEnvVar(vm.dump(key)), vm);
54
54
  });
55
55
  vm.setProp(bruObject, 'getEnvVar', getEnvVar);
56
56
  getEnvVar.dispose();
57
57
 
58
- let setEnvVar = vm.newFunction('setEnvVar', function (key, value, options = {}) {
59
- bru.setEnvVar(vm.dump(key), vm.dump(value), vm.dump(options));
58
+ const setEnvVar = vm.newFunction('setEnvVar', function (key, value) {
59
+ bru.setEnvVar(vm.dump(key), vm.dump(value));
60
60
  });
61
61
  vm.setProp(bruObject, 'setEnvVar', setEnvVar);
62
62
  setEnvVar.dispose();
63
63
 
64
- let deleteEnvVar = vm.newFunction('deleteEnvVar', function (key) {
64
+ const deleteEnvVar = vm.newFunction('deleteEnvVar', function (key) {
65
65
  bru.deleteEnvVar(vm.dump(key));
66
66
  });
67
67
  vm.setProp(bruObject, 'deleteEnvVar', deleteEnvVar);
68
68
  deleteEnvVar.dispose();
69
69
 
70
- let getAllEnvVars = vm.newFunction('getAllEnvVars', function () {
70
+ const getAllEnvVars = vm.newFunction('getAllEnvVars', function () {
71
71
  return marshallToVm(bru.getAllEnvVars(), vm);
72
72
  });
73
73
  vm.setProp(bruObject, 'getAllEnvVars', getAllEnvVars);
74
74
  getAllEnvVars.dispose();
75
75
 
76
- let deleteAllEnvVars = vm.newFunction('deleteAllEnvVars', function () {
76
+ const deleteAllEnvVars = vm.newFunction('deleteAllEnvVars', function () {
77
77
  bru.deleteAllEnvVars();
78
78
  });
79
79
  vm.setProp(bruObject, 'deleteAllEnvVars', deleteAllEnvVars);
80
80
  deleteAllEnvVars.dispose();
81
81
 
82
- let getGlobalEnvVar = vm.newFunction('getGlobalEnvVar', function (key) {
82
+ const getGlobalEnvVar = vm.newFunction('getGlobalEnvVar', function (key) {
83
83
  return marshallToVm(bru.getGlobalEnvVar(vm.dump(key)), vm);
84
84
  });
85
85
  vm.setProp(bruObject, 'getGlobalEnvVar', getGlobalEnvVar);
86
86
  getGlobalEnvVar.dispose();
87
87
 
88
- let getOauth2CredentialVar = vm.newFunction('getOauth2CredentialVar', function (key) {
88
+ const getOauth2CredentialVar = vm.newFunction('getOauth2CredentialVar', function (key) {
89
89
  return marshallToVm(bru.getOauth2CredentialVar(vm.dump(key)), vm);
90
90
  });
91
91
  vm.setProp(bruObject, 'getOauth2CredentialVar', getOauth2CredentialVar);
92
92
  getOauth2CredentialVar.dispose();
93
93
 
94
- let resetOauth2Credential = vm.newFunction('resetOauth2Credential', function (credentialId) {
94
+ const resetOauth2Credential = vm.newFunction('resetOauth2Credential', function (credentialId) {
95
95
  bru.resetOauth2Credential(vm.dump(credentialId));
96
96
  });
97
97
  vm.setProp(bruObject, 'resetOauth2Credential', resetOauth2Credential);
98
98
  resetOauth2Credential.dispose();
99
99
 
100
- let setGlobalEnvVar = vm.newFunction('setGlobalEnvVar', function (key, value) {
100
+ const setGlobalEnvVar = vm.newFunction('setGlobalEnvVar', function (key, value) {
101
101
  bru.setGlobalEnvVar(vm.dump(key), vm.dump(value));
102
102
  });
103
103
  vm.setProp(bruObject, 'setGlobalEnvVar', setGlobalEnvVar);
104
104
  setGlobalEnvVar.dispose();
105
105
 
106
- // TODO: deleteGlobalEnvVar works in the request lifecycle but does not update the UI.
107
- // Re-enable once the UI sync issue is resolved.
108
- // let deleteGlobalEnvVar = vm.newFunction('deleteGlobalEnvVar', function (key) {
109
- // bru.deleteGlobalEnvVar(vm.dump(key));
110
- // });
111
- // vm.setProp(bruObject, 'deleteGlobalEnvVar', deleteGlobalEnvVar);
112
- // deleteGlobalEnvVar.dispose();
106
+ const deleteGlobalEnvVar = vm.newFunction('deleteGlobalEnvVar', function (key) {
107
+ bru.deleteGlobalEnvVar(vm.dump(key));
108
+ });
109
+ vm.setProp(bruObject, 'deleteGlobalEnvVar', deleteGlobalEnvVar);
110
+ deleteGlobalEnvVar.dispose();
113
111
 
114
- let getAllGlobalEnvVars = vm.newFunction('getAllGlobalEnvVars', function () {
112
+ const getAllGlobalEnvVars = vm.newFunction('getAllGlobalEnvVars', function () {
115
113
  return marshallToVm(bru.getAllGlobalEnvVars(), vm);
116
114
  });
117
115
  vm.setProp(bruObject, 'getAllGlobalEnvVars', getAllGlobalEnvVars);
118
116
  getAllGlobalEnvVars.dispose();
119
117
 
120
- let hasGlobalEnvVar = vm.newFunction('hasGlobalEnvVar', function (key) {
118
+ const hasGlobalEnvVar = vm.newFunction('hasGlobalEnvVar', function (key) {
121
119
  return marshallToVm(bru.hasGlobalEnvVar(vm.dump(key)), vm);
122
120
  });
123
121
  vm.setProp(bruObject, 'hasGlobalEnvVar', hasGlobalEnvVar);
124
122
  hasGlobalEnvVar.dispose();
125
123
 
126
- // TODO: deleteAllGlobalEnvVars works in the request lifecycle but does not update the UI.
127
- // Re-enable once the UI sync issue is resolved.
128
- // let deleteAllGlobalEnvVars = vm.newFunction('deleteAllGlobalEnvVars', function () {
129
- // bru.deleteAllGlobalEnvVars();
130
- // });
131
- // vm.setProp(bruObject, 'deleteAllGlobalEnvVars', deleteAllGlobalEnvVars);
132
- // deleteAllGlobalEnvVars.dispose();
124
+ const deleteAllGlobalEnvVars = vm.newFunction('deleteAllGlobalEnvVars', function () {
125
+ bru.deleteAllGlobalEnvVars();
126
+ });
127
+ vm.setProp(bruObject, 'deleteAllGlobalEnvVars', deleteAllGlobalEnvVars);
128
+ deleteAllGlobalEnvVars.dispose();
133
129
 
134
- let hasVar = vm.newFunction('hasVar', function (key) {
130
+ const hasVar = vm.newFunction('hasVar', function (key) {
135
131
  return marshallToVm(bru.hasVar(vm.dump(key)), vm);
136
132
  });
137
133
  vm.setProp(bruObject, 'hasVar', hasVar);
138
134
  hasVar.dispose();
139
135
 
140
- let getVar = vm.newFunction('getVar', function (key) {
136
+ const getVar = vm.newFunction('getVar', function (key) {
141
137
  return marshallToVm(bru.getVar(vm.dump(key)), vm);
142
138
  });
143
139
  vm.setProp(bruObject, 'getVar', getVar);
144
140
  getVar.dispose();
145
141
 
146
- let setVar = vm.newFunction('setVar', function (key, value) {
142
+ const setVar = vm.newFunction('setVar', function (key, value) {
147
143
  bru.setVar(vm.dump(key), vm.dump(value));
148
144
  });
149
145
  vm.setProp(bruObject, 'setVar', setVar);
150
146
  setVar.dispose();
151
147
 
152
- let deleteVar = vm.newFunction('deleteVar', function (key) {
148
+ const deleteVar = vm.newFunction('deleteVar', function (key) {
153
149
  bru.deleteVar(vm.dump(key));
154
150
  });
155
151
  vm.setProp(bruObject, 'deleteVar', deleteVar);
156
152
  deleteVar.dispose();
157
153
 
158
- let deleteAllVars = vm.newFunction('deleteAllVars', function () {
154
+ const deleteAllVars = vm.newFunction('deleteAllVars', function () {
159
155
  bru.deleteAllVars();
160
156
  });
161
157
  vm.setProp(bruObject, 'deleteAllVars', deleteAllVars);
162
158
  deleteAllVars.dispose();
163
159
 
164
- let getAllVars = vm.newFunction('getAllVars', function () {
160
+ const getAllVars = vm.newFunction('getAllVars', function () {
165
161
  return marshallToVm(bru.getAllVars(), vm);
166
162
  });
167
163
  vm.setProp(bruObject, 'getAllVars', getAllVars);
168
164
  getAllVars.dispose();
169
165
 
170
- let setNextRequest = vm.newFunction('setNextRequest', function (nextRequest) {
166
+ const setNextRequest = vm.newFunction('setNextRequest', function (nextRequest) {
171
167
  bru.setNextRequest(vm.dump(nextRequest));
172
168
  });
173
169
  vm.setProp(bruObject, 'setNextRequest', setNextRequest);
174
170
  setNextRequest.dispose();
175
171
 
176
- let runnerSkipRequest = vm.newFunction('skipRequest', function () {
172
+ const runnerSkipRequest = vm.newFunction('skipRequest', function () {
177
173
  bru?.runner?.skipRequest();
178
174
  });
179
175
  vm.setProp(bruRunnerObject, 'skipRequest', runnerSkipRequest);
180
176
  runnerSkipRequest.dispose();
181
177
 
182
- let runnerStopExecution = vm.newFunction('stopExecution', function () {
178
+ const runnerStopExecution = vm.newFunction('stopExecution', function () {
183
179
  bru?.runner?.stopExecution();
184
180
  });
185
181
  vm.setProp(bruRunnerObject, 'stopExecution', runnerStopExecution);
186
182
  runnerStopExecution.dispose();
187
183
 
188
- let runnerSetNextRequest = vm.newFunction('setNextRequest', function (nextRequest) {
184
+ const runnerSetNextRequest = vm.newFunction('setNextRequest', function (nextRequest) {
189
185
  bru?.runner?.setNextRequest(vm.dump(nextRequest));
190
186
  });
191
187
  vm.setProp(bruRunnerObject, 'setNextRequest', runnerSetNextRequest);
192
188
  runnerSetNextRequest.dispose();
193
189
 
194
- let runnerIterationIndex = marshallToVm(bru?.runner?.iterationIndex, vm);
190
+ const runnerIterationIndex = marshallToVm(bru?.runner?.iterationIndex, vm);
195
191
  vm.setProp(bruRunnerObject, 'iterationIndex', runnerIterationIndex);
196
192
  runnerIterationIndex.dispose();
197
193
 
198
- let runnerTotalIterations = marshallToVm(bru?.runner?.totalIterations, vm);
194
+ const runnerTotalIterations = marshallToVm(bru?.runner?.totalIterations, vm);
199
195
  vm.setProp(bruRunnerObject, 'totalIterations', runnerTotalIterations);
200
196
  runnerTotalIterations.dispose();
201
197
 
202
- let runnerSetIterationData = vm.newFunction('set', function (key, value) {
198
+ const runnerSetIterationData = vm.newFunction('set', function (key, value) {
203
199
  bru?.runner?.iterationData?.set(vm.dump(key), vm.dump(value));
204
200
  });
205
201
  vm.setProp(bruRunnerIterationDataObject, 'set', runnerSetIterationData);
206
202
  runnerSetIterationData.dispose();
207
203
 
208
- let runnerUnsetIterationData = vm.newFunction('unset', function (key) {
204
+ const runnerUnsetIterationData = vm.newFunction('unset', function (key) {
209
205
  bru?.runner?.iterationData?.unset(vm.dump(key));
210
206
  });
211
207
  vm.setProp(bruRunnerIterationDataObject, 'unset', runnerUnsetIterationData);
212
208
  runnerUnsetIterationData.dispose();
213
209
 
214
- let runnerGetIterationData = vm.newFunction('get', function (key) {
210
+ const runnerGetIterationData = vm.newFunction('get', function (key) {
215
211
  if (key) {
216
212
  return marshallToVm(bru?.runner?.iterationData?.get(vm.dump(key)), vm);
217
213
  }
@@ -220,13 +216,13 @@ const addBruShimToContext = (vm, bru) => {
220
216
  vm.setProp(bruRunnerIterationDataObject, 'get', runnerGetIterationData);
221
217
  runnerGetIterationData.dispose();
222
218
 
223
- let runnerHasIterationData = vm.newFunction('has', function (key) {
219
+ const runnerHasIterationData = vm.newFunction('has', function (key) {
224
220
  return marshallToVm(bru?.runner?.iterationData?.has(vm.dump(key)), vm);
225
221
  });
226
222
  vm.setProp(bruRunnerIterationDataObject, 'has', runnerHasIterationData);
227
223
  runnerHasIterationData.dispose();
228
224
 
229
- let runnerStringifyIterationData = vm.newFunction('stringify', function () {
225
+ const runnerStringifyIterationData = vm.newFunction('stringify', function () {
230
226
  return marshallToVm(bru?.runner?.iterationData?.stringify(), vm);
231
227
  });
232
228
  vm.setProp(bruRunnerIterationDataObject, 'stringify', runnerStringifyIterationData);
@@ -235,7 +231,7 @@ const addBruShimToContext = (vm, bru) => {
235
231
  vm.setProp(bruRunnerObject, 'iterationData', bruRunnerIterationDataObject);
236
232
  bruRunnerIterationDataObject.dispose();
237
233
 
238
- let visualize = vm.newFunction('visualize', function (type, data) {
234
+ const visualize = vm.newFunction('visualize', function (type, data) {
239
235
  try {
240
236
  // Guard against missing handles: vm.dump() accesses .owner on the handle
241
237
  // to verify its VM context. When called with no args, the handles are
@@ -253,75 +249,67 @@ const addBruShimToContext = (vm, bru) => {
253
249
  vm.setProp(bruObject, 'visualize', visualize);
254
250
  visualize.dispose();
255
251
 
256
- let clearVisualizations = vm.newFunction('clearVisualizations', function () {
252
+ const clearVisualizations = vm.newFunction('clearVisualizations', function () {
257
253
  bru.clearVisualizations();
258
254
  });
259
255
  vm.setProp(bruObject, 'clearVisualizations', clearVisualizations);
260
256
  clearVisualizations.dispose();
261
257
 
262
- let getSecretVar = vm.newFunction('getSecretVar', function (key) {
258
+ const getSecretVar = vm.newFunction('getSecretVar', function (key) {
263
259
  return marshallToVm(bru.getSecretVar(vm.dump(key)), vm);
264
260
  });
265
261
  vm.setProp(bruObject, 'getSecretVar', getSecretVar);
266
262
  getSecretVar.dispose();
267
263
 
268
- let getRequestVar = vm.newFunction('getRequestVar', function (key) {
264
+ const getRequestVar = vm.newFunction('getRequestVar', function (key) {
269
265
  return marshallToVm(bru.getRequestVar(vm.dump(key)), vm);
270
266
  });
271
267
  vm.setProp(bruObject, 'getRequestVar', getRequestVar);
272
268
  getRequestVar.dispose();
273
269
 
274
- let getFolderVar = vm.newFunction('getFolderVar', function (key) {
270
+ const getFolderVar = vm.newFunction('getFolderVar', function (key) {
275
271
  return marshallToVm(bru.getFolderVar(vm.dump(key)), vm);
276
272
  });
277
273
  vm.setProp(bruObject, 'getFolderVar', getFolderVar);
278
274
  getFolderVar.dispose();
279
275
 
280
- let getCollectionVar = vm.newFunction('getCollectionVar', function (key) {
276
+ const getCollectionVar = vm.newFunction('getCollectionVar', function (key) {
281
277
  return marshallToVm(bru.getCollectionVar(vm.dump(key)), vm);
282
278
  });
283
279
  vm.setProp(bruObject, 'getCollectionVar', getCollectionVar);
284
280
  getCollectionVar.dispose();
285
281
 
286
- // TODO: setCollectionVar works in the request lifecycle but does not update the UI.
287
- // Re-enable once the UI sync issue is resolved.
288
- // let setCollectionVar = vm.newFunction('setCollectionVar', function (key, value) {
289
- // bru.setCollectionVar(vm.dump(key), vm.dump(value));
290
- // });
291
- // vm.setProp(bruObject, 'setCollectionVar', setCollectionVar);
292
- // setCollectionVar.dispose();
282
+ const setCollectionVar = vm.newFunction('setCollectionVar', function (key, value) {
283
+ bru.setCollectionVar(vm.dump(key), vm.dump(value));
284
+ });
285
+ vm.setProp(bruObject, 'setCollectionVar', setCollectionVar);
286
+ setCollectionVar.dispose();
293
287
 
294
- let hasCollectionVar = vm.newFunction('hasCollectionVar', function (key) {
288
+ const hasCollectionVar = vm.newFunction('hasCollectionVar', function (key) {
295
289
  return marshallToVm(bru.hasCollectionVar(vm.dump(key)), vm);
296
290
  });
297
291
  vm.setProp(bruObject, 'hasCollectionVar', hasCollectionVar);
298
292
  hasCollectionVar.dispose();
299
293
 
300
- // TODO: deleteCollectionVar works in the request lifecycle but does not update the UI.
301
- // Re-enable once the UI sync issue is resolved.
302
- // let deleteCollectionVar = vm.newFunction('deleteCollectionVar', function (key) {
303
- // bru.deleteCollectionVar(vm.dump(key));
304
- // });
305
- // vm.setProp(bruObject, 'deleteCollectionVar', deleteCollectionVar);
306
- // deleteCollectionVar.dispose();
307
-
308
- // TODO: deleteAllCollectionVars works in the request lifecycle but does not update the UI.
309
- // Re-enable once the UI sync issue is resolved.
310
- // let deleteAllCollectionVars = vm.newFunction('deleteAllCollectionVars', function () {
311
- // bru.deleteAllCollectionVars();
312
- // });
313
- // vm.setProp(bruObject, 'deleteAllCollectionVars', deleteAllCollectionVars);
314
- // deleteAllCollectionVars.dispose();
315
-
316
- // TODO: getAllCollectionVars works in the request lifecycle but does not update the UI.
317
- // Re-enable once the UI sync issue is resolved.
318
- // let getAllCollectionVars = vm.newFunction('getAllCollectionVars', function () {
319
- // return marshallToVm(bru.getAllCollectionVars(), vm);
320
- // });
321
- // vm.setProp(bruObject, 'getAllCollectionVars', getAllCollectionVars);
322
- // getAllCollectionVars.dispose();
323
-
324
- let getTestResults = vm.newFunction('getTestResults', () => {
294
+ const deleteCollectionVar = vm.newFunction('deleteCollectionVar', function (key) {
295
+ bru.deleteCollectionVar(vm.dump(key));
296
+ });
297
+ vm.setProp(bruObject, 'deleteCollectionVar', deleteCollectionVar);
298
+ deleteCollectionVar.dispose();
299
+
300
+ const deleteAllCollectionVars = vm.newFunction('deleteAllCollectionVars', function () {
301
+ bru.deleteAllCollectionVars();
302
+ });
303
+ vm.setProp(bruObject, 'deleteAllCollectionVars', deleteAllCollectionVars);
304
+ deleteAllCollectionVars.dispose();
305
+
306
+ const getAllCollectionVars = vm.newFunction('getAllCollectionVars', function () {
307
+ return marshallToVm(bru.getAllCollectionVars(), vm);
308
+ });
309
+ vm.setProp(bruObject, 'getAllCollectionVars', getAllCollectionVars);
310
+ getAllCollectionVars.dispose();
311
+
312
+ const getTestResults = vm.newFunction('getTestResults', () => {
325
313
  const promise = vm.newPromise();
326
314
  bru
327
315
  .getTestResults()
@@ -343,7 +331,7 @@ const addBruShimToContext = (vm, bru) => {
343
331
  });
344
332
  getTestResults.consume((handle) => vm.setProp(bruObject, 'getTestResults', handle));
345
333
 
346
- let getAssertionResults = vm.newFunction('getAssertionResults', () => {
334
+ const getAssertionResults = vm.newFunction('getAssertionResults', () => {
347
335
  const promise = vm.newPromise();
348
336
  bru
349
337
  .getAssertionResults()
@@ -365,7 +353,7 @@ const addBruShimToContext = (vm, bru) => {
365
353
  });
366
354
  getAssertionResults.consume((handle) => vm.setProp(bruObject, 'getAssertionResults', handle));
367
355
 
368
- let runRequestHandle = vm.newFunction('runRequest', (args) => {
356
+ const runRequestHandle = vm.newFunction('runRequest', (args) => {
369
357
  const promise = vm.newPromise();
370
358
  bru
371
359
  .runRequest(vm.dump(args))
@@ -387,7 +375,7 @@ const addBruShimToContext = (vm, bru) => {
387
375
  });
388
376
  runRequestHandle.consume((handle) => vm.setProp(bruObject, 'runRequest', handle));
389
377
 
390
- let sendRequestHandle = vm.newFunction('_sendRequest', (args) => {
378
+ const sendRequestHandle = vm.newFunction('_sendRequest', (args) => {
391
379
  const promise = vm.newPromise();
392
380
  bru
393
381
  .sendRequest(vm.dump(args))
@@ -408,7 +396,7 @@ const addBruShimToContext = (vm, bru) => {
408
396
  sendRequestHandle.consume((handle) => vm.setProp(bruObject, '_sendRequest', handle));
409
397
 
410
398
  // On vm.global, not bru, to stay off user-facing autocomplete.
411
- let setScopeHandle = vm.newFunction('__bruSetScope', (scopeArg) => {
399
+ const setScopeHandle = vm.newFunction('__bruSetScope', (scopeArg) => {
412
400
  bru._currentScope = vm.dump(scopeArg) || null;
413
401
  });
414
402
  setScopeHandle.consume((handle) => vm.setProp(vm.global, '__bruSetScope', handle));
@@ -428,7 +416,7 @@ const addBruShimToContext = (vm, bru) => {
428
416
  });
429
417
  sleep.consume((handle) => vm.setProp(bruObject, 'sleep', handle));
430
418
 
431
- let bruCookiesObject = vm.newObject();
419
+ const bruCookiesObject = vm.newObject();
432
420
  const { evalCode: cookiesEvalCode } = createPropertyListBridge(vm, bru.cookies, bruCookiesObject, {
433
421
  globalPath: 'globalThis.bru.cookies',
434
422
  syncReadMethods: ['get', 'has', 'count', 'indexOf', 'toObject', 'toString'],