@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.
- package/package.json +6 -6
- package/src/bru.js +29 -28
- package/src/bruno-request.js +4 -0
- package/src/bruno-response.js +4 -1
- package/src/grpc/bruno-grpc-request.js +52 -0
- package/src/grpc/bruno-grpc-response.js +43 -0
- package/src/grpc/grpc-message-list.js +74 -0
- package/src/grpc/grpc-message.js +20 -0
- package/src/grpc/grpc-metadata-list.js +214 -0
- package/src/grpc/grpc-metadata.js +21 -0
- package/src/grpc/grpc-script-runtime.js +302 -0
- package/src/index.js +2 -0
- package/src/interpolate-string.js +2 -1
- package/src/runtime/assert-runtime.js +1 -0
- package/src/runtime/script-runtime.js +20 -4
- package/src/runtime/test-runtime.js +15 -8
- package/src/sandbox/bundle-browser-rollup.js +32 -32
- package/src/sandbox/node-vm/cjs-loader.js +416 -51
- package/src/sandbox/node-vm/index.js +96 -65
- package/src/sandbox/node-vm/index.spec.js +896 -0
- package/src/sandbox/quickjs/index.js +101 -17
- package/src/sandbox/quickjs/shims/bru.js +54 -54
- package/src/sandbox/quickjs/shims/bruno-grpc.js +29 -0
- package/src/sandbox/quickjs/shims/grpc/bruno-grpc-request.js +47 -0
- package/src/sandbox/quickjs/shims/grpc/bruno-grpc-response.js +40 -0
- package/src/sandbox/quickjs/shims/grpc/grpc-message-list.js +29 -0
- package/src/sandbox/quickjs/shims/grpc/grpc-metadata-list.js +31 -0
- package/src/sandbox/quickjs/utils/index.js +42 -23
- package/src/utils/error-formatter.js +13 -9
- package/src/utils/error-formatter.spec.js +97 -2
- package/src/utils/results.js +30 -3
|
@@ -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 -
|
|
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
|
-
|
|
456
|
+
isolatedContext,
|
|
457
|
+
localModuleCache,
|
|
458
|
+
cacheModules = false
|
|
213
459
|
}) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
645
|
+
isolatedContext,
|
|
646
|
+
localModuleCache,
|
|
647
|
+
cacheModules
|
|
319
648
|
});
|
|
320
649
|
}
|
|
321
650
|
|
|
322
651
|
/**
|
|
323
|
-
* Creates require function
|
|
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
|
-
|
|
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
|
-
|
|
687
|
+
const exports = executeModuleInVmContext({
|
|
340
688
|
resolvedPath,
|
|
341
689
|
moduleName,
|
|
342
|
-
isolatedContext,
|
|
343
690
|
collectionPath,
|
|
344
|
-
|
|
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
|
-
|
|
708
|
+
const exports = executeModuleInVmContext({
|
|
358
709
|
resolvedPath,
|
|
359
710
|
moduleName,
|
|
360
|
-
isolatedContext,
|
|
361
711
|
collectionPath,
|
|
362
|
-
|
|
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
|
};
|