feat: default developer mode to nodevm and remove vm2 (#6187)

This commit is contained in:
lohit
2025-11-29 00:04:55 +05:30
committed by GitHub
parent 8c06a229e9
commit 4f8d2c0c67
24 changed files with 249 additions and 405 deletions

View File

@@ -241,7 +241,7 @@ const evaluateRhsOperand = (rhsOperand, operator, context, runtime) => {
class AssertRuntime {
constructor(props) {
this.runtime = props?.runtime || 'vm2';
this.runtime = props?.runtime || 'quickjs';
}
runAssertions(assertions, request, response, envVariables, runtimeVariables, processEnvVars) {

View File

@@ -1,45 +1,15 @@
const { NodeVM } = require('@usebruno/vm2');
const path = require('path');
const http = require('http');
const https = require('https');
const stream = require('stream');
const util = require('util');
const zlib = require('zlib');
const url = require('url');
const punycode = require('punycode');
const fs = require('fs');
const { get } = require('lodash');
const chai = require('chai');
const Bru = require('../bru');
const BrunoRequest = require('../bruno-request');
const BrunoResponse = require('../bruno-response');
const { cleanJson } = require('../utils');
const { createBruTestResultMethods } = require('../utils/results');
const { runScriptInNodeVm } = require('../sandbox/node-vm');
// Inbuilt Library Support
const ajv = require('ajv');
const addFormats = require('ajv-formats');
const atob = require('atob');
const btoa = require('btoa');
const lodash = require('lodash');
const moment = require('moment');
const uuid = require('uuid');
const nanoid = require('nanoid');
const axios = require('axios');
const fetch = require('node-fetch');
const chai = require('chai');
const CryptoJS = require('crypto-js');
const NodeVault = require('node-vault');
const xml2js = require('xml2js');
const cheerio = require('cheerio');
const tv4 = require('tv4');
const jsonwebtoken = require('jsonwebtoken');
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
const { mixinTypedArrays } = require('../sandbox/mixins/typed-arrays');
class ScriptRuntime {
constructor(props) {
this.runtime = props?.runtime || 'vm2';
this.runtime = props?.runtime || 'quickjs';
}
// This approach is getting out of hand
@@ -65,24 +35,6 @@ class ScriptRuntime {
const assertionResults = request?.assertionResults || [];
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables);
const req = new BrunoRequest(request);
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
const additionalContextRootsAbsolute = lodash
.chain(additionalContextRoots)
.map((acr) => (acr.startsWith('/') ? acr : path.join(collectionPath, acr)))
.value();
const whitelistedModules = {};
for (let module of moduleWhitelist) {
try {
whitelistedModules[module] = require(module);
} catch (e) {
// Ignore
console.warn(e);
}
}
// extend bru with result getter methods
const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai);
@@ -96,10 +48,6 @@ class ScriptRuntime {
__brunoTestResults: __brunoTestResults
};
if (this.runtime === 'vm2') {
mixinTypedArrays(context);
}
if (onConsoleLog && typeof onConsoleLog === 'function') {
const customLogger = (type) => {
return (...args) => {
@@ -140,69 +88,12 @@ class ScriptRuntime {
};
}
if (this.runtime === 'quickjs') {
await executeQuickJsVmAsync({
script: script,
context: context,
collectionPath
});
return {
request,
envVariables: cleanJson(envVariables),
runtimeVariables: cleanJson(runtimeVariables),
persistentEnvVariables: bru.persistentEnvVariables,
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
results: cleanJson(__brunoTestResults.getResults()),
nextRequestName: bru.nextRequest,
skipRequest: bru.skipRequest,
stopExecution: bru.stopExecution
};
}
// default runtime is vm2
const vm = new NodeVM({
sandbox: context,
require: {
context: 'sandbox',
builtin: [ "*" ],
external: true,
root: [collectionPath, ...additionalContextRootsAbsolute],
mock: {
// node libs
path,
stream,
util,
url,
http,
https,
punycode,
zlib,
// 3rd party libs
ajv,
'ajv-formats': addFormats,
atob,
btoa,
lodash,
moment,
uuid,
nanoid,
axios,
chai,
'node-fetch': fetch,
'crypto-js': CryptoJS,
xml2js: xml2js,
jsonwebtoken,
cheerio,
tv4,
...whitelistedModules,
fs: allowScriptFilesystemAccess ? fs : undefined,
'node-vault': NodeVault
}
}
// default runtime is `quickjs`
await executeQuickJsVmAsync({
script: script,
context: context,
collectionPath
});
const asyncVM = vm.run(`module.exports = async () => { ${script} }`, path.join(collectionPath, 'vm.js'));
await asyncVM();
return {
request,
@@ -240,24 +131,6 @@ class ScriptRuntime {
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables);
const req = new BrunoRequest(request);
const res = new BrunoResponse(response);
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
const additionalContextRootsAbsolute = lodash
.chain(additionalContextRoots)
.map((acr) => (acr.startsWith('/') ? acr : path.join(collectionPath, acr)))
.value();
const whitelistedModules = {};
for (let module of moduleWhitelist) {
try {
whitelistedModules[module] = require(module);
} catch (e) {
// Ignore
console.warn(e);
}
}
// extend bru with result getter methods
const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai);
@@ -272,10 +145,6 @@ class ScriptRuntime {
__brunoTestResults: __brunoTestResults
};
if (this.runtime === 'vm2') {
mixinTypedArrays(context);
}
if (onConsoleLog && typeof onConsoleLog === 'function') {
const customLogger = (type) => {
return (...args) => {
@@ -316,70 +185,13 @@ class ScriptRuntime {
};
}
if (this.runtime === 'quickjs') {
await executeQuickJsVmAsync({
script: script,
context: context,
collectionPath
});
return {
response,
envVariables: cleanJson(envVariables),
persistentEnvVariables: cleanJson(bru.persistentEnvVariables),
runtimeVariables: cleanJson(runtimeVariables),
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
results: cleanJson(__brunoTestResults.getResults()),
nextRequestName: bru.nextRequest,
skipRequest: bru.skipRequest,
stopExecution: bru.stopExecution
};
}
// default runtime is vm2
const vm = new NodeVM({
sandbox: context,
require: {
context: 'sandbox',
builtin: [ "*" ],
external: true,
root: [collectionPath, ...additionalContextRootsAbsolute],
mock: {
// node libs
path,
stream,
util,
url,
http,
https,
punycode,
zlib,
// 3rd party libs
ajv,
'ajv-formats': addFormats,
atob,
btoa,
lodash,
moment,
uuid,
nanoid,
axios,
'node-fetch': fetch,
'crypto-js': CryptoJS,
'xml2js': xml2js,
jsonwebtoken,
cheerio,
tv4,
...whitelistedModules,
fs: allowScriptFilesystemAccess ? fs : undefined,
'node-vault': NodeVault
}
}
// default runtime is `quickjs`
await executeQuickJsVmAsync({
script: script,
context: context,
collectionPath
});
const asyncVM = vm.run(`module.exports = async () => { ${script} }`, path.join(collectionPath, 'vm.js'));
await asyncVM();
return {
response,
envVariables: cleanJson(envVariables),

View File

@@ -1,47 +1,16 @@
const { NodeVM } = require('@usebruno/vm2');
const { runScriptInNodeVm } = require('../sandbox/node-vm');
const chai = require('chai');
const path = require('path');
const http = require('http');
const https = require('https');
const stream = require('stream');
const util = require('util');
const zlib = require('zlib');
const url = require('url');
const punycode = require('punycode');
const fs = require('fs');
const { get } = require('lodash');
const Bru = require('../bru');
const BrunoRequest = require('../bruno-request');
const BrunoResponse = require('../bruno-response');
const Test = require('../test');
const TestResults = require('../test-results');
const { cleanJson } = require('../utils');
const { createBruTestResultMethods } = require('../utils/results');
// Inbuilt Library Support
const ajv = require('ajv');
const addFormats = require('ajv-formats');
const atob = require('atob');
const btoa = require('btoa');
const lodash = require('lodash');
const moment = require('moment');
const uuid = require('uuid');
const nanoid = require('nanoid');
const axios = require('axios');
const fetch = require('node-fetch');
const CryptoJS = require('crypto-js');
const NodeVault = require('node-vault');
const xml2js = require('xml2js');
const cheerio = require('cheerio');
const tv4 = require('tv4');
const { runScriptInNodeVm } = require('../sandbox/node-vm');
const jsonwebtoken = require('jsonwebtoken');
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
const { mixinTypedArrays } = require('../sandbox/mixins/typed-arrays');
class TestRuntime {
constructor(props) {
this.runtime = props?.runtime || 'vm2';
this.runtime = props?.runtime || 'quickjs';
}
async runTests(
@@ -66,24 +35,6 @@ class TestRuntime {
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, {}, collectionName, promptVariables);
const req = new BrunoRequest(request);
const res = new BrunoResponse(response);
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
const additionalContextRootsAbsolute = lodash
.chain(additionalContextRoots)
.map((acr) => (acr.startsWith('/') ? acr : path.join(collectionPath, acr)))
.value();
const whitelistedModules = {};
for (let module of moduleWhitelist) {
try {
whitelistedModules[module] = require(module);
} catch (e) {
// Ignore
console.warn(e);
}
}
// extend bru with result getter methods
const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai);
@@ -110,10 +61,6 @@ class TestRuntime {
jwt: jsonwebtoken
};
if (this.runtime === 'vm2') {
mixinTypedArrays(context);
}
if (onConsoleLog && typeof onConsoleLog === 'function') {
const customLogger = (type) => {
return (...args) => {
@@ -136,13 +83,7 @@ class TestRuntime {
let scriptError = null;
try {
if (this.runtime === 'quickjs') {
await executeQuickJsVmAsync({
script: testsFile,
context: context,
collectionPath
});
} else if (this.runtime === 'nodevm') {
if (this.runtime === 'nodevm') {
await runScriptInNodeVm({
script: testsFile,
context,
@@ -150,49 +91,12 @@ class TestRuntime {
scriptingConfig
});
} else {
// default runtime is vm2
const vm = new NodeVM({
sandbox: context,
require: {
context: 'sandbox',
external: true,
builtin: ['*'],
root: [collectionPath, ...additionalContextRootsAbsolute],
mock: {
// node libs
path,
stream,
util,
url,
http,
https,
punycode,
zlib,
// 3rd party libs
ajv,
'ajv-formats': addFormats,
btoa,
atob,
lodash,
moment,
uuid,
nanoid,
axios,
chai,
'node-fetch': fetch,
'crypto-js': CryptoJS,
'xml2js': xml2js,
cheerio,
tv4,
'jsonwebtoken': jsonwebtoken,
...whitelistedModules,
fs: allowScriptFilesystemAccess ? fs : undefined,
'node-vault': NodeVault
}
}
// default runtime is `quickjs`
await executeQuickJsVmAsync({
script: testsFile,
context: context,
collectionPath
});
const asyncVM = vm.run(`module.exports = async () => { ${testsFile}}`, path.join(collectionPath, 'vm.js'));
await asyncVM();
}
} catch (error) {
scriptError = error;

View File

@@ -20,7 +20,7 @@ const evaluateJsExpressionBasedOnRuntime = (expr, context, runtime, mode) => {
class VarsRuntime {
constructor(props) {
this.runtime = props?.runtime || 'vm2';
this.runtime = props?.runtime || 'quickjs';
this.mode = props?.mode || 'developer';
}

View File

@@ -3,7 +3,6 @@ const fs = require('node:fs');
const path = require('node:path');
const { get } = require('lodash');
const lodash = require('lodash');
const { cleanJson } = require('../../utils');
const { mixinTypedArrays } = require('../mixins/typed-arrays');
class ScriptError extends Error {
@@ -37,6 +36,8 @@ async function runScriptInNodeVm({
}
try {
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
// Create script context with all necessary variables
const scriptContext = {
// Bruno context
@@ -77,7 +78,8 @@ async function runScriptInNodeVm({
collectionPath,
scriptContext,
currentModuleDir: collectionPath,
localModuleCache
localModuleCache,
allowScriptFilesystemAccess
});
// Execute the script in an isolated VM context
@@ -105,6 +107,7 @@ async function runScriptInNodeVm({
* @param {Object} options.scriptContext - Script execution context
* @param {string} options.currentModuleDir - Current module directory for relative imports
* @param {Map} options.localModuleCache - Cache for loaded local modules
* @param {boolean} options.allowScriptFilesystemAccess - Whether to allow fs module access
* @returns {Function} Custom require function
*/
function createCustomRequire({
@@ -112,7 +115,8 @@ function createCustomRequire({
collectionPath,
scriptContext,
currentModuleDir = collectionPath,
localModuleCache = new Map()
localModuleCache = new Map(),
allowScriptFilesystemAccess = false
}) {
const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
const additionalContextRootsAbsolute = lodash
@@ -127,17 +131,42 @@ function createCustomRequire({
return loadLocalModule({ moduleName, collectionPath, scriptContext, localModuleCache, currentModuleDir });
}
// Helper function to check if a module is the fs module or a submodule
const isFsModule = (module) => {
if (!module) return false;
const fsModule = require('fs');
// Check if it's the fs module itself
if (module === fsModule) return true;
// Check if it's fs/promises submodule
if (module === fsModule.promises) return true;
// Check if it's fs/promises by comparing with require('fs/promises')
try {
if (module === require('fs/promises')) return true;
} catch {
// fs/promises might not be available in all Node versions
}
return false;
};
// First try to require as a native/npm module
try {
return require(moduleName);
} catch {
// If that fails, try to resolve from additionalContextRoots
try {
const modulePath = require.resolve(moduleName, { paths: additionalContextRootsAbsolute });
return require(modulePath);
} catch (error) {
throw new Error(`Could not resolve module "${moduleName}": ${error.message}\n\nThis most likely means you did not install the module under "additionalContextRoots" using a package manager like npm.\n\nThese are your current "additionalContextRoots":\n${additionalContextRootsAbsolute.map(root => ` - ${root}`).join('\n') || ' - No "additionalContextRoots" defined'}`);
const requiredModulePath = require.resolve(moduleName, { paths: [...additionalContextRootsAbsolute, ...module.paths] });
const requiredModule = require(requiredModulePath);
// Block filesystem module access if filesystem access is not allowed
if (!allowScriptFilesystemAccess && isFsModule(requiredModule)) {
throw new Error('Filesystem access is not allowed. Enable "filesystemAccess.allow" in scripting config to use the fs module.');
}
return requiredModule;
} catch (requireError) {
// Re-throw if it's our filesystem access error
if (requireError.message && requireError.message.includes('Enable "filesystemAccess.allow"')) {
throw requireError;
}
// If that fails, try to resolve from additionalContextRoots
throw new Error(`Could not resolve module "${moduleName}": ${requireError.message}\n\nThis most likely means you did not install the module under the collection or the "additionalContextRoots" using a package manager like npm.\n\nThese are your current "additionalContextRoots":\n${additionalContextRootsAbsolute.map((root) => ` - ${root}`).join('\n') || ' - No "additionalContextRoots" defined'}`);
}
};
}
@@ -202,11 +231,12 @@ function loadLocalModule({
__dirname: moduleDir,
// Create a custom require function for this module that resolves relative to its directory
require: createCustomRequire({
scriptingConfig: scriptContext.scriptingConfig || {},
collectionPath,
scriptContext,
currentModuleDir: moduleDir,
localModuleCache
scriptingConfig: scriptContext.scriptingConfig || {},
collectionPath,
scriptContext,
currentModuleDir: moduleDir,
localModuleCache,
allowScriptFilesystemAccess: get(scriptContext.scriptingConfig, 'filesystemAccess.allow', false)
})
};

View File

@@ -128,7 +128,7 @@ const createResponseParser = (response = {}) => {
};
/**
* Objects that are created inside vm2 execution context result in an serialization error when sent to the renderer process
* Objects that are created inside developer mode execution context result in an serialization error when sent to the renderer process
* Error sending from webFrameMain: Error: Failed to serialize arguments
* at s.send (node:electron/js2c/browser_init:169:631)
* at g.send (node:electron/js2c/browser_init:165:2156)