@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.
@@ -4,7 +4,7 @@ const { get } = require('lodash');
4
4
  const lodash = require('lodash');
5
5
  const { wrapConsoleWithSerializers } = require('./console');
6
6
  const { ScriptError, resolveVmFilename } = require('./utils');
7
- const { createCustomRequire } = require('./cjs-loader');
7
+ const { createCustomRequire, runWithScriptContext, getSharedNpmContext, attachRunLoaderState } = require('./cjs-loader');
8
8
  const { safeGlobals } = require('./constants');
9
9
  const { mixinTypedArrays } = require('../mixins/typed-arrays');
10
10
  const { wrapScriptInClosure, SANDBOX } = require('../../utils/sandbox');
@@ -44,82 +44,113 @@ async function runScriptInNodeVm({
44
44
 
45
45
  // Build the script context with Bruno objects and globals
46
46
  const scriptContext = buildScriptContext(context, scriptingConfig);
47
-
48
- // Create truly isolated context - scriptContext becomes the global object
49
- // Scripts can ONLY access what's explicitly in scriptContext
50
- const isolatedContext = vm.createContext(scriptContext);
51
-
52
- // Add global/globalThis pointing to the isolated context (not host global)
53
- // This allows libraries that reference 'global' to work while maintaining isolation
54
- scriptContext.global = scriptContext;
55
- scriptContext.globalThis = scriptContext;
56
-
57
- // Create module cache for CJS modules
58
47
  const localModuleCache = new Map();
48
+ const cacheModules = get(scriptingConfig, 'cacheModules', false) === true;
49
+
50
+ // cacheModules: one shared VM realm for scripts + npm modules so instanceof
51
+ // matches. Per-run bru/req/res/require resolve via ALS facades on that realm.
52
+ // Otherwise: fresh isolated context per script (default).
53
+ let vmContext;
54
+ if (cacheModules) {
55
+ vmContext = getSharedNpmContext();
56
+ } else {
57
+ vmContext = vm.createContext(scriptContext);
58
+ scriptContext.global = scriptContext;
59
+ scriptContext.globalThis = scriptContext;
60
+ }
59
61
 
60
- // Add require() function for CJS module loading
61
62
  scriptContext.require = createCustomRequire({
62
63
  collectionPath,
63
- isolatedContext,
64
+ isolatedContext: vmContext,
64
65
  currentModuleDir: collectionPath,
65
66
  localModuleCache,
66
- additionalContextRootsAbsolute
67
+ additionalContextRootsAbsolute,
68
+ cacheModules
67
69
  });
70
+ // Stashed for shared npm requires that outlive this run's createCustomRequire closure.
71
+ // Symbol key so runWithScriptContext does not publish these as sandbox globals.
72
+ attachRunLoaderState(scriptContext, { localModuleCache, vmContext });
73
+
74
+ // cacheModules: onFail runs after the script's ALS store exits, so re-bind
75
+ // registered callbacks to this run's context (bru/req/... facades).
76
+ let restoreOnFail;
77
+ if (cacheModules && typeof scriptContext.req?.onFail === 'function') {
78
+ const req = scriptContext.req;
79
+ const originalOnFail = req.onFail;
80
+ req.onFail = (callback) => {
81
+ if (typeof callback !== 'function') {
82
+ return originalOnFail.call(req, callback);
83
+ }
84
+ return originalOnFail.call(req, (error) => runWithScriptContext(scriptContext, () => callback(error)));
85
+ };
86
+ restoreOnFail = () => {
87
+ req.onFail = originalOnFail;
88
+ };
89
+ }
68
90
 
69
- const vmFilename = resolveVmFilename(scriptPath, collectionPath);
70
-
71
- // Execute the script in the isolated context
72
- const wrappedScript = wrapScriptInClosure(script, SANDBOX.NODEVM);
73
- let compiledScript;
74
91
  try {
75
- compiledScript = new vm.Script(wrappedScript, {
76
- filename: vmFilename
77
- });
78
- } catch (error) {
79
- // V8 puts "filename:line" as the first line of syntax error stacks.
80
- // Parse it so the error formatter can map to the correct source location.
81
- const firstLine = error.stack?.split('\n')[0];
82
- const match = firstLine?.match(/^(.+):(\d+)$/);
83
- if (match && match[1] === vmFilename) {
84
- error.__callSites = [{
85
- filePath: vmFilename,
86
- line: parseInt(match[2], 10),
87
- column: null,
88
- functionName: null
89
- }];
92
+ const vmFilename = resolveVmFilename(scriptPath, collectionPath);
93
+
94
+ // Execute the script in the isolated context
95
+ const wrappedScript = wrapScriptInClosure(script, SANDBOX.NODEVM);
96
+ let compiledScript;
97
+ try {
98
+ compiledScript = new vm.Script(wrappedScript, {
99
+ filename: vmFilename
100
+ });
101
+ } catch (error) {
102
+ // V8 puts "filename:line" as the first line of syntax error stacks.
103
+ // Parse it so the error formatter can map to the correct source location.
104
+ const firstLine = error.stack?.split('\n')[0];
105
+ const match = firstLine?.match(/^(.+):(\d+)$/);
106
+ if (match && match[1] === vmFilename) {
107
+ error.__callSites = [{
108
+ filePath: vmFilename,
109
+ line: parseInt(match[2], 10),
110
+ column: null,
111
+ functionName: null
112
+ }];
113
+ }
114
+ throw error;
90
115
  }
91
- throw error;
92
- }
93
-
94
- // Capture structured call sites for error-formatter line mapping
95
- const originalPrepareStackTrace = Error.prepareStackTrace;
96
- Error.prepareStackTrace = (error, callSites) => {
97
- error.__callSites = callSites
98
- .filter((site) => site.getFileName() === vmFilename)
99
- .map((site) => ({
100
- filePath: site.getFileName(),
101
- line: site.getLineNumber(),
102
- column: site.getColumnNumber(),
103
- functionName: site.getFunctionName() || null
104
- }));
105
-
106
- return error.toString() + '\n' + callSites
107
- .map((site) => ` at ${site}`)
108
- .join('\n');
109
- };
110
116
 
111
- try {
112
- await compiledScript.runInContext(isolatedContext, {
113
- displayErrors: true
114
- });
115
- } catch (error) {
116
- // V8 invokes prepareStackTrace lazily on first .stack access.
117
- // Reading .stack here so custom handler runs and populates error.__callSites
118
- // (used later by the error formatter to map stack frames to the .bru/.yml script)
119
- void error.stack;
120
- throw error;
117
+ // Capture structured call sites for error-formatter line mapping
118
+ const originalPrepareStackTrace = Error.prepareStackTrace;
119
+ Error.prepareStackTrace = (error, callSites) => {
120
+ error.__callSites = callSites
121
+ .filter((site) => site.getFileName() === vmFilename)
122
+ .map((site) => ({
123
+ filePath: site.getFileName(),
124
+ line: site.getLineNumber(),
125
+ column: site.getColumnNumber(),
126
+ functionName: site.getFunctionName() || null
127
+ }));
128
+
129
+ return error.toString() + '\n' + callSites
130
+ .map((site) => ` at ${site}`)
131
+ .join('\n');
132
+ };
133
+
134
+ try {
135
+ const runScript = () => compiledScript.runInContext(vmContext, {
136
+ displayErrors: true
137
+ });
138
+ if (cacheModules) {
139
+ await runWithScriptContext(scriptContext, runScript);
140
+ } else {
141
+ await runScript();
142
+ }
143
+ } catch (error) {
144
+ // V8 invokes prepareStackTrace lazily on first .stack access.
145
+ // Reading .stack here so custom handler runs and populates error.__callSites
146
+ // (used later by the error formatter to map stack frames to the .bru/.yml script)
147
+ void error.stack;
148
+ throw error;
149
+ } finally {
150
+ Error.prepareStackTrace = originalPrepareStackTrace;
151
+ }
121
152
  } finally {
122
- Error.prepareStackTrace = originalPrepareStackTrace;
153
+ restoreOnFail?.();
123
154
  }
124
155
  } catch (error) {
125
156
  throw new ScriptError(error, script);