@usebruno/js 0.50.0 → 0.52.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.
@@ -2,8 +2,214 @@ const vm = require('node:vm');
2
2
  const fs = require('node:fs');
3
3
  const path = require('node:path');
4
4
  const nodeModule = require('node:module');
5
+ const { AsyncLocalStorage } = require('node:async_hooks');
5
6
 
6
7
  const { isBuiltinModule, isPathWithinAllowedRoots } = require('./utils');
8
+ const { safeGlobals } = require('./constants');
9
+ const { mixinTypedArrays } = require('../mixins/typed-arrays');
10
+
11
+ // Shared npm context (once per process) so modules aren't re-eval'd per script. #9074
12
+ const activeScriptContext = new AsyncLocalStorage();
13
+ const npmModuleEval = new AsyncLocalStorage();
14
+ const sharedNpmModuleCache = new Map();
15
+ const contextBoundModulePaths = new Set();
16
+ // path -> paths required while that module was evaluating (for context-bound eviction)
17
+ const moduleRequireGraph = new Map();
18
+ let sharedNpmSandbox = null;
19
+ let sharedNpmContext = null;
20
+
21
+ const BRUNO_CONTEXT_KEYS = [
22
+ 'bru',
23
+ 'req',
24
+ 'res',
25
+ 'test',
26
+ 'expect',
27
+ 'assert',
28
+ '__brunoTestResults',
29
+ '__bruSetScope',
30
+ 'jwt',
31
+ 'console',
32
+ 'scriptingConfig'
33
+ ];
34
+
35
+ const facades = new Map();
36
+
37
+ const RUN_LOADER_STATE = Symbol('brunoRunLoaderState');
38
+
39
+ function attachRunLoaderState(scriptContext, { localModuleCache, vmContext }) {
40
+ scriptContext[RUN_LOADER_STATE] = { localModuleCache, vmContext };
41
+ }
42
+
43
+ function getRunLoaderState(store) {
44
+ return store?.[RUN_LOADER_STATE];
45
+ }
46
+
47
+ /**
48
+ * Late-bound facade for a Bruno global in the shared npm context.
49
+ * @param {string} key - The Bruno global's name
50
+ * @param {boolean} callable - Whether the current value is a function
51
+ */
52
+ function facadeFor(key, callable) {
53
+ const cacheKey = `${key}:${callable ? 'fn' : 'obj'}`;
54
+ if (facades.has(cacheKey)) {
55
+ return facades.get(cacheKey);
56
+ }
57
+ const current = () => activeScriptContext.getStore()?.[key];
58
+ const isMissing = (value) => value === undefined || value === null;
59
+ const target = callable ? () => {} : {};
60
+
61
+ // Cache wrappers so `bru.setVar === bru.setVar` remains true.
62
+ const methods = new Map();
63
+ const lateBoundMethod = (prop) => {
64
+ if (!methods.has(prop)) {
65
+ methods.set(prop, (...args) => {
66
+ const value = current();
67
+ const member = isMissing(value) ? undefined : value[prop];
68
+ if (typeof member !== 'function') {
69
+ throw new TypeError(`${key}.${String(prop)} is not available outside of a script execution`);
70
+ }
71
+ return Reflect.apply(member, value, args);
72
+ });
73
+ }
74
+ return methods.get(prop);
75
+ };
76
+ const facade = new Proxy(target, {
77
+ get: (_, prop) => {
78
+ const targetDescriptor = Reflect.getOwnPropertyDescriptor(target, prop);
79
+ if (targetDescriptor && !targetDescriptor.configurable && !targetDescriptor.writable) {
80
+ return targetDescriptor.value;
81
+ }
82
+ const value = current();
83
+ if (isMissing(value)) {
84
+ return undefined;
85
+ }
86
+ const member = value[prop];
87
+ return typeof member === 'function' ? lateBoundMethod(prop) : member;
88
+ },
89
+ set: (_, prop, newValue) => {
90
+ const targetDescriptor = Reflect.getOwnPropertyDescriptor(target, prop);
91
+ if (targetDescriptor && !targetDescriptor.configurable && !targetDescriptor.writable) {
92
+ return Object.is(targetDescriptor.value, newValue);
93
+ }
94
+ const value = current();
95
+ if (isMissing(value)) {
96
+ return false;
97
+ }
98
+ value[prop] = newValue;
99
+ return true;
100
+ },
101
+ has: (_, prop) => {
102
+ const targetDescriptor = Reflect.getOwnPropertyDescriptor(target, prop);
103
+ if (targetDescriptor && !targetDescriptor.configurable) {
104
+ return true;
105
+ }
106
+ if (!Object.isExtensible(target)) {
107
+ return false;
108
+ }
109
+ const value = current();
110
+ return !isMissing(value) && prop in Object(value);
111
+ },
112
+ ownKeys: () => {
113
+ // Once freeze/preventExtensions runs, only report keys on the inert target
114
+ // or Proxy throws and the poisoned facade breaks every later script.
115
+ if (!Object.isExtensible(target)) {
116
+ return Reflect.ownKeys(target);
117
+ }
118
+ const value = current();
119
+ const currentKeys = isMissing(value) ? [] : Reflect.ownKeys(Object(value));
120
+ return [...new Set([...Reflect.ownKeys(target), ...currentKeys])];
121
+ },
122
+ getOwnPropertyDescriptor: (_, prop) => {
123
+ const targetDescriptor = Reflect.getOwnPropertyDescriptor(target, prop);
124
+ if (targetDescriptor && !targetDescriptor.configurable) {
125
+ return targetDescriptor;
126
+ }
127
+ if (!Object.isExtensible(target)) {
128
+ return undefined;
129
+ }
130
+ const value = current();
131
+ const descriptor = isMissing(value) ? undefined : Object.getOwnPropertyDescriptor(Object(value), prop);
132
+ return descriptor ? { ...descriptor, configurable: true } : undefined;
133
+ },
134
+ getPrototypeOf: () => {
135
+ const value = current();
136
+ return isMissing(value) ? null : Object.getPrototypeOf(Object(value));
137
+ },
138
+ apply: (_, thisArg, args) => {
139
+ const value = current();
140
+ if (typeof value !== 'function') {
141
+ throw new TypeError(`${key} is not available outside of a script execution`);
142
+ }
143
+ return Reflect.apply(value, thisArg, args);
144
+ }
145
+ });
146
+ facades.set(cacheKey, facade);
147
+ return facade;
148
+ }
149
+
150
+ function defineDynamicGlobal(key) {
151
+ if (Object.prototype.hasOwnProperty.call(sharedNpmSandbox, key)) {
152
+ return;
153
+ }
154
+ Object.defineProperty(sharedNpmSandbox, key, {
155
+ enumerable: true,
156
+ configurable: true,
157
+ get: () => {
158
+ const evalStore = npmModuleEval.getStore();
159
+ if (evalStore) {
160
+ evalStore.touched.add(key);
161
+ }
162
+ const value = activeScriptContext.getStore()?.[key];
163
+ if (value === undefined || value === null) {
164
+ return undefined;
165
+ }
166
+ if (typeof value !== 'object' && typeof value !== 'function') {
167
+ return value;
168
+ }
169
+ // Custom script globals (arrays, plain data, callbacks, require) stay raw so
170
+ // Array.isArray / identity work. Bruno APIs need facades for late-binding.
171
+ if (!BRUNO_CONTEXT_KEYS.includes(key)) {
172
+ return value;
173
+ }
174
+ return facadeFor(key, typeof value === 'function');
175
+ }
176
+ });
177
+ }
178
+
179
+ function getSharedNpmContext() {
180
+ if (!sharedNpmContext) {
181
+ sharedNpmSandbox = Object.fromEntries(
182
+ safeGlobals
183
+ .filter((key) => global[key] !== undefined)
184
+ .map((key) => [key, global[key]])
185
+ );
186
+ mixinTypedArrays(sharedNpmSandbox);
187
+ sharedNpmContext = vm.createContext(sharedNpmSandbox);
188
+ sharedNpmSandbox.global = sharedNpmSandbox;
189
+ sharedNpmSandbox.globalThis = sharedNpmSandbox;
190
+ BRUNO_CONTEXT_KEYS.forEach(defineDynamicGlobal);
191
+ }
192
+ return sharedNpmContext;
193
+ }
194
+
195
+ /**
196
+ * Runs `fn` with `scriptContext` as the currently executing script context, so
197
+ * npm modules called from it (or from async work it starts) resolve `bru`,
198
+ * `req`, `res`, ... to it. Safe for interleaved executions; nested executions
199
+ * (`bru.runRequest`) see their own context.
200
+ * @param {Object} scriptContext - The script's vm global object
201
+ * @param {Function} fn - The execution, typically `() => script.runInContext(...)`
202
+ * @returns {*} Whatever `fn` returns
203
+ */
204
+ function runWithScriptContext(scriptContext, fn) {
205
+ getSharedNpmContext();
206
+ for (const key of Object.keys(scriptContext)) {
207
+ if (key !== 'global' && key !== 'globalThis') {
208
+ defineDynamicGlobal(key);
209
+ }
210
+ }
211
+ return activeScriptContext.run(scriptContext, fn);
212
+ }
7
213
 
8
214
  /**
9
215
  * Resolve a local module path, handling files and directories
@@ -66,7 +272,7 @@ function resolveLocalModulePath(fromDir, moduleName) {
66
272
  * @param {Object} options.isolatedContext - The VM isolated context created with vm.createContext()
67
273
  * @param {string} options.currentModuleDir - Current module directory for resolving relative paths
68
274
  * @param {Map} options.localModuleCache - Cache for loaded modules
69
- * @param {string[]} options.additionalContextRootsAbsolute - Additional allowed root paths
275
+ * @param {string[]} options.additionalContextRootsAbsolute - Allowed roots for local file imports
70
276
  * @returns {Function} Custom require function
71
277
  */
72
278
  function createCustomRequire({
@@ -74,7 +280,8 @@ function createCustomRequire({
74
280
  isolatedContext,
75
281
  currentModuleDir = collectionPath,
76
282
  localModuleCache = new Map(),
77
- additionalContextRootsAbsolute = []
283
+ additionalContextRootsAbsolute = [],
284
+ cacheModules = false
78
285
  }) {
79
286
  return (moduleName) => {
80
287
  const normalizedModuleName = moduleName.replace(/\\/g, '/');
@@ -87,7 +294,8 @@ function createCustomRequire({
87
294
  isolatedContext,
88
295
  localModuleCache,
89
296
  currentModuleDir,
90
- additionalContextRootsAbsolute
297
+ additionalContextRootsAbsolute,
298
+ cacheModules
91
299
  });
92
300
  }
93
301
 
@@ -100,7 +308,8 @@ function createCustomRequire({
100
308
  isolatedContext,
101
309
  localModuleCache,
102
310
  currentModuleDir,
103
- additionalContextRootsAbsolute
311
+ additionalContextRootsAbsolute,
312
+ cacheModules
104
313
  });
105
314
  }
106
315
 
@@ -115,8 +324,10 @@ function createCustomRequire({
115
324
  return loadNpmModule({
116
325
  moduleName,
117
326
  collectionPath,
327
+ currentModuleDir,
118
328
  isolatedContext,
119
- localModuleCache
329
+ localModuleCache,
330
+ cacheModules
120
331
  });
121
332
  };
122
333
  }
@@ -133,7 +344,8 @@ function loadLocalModule({
133
344
  isolatedContext,
134
345
  localModuleCache,
135
346
  currentModuleDir,
136
- additionalContextRootsAbsolute = []
347
+ additionalContextRootsAbsolute = [],
348
+ cacheModules = false
137
349
  }) {
138
350
  // Validate the raw module name doesn't try to escape allowed roots
139
351
  const preliminaryPath = path.resolve(currentModuleDir, moduleName);
@@ -181,7 +393,8 @@ function loadLocalModule({
181
393
  isolatedContext,
182
394
  currentModuleDir: moduleDir,
183
395
  localModuleCache,
184
- additionalContextRootsAbsolute
396
+ additionalContextRootsAbsolute,
397
+ cacheModules
185
398
  });
186
399
 
187
400
  try {
@@ -198,6 +411,38 @@ function loadLocalModule({
198
411
  }
199
412
  }
200
413
 
414
+ function recordRequireEdge(resolvedPath) {
415
+ const parent = npmModuleEval.getStore();
416
+ if (!parent?.resolvedPath) {
417
+ return;
418
+ }
419
+ if (!moduleRequireGraph.has(parent.resolvedPath)) {
420
+ moduleRequireGraph.set(parent.resolvedPath, new Set());
421
+ }
422
+ moduleRequireGraph.get(parent.resolvedPath).add(resolvedPath);
423
+ }
424
+
425
+ function evictContextBoundGraph(rootPath, rootModuleObj, localModuleCache) {
426
+ const stack = [rootPath];
427
+ const visited = new Set();
428
+ while (stack.length) {
429
+ const modulePath = stack.pop();
430
+ if (visited.has(modulePath)) {
431
+ continue;
432
+ }
433
+ visited.add(modulePath);
434
+ contextBoundModulePaths.add(modulePath);
435
+ const moduleObj = modulePath === rootPath ? rootModuleObj : sharedNpmModuleCache.get(modulePath);
436
+ sharedNpmModuleCache.delete(modulePath);
437
+ if (moduleObj) {
438
+ localModuleCache.set(modulePath, moduleObj);
439
+ }
440
+ for (const dep of moduleRequireGraph.get(modulePath) || []) {
441
+ stack.push(dep);
442
+ }
443
+ }
444
+ }
445
+
201
446
  /**
202
447
  * Executes a module in the VM context with caching and special file handling
203
448
  * @param {Object} options - Configuration options
@@ -207,22 +452,82 @@ function loadLocalModule({
207
452
  function executeModuleInVmContext({
208
453
  resolvedPath,
209
454
  moduleName,
210
- isolatedContext,
211
455
  collectionPath,
212
- localModuleCache
456
+ isolatedContext,
457
+ localModuleCache,
458
+ cacheModules = false
213
459
  }) {
214
- // Check cache - we cache moduleObj, return its exports
215
- if (localModuleCache.has(resolvedPath)) {
216
- return localModuleCache.get(resolvedPath).exports;
460
+ if (cacheModules) {
461
+ recordRequireEdge(resolvedPath);
462
+ }
463
+
464
+ if (!cacheModules) {
465
+ if (localModuleCache.has(resolvedPath)) {
466
+ return localModuleCache.get(resolvedPath).exports;
467
+ }
468
+ return evaluateNpmModule({
469
+ resolvedPath,
470
+ moduleName,
471
+ collectionPath,
472
+ isolatedContext,
473
+ localModuleCache,
474
+ useSharedContext: false,
475
+ cacheModules: false
476
+ });
477
+ }
478
+
479
+ const isContextBound = contextBoundModulePaths.has(resolvedPath);
480
+
481
+ if (isContextBound) {
482
+ if (localModuleCache?.has(resolvedPath)) {
483
+ return localModuleCache.get(resolvedPath).exports;
484
+ }
485
+ return evaluateNpmModule({
486
+ resolvedPath,
487
+ moduleName,
488
+ collectionPath,
489
+ isolatedContext,
490
+ localModuleCache,
491
+ useSharedContext: false,
492
+ cacheModules: true
493
+ });
217
494
  }
218
495
 
496
+ if (sharedNpmModuleCache.has(resolvedPath)) {
497
+ return sharedNpmModuleCache.get(resolvedPath).exports;
498
+ }
499
+
500
+ return evaluateNpmModule({
501
+ resolvedPath,
502
+ moduleName,
503
+ collectionPath,
504
+ isolatedContext,
505
+ localModuleCache,
506
+ useSharedContext: true,
507
+ cacheModules: true
508
+ });
509
+ }
510
+
511
+ function evaluateNpmModule({
512
+ resolvedPath,
513
+ moduleName,
514
+ collectionPath,
515
+ isolatedContext,
516
+ localModuleCache,
517
+ useSharedContext,
518
+ cacheModules = false
519
+ }) {
219
520
  // Native modules (.node files) - fall back to host require
220
521
  // Note: This bypasses VM isolation for native addons.
221
522
  // This is intentional - [`developer` mode] node-vm isolation need not be strict for native modules.
222
523
  if (resolvedPath.endsWith('.node')) {
223
524
  const result = require(resolvedPath);
224
- // Wrap in moduleObj format for consistent cache retrieval
225
- localModuleCache.set(resolvedPath, { exports: result });
525
+ const moduleObj = { exports: result };
526
+ if (useSharedContext) {
527
+ sharedNpmModuleCache.set(resolvedPath, moduleObj);
528
+ } else {
529
+ localModuleCache.set(resolvedPath, moduleObj);
530
+ }
226
531
  return result;
227
532
  }
228
533
 
@@ -230,37 +535,50 @@ function executeModuleInVmContext({
230
535
  if (resolvedPath.endsWith('.json')) {
231
536
  const jsonContent = fs.readFileSync(resolvedPath, 'utf8');
232
537
  const result = JSON.parse(jsonContent);
233
- // Wrap in moduleObj format for consistent cache retrieval
234
- localModuleCache.set(resolvedPath, { exports: result });
538
+ const moduleObj = { exports: result };
539
+ if (useSharedContext) {
540
+ sharedNpmModuleCache.set(resolvedPath, moduleObj);
541
+ } else {
542
+ localModuleCache.set(resolvedPath, moduleObj);
543
+ }
235
544
  return result;
236
545
  }
237
546
 
238
- // JavaScript files
239
547
  const moduleSource = fs.readFileSync(resolvedPath, 'utf8');
240
548
  const moduleDir = path.dirname(resolvedPath);
241
549
  const moduleObj = { exports: {} };
550
+ const moduleCache = useSharedContext ? sharedNpmModuleCache : localModuleCache;
242
551
 
243
- // Pre-populate cache with moduleObj BEFORE execution to handle circular dependencies
244
- // This allows re-entrant requires to get partial exports (Node.js behavior)
245
- // We cache moduleObj (not moduleObj.exports) so that module.exports reassignment works
246
- localModuleCache.set(resolvedPath, moduleObj);
552
+ moduleCache.set(resolvedPath, moduleObj);
247
553
 
248
554
  const moduleRequire = createNpmModuleRequire({
249
555
  collectionPath,
250
- isolatedContext,
251
556
  currentModuleDir: moduleDir,
252
- localModuleCache
557
+ isolatedContext,
558
+ localModuleCache,
559
+ cacheModules
253
560
  });
254
561
 
562
+ const vmContext = useSharedContext ? getSharedNpmContext() : isolatedContext;
563
+ const evalStore = { resolvedPath, touched: new Set() };
564
+
255
565
  try {
256
- // Wrap module code in a function that receives CJS parameters
257
566
  const wrappedCode = `(function(module, exports, require, __filename, __dirname) {\n${moduleSource}\n})`;
258
567
  const compiledScript = new vm.Script(wrappedCode, { filename: resolvedPath });
259
- const moduleFunction = compiledScript.runInContext(isolatedContext);
260
- moduleFunction(moduleObj, moduleObj.exports, moduleRequire, resolvedPath, moduleDir);
568
+ const moduleFunction = compiledScript.runInContext(vmContext);
569
+ const runModule = () => {
570
+ moduleFunction(moduleObj, moduleObj.exports, moduleRequire, resolvedPath, moduleDir);
571
+ };
572
+ if (cacheModules && useSharedContext) {
573
+ npmModuleEval.run(evalStore, runModule);
574
+ if (evalStore.touched.size > 0) {
575
+ evictContextBoundGraph(resolvedPath, moduleObj, localModuleCache);
576
+ }
577
+ } else {
578
+ runModule();
579
+ }
261
580
  } catch (error) {
262
- // Remove failed module from cache to allow retry
263
- localModuleCache.delete(resolvedPath);
581
+ moduleCache.delete(resolvedPath);
264
582
  const stack = error.stack || '';
265
583
  throw new Error(`Error loading module ${moduleName}: ${error.message}\nStack: ${stack}`);
266
584
  }
@@ -269,7 +587,13 @@ function executeModuleInVmContext({
269
587
  }
270
588
 
271
589
  /**
272
- * Loads an npm module into the vm context
590
+ * Loads an npm module into the vm context.
591
+ *
592
+ * Resolution order matches standard Node.js walk-up:
593
+ * 1. currentModuleDir/node_modules → walk up parent dirs
594
+ * 2. collectionPath/node_modules
595
+ * 3. Bruno's bundled node_modules (final fallback for chai/ajv/axios/etc.)
596
+ *
273
597
  * @param {Object} options - Configuration options
274
598
  * @returns {*} The exported content of the loaded module
275
599
  * @throws {Error} When module cannot be resolved or loaded
@@ -277,19 +601,23 @@ function executeModuleInVmContext({
277
601
  function loadNpmModule({
278
602
  moduleName,
279
603
  collectionPath,
604
+ currentModuleDir,
280
605
  isolatedContext,
281
- localModuleCache
606
+ localModuleCache,
607
+ cacheModules = false
282
608
  }) {
283
609
  let resolvedPath;
284
610
 
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) {
611
+ if (currentModuleDir) {
612
+ try {
613
+ const callerRequire = nodeModule.createRequire(path.join(currentModuleDir, 'package.json'));
614
+ resolvedPath = callerRequire.resolve(moduleName);
615
+ } catch {
616
+ // Not found via walk-up, continue to fallbacks
617
+ }
618
+ }
619
+
620
+ if (!resolvedPath && collectionPath) {
293
621
  try {
294
622
  const collectionRequire = nodeModule.createRequire(path.join(collectionPath, 'package.json'));
295
623
  resolvedPath = collectionRequire.resolve(moduleName);
@@ -298,7 +626,7 @@ function loadNpmModule({
298
626
  }
299
627
  }
300
628
 
301
- // Fall back to Bruno's node_modules
629
+ // Fall back to Bruno's bundled node_modules
302
630
  if (!resolvedPath) {
303
631
  try {
304
632
  resolvedPath = require.resolve(moduleName, { paths: module.paths });
@@ -313,36 +641,59 @@ function loadNpmModule({
313
641
  return executeModuleInVmContext({
314
642
  resolvedPath,
315
643
  moduleName,
316
- isolatedContext,
317
644
  collectionPath,
318
- localModuleCache
645
+ isolatedContext,
646
+ localModuleCache,
647
+ cacheModules
319
648
  });
320
649
  }
321
650
 
322
651
  /**
323
- * Creates require function for npm module dependencies
652
+ * Creates the require function handed to a loaded npm module. Resolution is
653
+ * plain Node.js walk-up from the module's own directory — internal relative
654
+ * requires, sibling packages, and npm-linked / file: dependencies all resolve
655
+ * the way native `require` would from that location.
656
+ *
324
657
  * @param {Object} options - Configuration options
325
658
  * @returns {Function} Custom require function for npm module dependencies
326
659
  */
660
+ function markParentContextBoundIfNeeded(resolvedPath) {
661
+ const evalStore = npmModuleEval.getStore();
662
+ if (evalStore && contextBoundModulePaths.has(resolvedPath)) {
663
+ evalStore.touched.add('*');
664
+ }
665
+ }
666
+
327
667
  function createNpmModuleRequire({
328
668
  collectionPath,
329
- isolatedContext,
330
669
  currentModuleDir,
331
- localModuleCache
670
+ isolatedContext,
671
+ localModuleCache,
672
+ cacheModules = false
332
673
  }) {
333
674
  const moduleRequire = nodeModule.createRequire(path.join(currentModuleDir, 'index.js'));
334
675
 
335
676
  return (moduleName) => {
677
+ // Shared parents keep this require for the process lifetime; always prefer the
678
+ // active script run's cache/context so lazy context-bound leaves re-eval.
679
+ const store = cacheModules ? activeScriptContext.getStore() : null;
680
+ const runState = getRunLoaderState(store);
681
+ const runLocalCache = runState?.localModuleCache ?? localModuleCache;
682
+ const runContext = runState?.vmContext ?? isolatedContext;
683
+
336
684
  // Handle relative imports within npm module
337
685
  if (moduleName.startsWith('./') || moduleName.startsWith('../')) {
338
686
  const resolvedPath = moduleRequire.resolve(moduleName);
339
- return executeModuleInVmContext({
687
+ const exports = executeModuleInVmContext({
340
688
  resolvedPath,
341
689
  moduleName,
342
- isolatedContext,
343
690
  collectionPath,
344
- localModuleCache
691
+ isolatedContext: runContext,
692
+ localModuleCache: runLocalCache,
693
+ cacheModules
345
694
  });
695
+ markParentContextBoundIfNeeded(resolvedPath);
696
+ return exports;
346
697
  }
347
698
 
348
699
  // Handle builtins
@@ -354,16 +705,30 @@ function createNpmModuleRequire({
354
705
 
355
706
  // Handle npm dependencies - resolve from current module's directory
356
707
  const resolvedPath = moduleRequire.resolve(moduleName);
357
- return executeModuleInVmContext({
708
+ const exports = executeModuleInVmContext({
358
709
  resolvedPath,
359
710
  moduleName,
360
- isolatedContext,
361
711
  collectionPath,
362
- localModuleCache
712
+ isolatedContext: runContext,
713
+ localModuleCache: runLocalCache,
714
+ cacheModules
363
715
  });
716
+ markParentContextBoundIfNeeded(resolvedPath);
717
+ return exports;
364
718
  };
365
719
  }
366
720
 
367
721
  module.exports = {
368
- createCustomRequire
722
+ createCustomRequire,
723
+ runWithScriptContext,
724
+ getSharedNpmContext,
725
+ attachRunLoaderState,
726
+ __resetNpmModuleStateForTests: () => {
727
+ sharedNpmModuleCache.clear();
728
+ contextBoundModulePaths.clear();
729
+ moduleRequireGraph.clear();
730
+ facades.clear();
731
+ sharedNpmSandbox = null;
732
+ sharedNpmContext = null;
733
+ }
369
734
  };