diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js index 8ab3ddc03..1c7b5aeb9 100644 --- a/packages/bruno-js/src/runtime/script-runtime.js +++ b/packages/bruno-js/src/runtime/script-runtime.js @@ -35,6 +35,7 @@ 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) { @@ -94,6 +95,10 @@ class ScriptRuntime { __brunoTestResults: __brunoTestResults }; + if (this.runtime === 'vm2') { + mixinTypedArrays(context); + } + if (onConsoleLog && typeof onConsoleLog === 'function') { const customLogger = (type) => { return (...args) => { @@ -265,6 +270,10 @@ class ScriptRuntime { __brunoTestResults: __brunoTestResults }; + if (this.runtime === 'vm2') { + mixinTypedArrays(context); + } + if (onConsoleLog && typeof onConsoleLog === 'function') { const customLogger = (type) => { return (...args) => { diff --git a/packages/bruno-js/src/runtime/test-runtime.js b/packages/bruno-js/src/runtime/test-runtime.js index e8ee51f76..6f42d1b5b 100644 --- a/packages/bruno-js/src/runtime/test-runtime.js +++ b/packages/bruno-js/src/runtime/test-runtime.js @@ -37,6 +37,7 @@ 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 TestRuntime { constructor(props) { @@ -108,6 +109,10 @@ class TestRuntime { jwt: jsonwebtoken }; + if (this.runtime === 'vm2') { + mixinTypedArrays(context); + } + if (onConsoleLog && typeof onConsoleLog === 'function') { const customLogger = (type) => { return (...args) => { diff --git a/packages/bruno-js/src/sandbox/mixins/typed-arrays.js b/packages/bruno-js/src/sandbox/mixins/typed-arrays.js new file mode 100644 index 000000000..53ca01ddf --- /dev/null +++ b/packages/bruno-js/src/sandbox/mixins/typed-arrays.js @@ -0,0 +1,15 @@ +exports.mixinTypedArrays = (obj) => { + Object.assign(obj, { + Int8Array: Int8Array, + Uint8Array: Uint8Array, + Uint8ClampedArray: Uint8ClampedArray, + Int16Array: Int16Array, + Uint16Array: Uint16Array, + Int32Array: Int32Array, + Uint32Array: Uint32Array, + Float32Array: Float32Array, + Float64Array: Float64Array, + BigInt64Array: BigInt64Array, + BigUint64Array: BigUint64Array + }); +}; diff --git a/packages/bruno-js/src/sandbox/node-vm/index.js b/packages/bruno-js/src/sandbox/node-vm/index.js index 82dd71702..990474ac6 100644 --- a/packages/bruno-js/src/sandbox/node-vm/index.js +++ b/packages/bruno-js/src/sandbox/node-vm/index.js @@ -4,6 +4,7 @@ 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 { constructor(error, script) { @@ -60,6 +61,8 @@ async function runScriptInNodeVm({ clearImmediate: global.clearImmediate }; + mixinTypedArrays(scriptContext); + // Create shared cache for local modules const localModuleCache = new Map(); diff --git a/packages/bruno-js/src/utils.js b/packages/bruno-js/src/utils.js index ca26d6f4d..e5fdddd33 100644 --- a/packages/bruno-js/src/utils.js +++ b/packages/bruno-js/src/utils.js @@ -136,10 +136,58 @@ const createResponseParser = (response = {}) => { * Remove the cleanJson fix and execute the below post response script * bru.setVar("a", {b:3}); * Todo: Find a better fix + * + * serializes typedArrays by using Buffer to handle most binary cases + * // TODO: reaper, replace with `devalue` after evaluating all cases, current setup is + * more of a hotfix */ const cleanJson = (data) => { + const typedArrays = [ + // Baseline typed arrays + Int8Array, + Uint8Array, + Uint8ClampedArray, + Int16Array, + Uint16Array, + Int32Array, + Uint32Array, + Float32Array, + Float64Array, + BigInt64Array, + BigUint64Array, + + // Baseline 2025 Newly available + 'Float16Array' in globalThis ? Float16Array : null + ].filter(Boolean); + const binaryNames = typedArrays.map((d) => d.name); + + const replacer = (key, value) => { + const isBinary = typedArrays.find((d) => value instanceof d); + if (isBinary) { + return { + __cleanJSONType: isBinary.name, + __cleanJSONValue: Buffer.from(value.buffer).toJSON() + }; + } + return value; + }; + + const reviver = (key, value) => { + if (typeof value !== 'object' || value === null) { + return value; + } + if ('__cleanJSONType' in value && '__cleanJSONValue' in value) { + const matchedName = binaryNames.find((d) => value.__cleanJSONType === d); + if (!matchedName) return value; + const binConstructor = typedArrays.find((d) => d.name === matchedName); + + return binConstructor.from(Buffer.from(value.__cleanJSONValue)); + } + return value; + }; + try { - return JSON.parse(JSON.stringify(data)); + return JSON.parse(JSON.stringify(data, replacer), reviver); } catch (e) { return data; } diff --git a/packages/bruno-js/tests/utils.spec.js b/packages/bruno-js/tests/utils.spec.js index b1ecc7db7..b7c6e6022 100644 --- a/packages/bruno-js/tests/utils.spec.js +++ b/packages/bruno-js/tests/utils.spec.js @@ -1,5 +1,5 @@ const { describe, it, expect } = require('@jest/globals'); -const { evaluateJsExpression, internalExpressionCache: cache, createResponseParser } = require('../src/utils'); +const { evaluateJsExpression, internalExpressionCache: cache, createResponseParser, cleanJson } = require('../src/utils'); describe('utils', () => { describe('expression evaluation', () => { @@ -153,4 +153,64 @@ describe('utils', () => { expect(value).toBe(20); }); }); + + describe('cleanJson', () => { + it('primitives should be kept as is', () => { + const input = { + number: 1, + string: 'hello world', + booleanFalse: false, + booleanTrue: true, + float: 2.1, + floatDeep: 2.2222222 + }; + expect(cleanJson(input)).toEqual(input); + }); + + it('functions are lost', () => { + const func = function (x, y) { + return x + y; + }; + + const input = { + func, + number: 1 + }; + + expect(cleanJson(input)).toEqual({ + number: 1 + }); + }); + + it('dates are serialized', () => { + const date = new Date(); + const str = date.toISOString(); + + const input = { + date + }; + + expect(cleanJson(input)).toEqual({ + date: str + }); + }); + + it('typed arrays should be kept as is', () => { + const input = { + Int8Array: Int8Array.from(Buffer.from('hello world').toString()), + Uint8Array: Uint8Array.from(Buffer.from('hello world').toString()), + Uint8ClampedArray: Uint8ClampedArray.from(Buffer.from('hello world').toString()), + Int16Array: Int16Array.from(Buffer.from('hello world').toString()), + Uint16Array: Uint16Array.from(Buffer.from('hello world').toString()), + Int32Array: Int32Array.from(Buffer.from('hello world').toString()), + Uint32Array: Uint32Array.from(Buffer.from('hello world').toString()), + Float32Array: Float32Array.from(Buffer.from('hello world').toString()), + Float64Array: Float64Array.from(Buffer.from('hello world').toString()), + BigInt64Array: BigInt64Array.from(Buffer.from('123').toString()), + BigUint64Array: BigUint64Array.from(Buffer.from('234').toString()) + }; + + expect(cleanJson(input)).toEqual(input); + }); + }); });