mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-22 12:15:38 +00:00
feat(console): minor refactor and extend set and map logging support into developer mode
This commit is contained in:
@@ -64,80 +64,106 @@ const LogTimestamp = ({ timestamp }) => {
|
||||
return <span className="log-timestamp">{time}</span>;
|
||||
};
|
||||
|
||||
// Helper function to check if an object is a plain object (not a class instance)
|
||||
const isPlainObject = (obj) => {
|
||||
if (typeof obj !== 'object' || obj === null) return false;
|
||||
const proto = Object.getPrototypeOf(obj);
|
||||
return proto === null || proto === Object.prototype;
|
||||
};
|
||||
|
||||
// Helper function to transform Bruno special types back to readable format
|
||||
// Extracted outside component to avoid recreation on every render
|
||||
const transformBrunoTypes = (obj, seen = new WeakSet()) => {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Guard against circular references
|
||||
if (seen.has(obj)) {
|
||||
return '[Circular]';
|
||||
}
|
||||
seen.add(obj);
|
||||
|
||||
// Handle Bruno special types
|
||||
if (obj.__brunoType) {
|
||||
switch (obj.__brunoType) {
|
||||
case 'Set':
|
||||
// Transform Set to display values at top level with numeric indices
|
||||
if (Array.isArray(obj.__brunoValue)) {
|
||||
return Object.fromEntries(
|
||||
obj.__brunoValue.map((value, index) => [index, transformBrunoTypes(value, seen)])
|
||||
);
|
||||
}
|
||||
return {};
|
||||
case 'Map':
|
||||
// Transform Map to display entries at top level with => notation
|
||||
if (Array.isArray(obj.__brunoValue)) {
|
||||
const mapEntries = {};
|
||||
for (const entry of obj.__brunoValue) {
|
||||
// Defensive check: ensure entry is a valid [key, value] pair
|
||||
if (Array.isArray(entry) && entry.length >= 2) {
|
||||
const [key, value] = entry;
|
||||
mapEntries[`${String(key)} =>`] = transformBrunoTypes(value, seen);
|
||||
}
|
||||
}
|
||||
return mapEntries;
|
||||
}
|
||||
return {};
|
||||
case 'Function':
|
||||
return `[Function: ${obj.__brunoValue?.split?.('\n')?.[0]?.substring(0, 50) ?? 'anonymous'}...]`;
|
||||
case 'undefined':
|
||||
return 'undefined';
|
||||
default:
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle arrays - recurse into elements
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => transformBrunoTypes(item, seen));
|
||||
}
|
||||
|
||||
// Preserve non-plain objects (Date, Error, RegExp, class instances, etc.)
|
||||
if (!isPlainObject(obj)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Only deep-clone plain objects
|
||||
const transformed = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
transformed[key] = transformBrunoTypes(value, seen);
|
||||
}
|
||||
return transformed;
|
||||
};
|
||||
|
||||
// Helper to get metadata about Bruno types for display purposes
|
||||
const getBrunoTypeMetadata = (obj) => {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return {};
|
||||
}
|
||||
if (obj.__brunoType === 'Set' || obj.__brunoType === 'Map') {
|
||||
return { type: obj.__brunoType };
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const LogMessage = ({ message, args }) => {
|
||||
const { displayedTheme } = useTheme();
|
||||
|
||||
// Helper function to transform Bruno special types back to readable format
|
||||
// Returns { data, metadata } where metadata contains type information
|
||||
const transformBrunoTypes = (obj, returnMetadata = false) => {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return returnMetadata ? { data: obj, metadata: {} } : obj;
|
||||
}
|
||||
|
||||
// Handle Bruno special types
|
||||
if (obj.__brunoType) {
|
||||
switch (obj.__brunoType) {
|
||||
case 'Set':
|
||||
// Transform Set to display values at top level with numeric indices
|
||||
// Convert array of values to object with numeric keys (0, 1, 2, ...)
|
||||
const setEntries = {};
|
||||
if (Array.isArray(obj.__brunoValue)) {
|
||||
obj.__brunoValue.forEach((value, index) => {
|
||||
setEntries[index] = transformBrunoTypes(value, false);
|
||||
});
|
||||
}
|
||||
return returnMetadata ? { data: setEntries, metadata: { type: 'Set' } } : setEntries;
|
||||
case 'Map':
|
||||
// Transform Map to display entries at top level with => notation
|
||||
// Convert array of [key, value] pairs to object with "key => value" format
|
||||
const mapEntries = {};
|
||||
if (Array.isArray(obj.__brunoValue)) {
|
||||
obj.__brunoValue.forEach(([key, value]) => {
|
||||
// Use => notation to clearly indicate Map entries
|
||||
const displayKey = `${String(key)} =>`;
|
||||
mapEntries[displayKey] = transformBrunoTypes(value, false);
|
||||
});
|
||||
}
|
||||
return returnMetadata ? { data: mapEntries, metadata: { type: 'Map' } } : mapEntries;
|
||||
case 'Function':
|
||||
const funcData = `[Function: ${obj.__brunoValue.split('\n')[0].substring(0, 50)}...]`;
|
||||
return returnMetadata ? { data: funcData, metadata: {} } : funcData;
|
||||
case 'undefined':
|
||||
return returnMetadata ? { data: 'undefined', metadata: {} } : 'undefined';
|
||||
default:
|
||||
return returnMetadata ? { data: obj, metadata: {} } : obj;
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively transform nested objects
|
||||
if (Array.isArray(obj)) {
|
||||
const transformed = obj.map((item) => transformBrunoTypes(item, false));
|
||||
return returnMetadata ? { data: transformed, metadata: {} } : transformed;
|
||||
}
|
||||
|
||||
const transformed = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
transformed[key] = transformBrunoTypes(value, false);
|
||||
}
|
||||
return returnMetadata ? { data: transformed, metadata: {} } : transformed;
|
||||
};
|
||||
|
||||
const formatMessage = (msg, originalArgs) => {
|
||||
if (originalArgs && originalArgs.length > 0) {
|
||||
return originalArgs.map((arg, index) => {
|
||||
if (typeof arg === 'object' && arg !== null) {
|
||||
const { data: transformedArg, metadata } = transformBrunoTypes(arg, true);
|
||||
const metadata = getBrunoTypeMetadata(arg);
|
||||
const transformedArg = transformBrunoTypes(arg);
|
||||
|
||||
// Determine the name to display based on the type
|
||||
let displayName = false;
|
||||
let shouldCollapse = 1; // Default: collapse at depth 1 for regular objects
|
||||
|
||||
if (metadata.type === 'Map') {
|
||||
displayName = 'Map';
|
||||
shouldCollapse = true; // Fully collapse Maps by default
|
||||
} else if (metadata.type === 'Set') {
|
||||
displayName = 'Set';
|
||||
shouldCollapse = true; // Fully collapse Sets by default
|
||||
if (metadata.type === 'Map' || metadata.type === 'Set') {
|
||||
displayName = metadata.type;
|
||||
shouldCollapse = true; // Fully collapse Maps/Sets by default
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
102
packages/bruno-js/src/sandbox/node-vm/console.js
Normal file
102
packages/bruno-js/src/sandbox/node-vm/console.js
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Gets the type tag of a value using Object.prototype.toString
|
||||
* This works across VM context boundaries unlike instanceof
|
||||
* @param {*} value - The value to check
|
||||
* @returns {string} The type tag (e.g., 'Set', 'Map', 'Array', 'Object')
|
||||
*/
|
||||
function getTypeTag(value) {
|
||||
return Object.prototype.toString.call(value).slice(8, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a value, converting Set and Map to a special format for display
|
||||
* Uses Object.prototype.toString for cross-context type detection
|
||||
* @param {*} value - The value to transform
|
||||
* @param {WeakSet} seen - Set of already visited objects for circular ref detection
|
||||
* @returns {*} Transformed value with Set/Map converted to __brunoType format
|
||||
*/
|
||||
function transformValue(value, seen = new WeakSet()) {
|
||||
// Return primitives as-is
|
||||
if (value === null || value === undefined || typeof value !== 'object' && typeof value !== 'function') {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Circular reference check for objects
|
||||
if (typeof value === 'object') {
|
||||
if (seen.has(value)) {
|
||||
return '[Circular]';
|
||||
}
|
||||
seen.add(value);
|
||||
}
|
||||
|
||||
const typeTag = getTypeTag(value);
|
||||
|
||||
if (typeTag === 'Set') {
|
||||
return {
|
||||
__brunoType: 'Set',
|
||||
__brunoValue: Array.from(value).map((item) => transformValue(item, seen))
|
||||
};
|
||||
}
|
||||
|
||||
if (typeTag === 'Map') {
|
||||
return {
|
||||
__brunoType: 'Map',
|
||||
__brunoValue: Array.from(value.entries()).map(([k, v]) => [
|
||||
transformValue(k, seen),
|
||||
transformValue(v, seen)
|
||||
])
|
||||
};
|
||||
}
|
||||
|
||||
if (typeTag === 'Array') {
|
||||
return value.map((item) => transformValue(item, seen));
|
||||
}
|
||||
|
||||
if (typeTag === 'Object') {
|
||||
const transformed = {};
|
||||
for (const [key, val] of Object.entries(value)) {
|
||||
transformed[key] = transformValue(val, seen);
|
||||
}
|
||||
return transformed;
|
||||
}
|
||||
|
||||
// Handle functions - show clean wrapper
|
||||
if (typeTag === 'Function' || typeof value === 'function') {
|
||||
const name = value.name || 'anonymous';
|
||||
return `function ${name}() {\n [native code]\n}`;
|
||||
}
|
||||
|
||||
// Handle other built-in types (Date, RegExp, Error, etc.) - convert to string representation
|
||||
try {
|
||||
return value?.toString?.() ?? String(value);
|
||||
} catch {
|
||||
return `[${typeTag}]`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a console object to add Set/Map support for logging
|
||||
* @param {Object} originalConsole - The original console object
|
||||
* @returns {Object} Wrapped console with Set/Map transformation
|
||||
*/
|
||||
function wrapConsoleWithSerializers(originalConsole) {
|
||||
if (!originalConsole) return originalConsole;
|
||||
|
||||
const methodsToWrap = ['log', 'debug', 'info', 'warn', 'error'];
|
||||
const wrappedConsole = { ...originalConsole };
|
||||
|
||||
for (const method of methodsToWrap) {
|
||||
if (typeof originalConsole[method] === 'function') {
|
||||
wrappedConsole[method] = (...args) => {
|
||||
const transformedArgs = args.map((arg) => transformValue(arg));
|
||||
originalConsole[method](...transformedArgs);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return wrappedConsole;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
wrapConsoleWithSerializers
|
||||
};
|
||||
@@ -4,6 +4,7 @@ const path = require('node:path');
|
||||
const { get } = require('lodash');
|
||||
const lodash = require('lodash');
|
||||
const { mixinTypedArrays } = require('../mixins/typed-arrays');
|
||||
const { wrapConsoleWithSerializers } = require('./console');
|
||||
|
||||
class ScriptError extends Error {
|
||||
constructor(error, script) {
|
||||
@@ -47,8 +48,8 @@ async function runScriptInNodeVm({
|
||||
|
||||
// Create script context with all necessary variables
|
||||
const scriptContext = {
|
||||
// Bruno context
|
||||
console: context.console,
|
||||
// Bruno context (wrap console with Set/Map support)
|
||||
console: wrapConsoleWithSerializers(context.console),
|
||||
req: context.req,
|
||||
res: context.res,
|
||||
bru: context.bru,
|
||||
|
||||
@@ -164,54 +164,6 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
|
||||
fn.apply();
|
||||
}
|
||||
|
||||
// Override console.log to handle Sets and Maps properly
|
||||
const originalConsoleLog = console.log;
|
||||
console.log = function(...args) {
|
||||
const processedArgs = args.map(arg => {
|
||||
if (arg instanceof Set) {
|
||||
return {
|
||||
__brunoType: 'Set',
|
||||
__brunoValue: Array.from(arg),
|
||||
size: arg.size
|
||||
};
|
||||
}
|
||||
if (arg instanceof Map) {
|
||||
return {
|
||||
__brunoType: 'Map',
|
||||
__brunoValue: Array.from(arg.entries()),
|
||||
size: arg.size
|
||||
};
|
||||
}
|
||||
return arg;
|
||||
});
|
||||
return originalConsoleLog.apply(this, processedArgs);
|
||||
};
|
||||
|
||||
// Also override other console methods
|
||||
['debug', 'info', 'warn', 'error'].forEach(method => {
|
||||
const originalMethod = console[method];
|
||||
console[method] = function(...args) {
|
||||
const processedArgs = args.map(arg => {
|
||||
if (arg instanceof Set) {
|
||||
return {
|
||||
__brunoType: 'Set',
|
||||
__brunoValue: Array.from(arg),
|
||||
size: arg.size
|
||||
};
|
||||
}
|
||||
if (arg instanceof Map) {
|
||||
return {
|
||||
__brunoType: 'Map',
|
||||
__brunoValue: Array.from(arg.entries()),
|
||||
size: arg.size
|
||||
};
|
||||
}
|
||||
return arg;
|
||||
});
|
||||
return originalMethod.apply(this, processedArgs);
|
||||
};
|
||||
});
|
||||
|
||||
await bru.sleep(0);
|
||||
try {
|
||||
${externalScript}
|
||||
|
||||
@@ -2,150 +2,121 @@ const addConsoleShimToContext = (vm, console) => {
|
||||
if (!console) return;
|
||||
|
||||
// Helper function to convert QuickJS values to native values with Set/Map support
|
||||
const dumpWithSetMapSupport = arg => {
|
||||
const dumpWithSerializers = (arg) => {
|
||||
// Track all handles for centralized disposal
|
||||
let nameProp, constructorProp, constructorNameProp, toStringFn, toStringResult;
|
||||
let arrayFn, fromFn, arrayResult;
|
||||
|
||||
try {
|
||||
// First try to dump normally
|
||||
const dumped = vm.dump(arg);
|
||||
const argType = vm.typeof(arg);
|
||||
|
||||
// Check if it's a Set by trying to access Set-specific properties
|
||||
if (vm.typeof(arg) === 'object' && arg !== vm.null && arg !== vm.undefined) {
|
||||
// Try to get the constructor name
|
||||
const constructorProp = vm.getProp(arg, 'constructor');
|
||||
if (constructorProp) {
|
||||
const constructorNameProp = vm.getProp(constructorProp, 'name');
|
||||
if (constructorNameProp) {
|
||||
const constructorName = vm.dump(constructorNameProp);
|
||||
constructorNameProp.dispose();
|
||||
|
||||
if (constructorName === 'Set') {
|
||||
// It's a Set, convert to our special format
|
||||
const sizeProp = vm.getProp(arg, 'size');
|
||||
const size = sizeProp ? vm.dump(sizeProp) : 0;
|
||||
sizeProp?.dispose();
|
||||
|
||||
// Get values by calling values() method
|
||||
const valuesFn = vm.getProp(arg, 'values');
|
||||
if (valuesFn) {
|
||||
const valuesIterator = vm.callFunction(valuesFn, arg);
|
||||
const values = [];
|
||||
|
||||
// Try to extract values (this is a simplified approach)
|
||||
try {
|
||||
// For now, we'll use a different approach - convert via Array.from
|
||||
const arrayFromFn = vm.getProp(vm.global, 'Array');
|
||||
if (arrayFromFn) {
|
||||
const fromFn = vm.getProp(arrayFromFn, 'from');
|
||||
if (fromFn) {
|
||||
const arrayResult = vm.callFunction(fromFn, arrayFromFn, arg);
|
||||
const arrayValues = vm.dump(arrayResult);
|
||||
arrayResult.dispose();
|
||||
fromFn.dispose();
|
||||
|
||||
constructorProp.dispose();
|
||||
valuesFn.dispose();
|
||||
valuesIterator?.dispose();
|
||||
arrayFromFn.dispose();
|
||||
|
||||
return {
|
||||
__brunoType: 'Set',
|
||||
__brunoValue: arrayValues,
|
||||
size: size,
|
||||
};
|
||||
}
|
||||
fromFn?.dispose();
|
||||
}
|
||||
arrayFromFn?.dispose();
|
||||
} catch (e) {
|
||||
// Fallback to empty array
|
||||
}
|
||||
|
||||
valuesFn.dispose();
|
||||
valuesIterator?.dispose();
|
||||
|
||||
return {
|
||||
__brunoType: 'Set',
|
||||
__brunoValue: values,
|
||||
size: size,
|
||||
};
|
||||
}
|
||||
} else if (constructorName === 'Map') {
|
||||
// It's a Map, convert to our special format
|
||||
const sizeProp = vm.getProp(arg, 'size');
|
||||
const size = sizeProp ? vm.dump(sizeProp) : 0;
|
||||
sizeProp?.dispose();
|
||||
|
||||
// Try to convert via Array.from
|
||||
try {
|
||||
const arrayFromFn = vm.getProp(vm.global, 'Array');
|
||||
if (arrayFromFn) {
|
||||
const fromFn = vm.getProp(arrayFromFn, 'from');
|
||||
if (fromFn) {
|
||||
const arrayResult = vm.callFunction(fromFn, arrayFromFn, arg);
|
||||
const arrayValues = vm.dump(arrayResult);
|
||||
arrayResult.dispose();
|
||||
fromFn.dispose();
|
||||
arrayFromFn.dispose();
|
||||
|
||||
constructorProp.dispose();
|
||||
|
||||
return {
|
||||
__brunoType: 'Map',
|
||||
__brunoValue: arrayValues,
|
||||
size: size,
|
||||
};
|
||||
}
|
||||
fromFn?.dispose();
|
||||
}
|
||||
arrayFromFn?.dispose();
|
||||
} catch (e) {
|
||||
// Fallback
|
||||
}
|
||||
|
||||
constructorProp.dispose();
|
||||
|
||||
return {
|
||||
__brunoType: 'Map',
|
||||
__brunoValue: [],
|
||||
size: size,
|
||||
};
|
||||
}
|
||||
}
|
||||
constructorNameProp?.dispose();
|
||||
}
|
||||
constructorProp?.dispose();
|
||||
// Early return for primitives (string, number, boolean, undefined, null)
|
||||
if (arg == null || arg === vm.null || arg === vm.undefined) {
|
||||
return vm.dump(arg);
|
||||
}
|
||||
|
||||
return dumped;
|
||||
if (argType !== 'object' && argType !== 'function') {
|
||||
return vm.dump(arg);
|
||||
}
|
||||
|
||||
// Handle functions - show clean wrapper
|
||||
if (argType === 'function') {
|
||||
nameProp = vm.getProp(arg, 'name');
|
||||
const name = nameProp ? vm.dump(nameProp) || 'anonymous' : 'anonymous';
|
||||
return `function ${name}() {\n [native code]\n}`;
|
||||
}
|
||||
|
||||
// Try to get the constructor name to detect Set/Map
|
||||
constructorProp = vm.getProp(arg, 'constructor');
|
||||
if (!constructorProp) {
|
||||
return vm.dump(arg);
|
||||
}
|
||||
|
||||
let constructorName = null;
|
||||
constructorNameProp = vm.getProp(constructorProp, 'name');
|
||||
if (constructorNameProp) {
|
||||
constructorName = vm.dump(constructorNameProp);
|
||||
}
|
||||
|
||||
// Handle Date, RegExp, Error - call toString()
|
||||
if (constructorName === 'Date' || constructorName === 'RegExp' || constructorName?.endsWith?.('Error')) {
|
||||
toStringFn = vm.getProp(arg, 'toString');
|
||||
if (toStringFn) {
|
||||
toStringResult = vm.callFunction(toStringFn, arg);
|
||||
if (toStringResult.error) {
|
||||
return vm.dump(arg);
|
||||
}
|
||||
return vm.dump(toStringResult.value);
|
||||
}
|
||||
}
|
||||
|
||||
// If not a Set or Map, use standard dump
|
||||
if (constructorName !== 'Set' && constructorName !== 'Map') {
|
||||
return vm.dump(arg);
|
||||
}
|
||||
|
||||
// Convert Set or Map to array via Array.from
|
||||
arrayFn = vm.getProp(vm.global, 'Array');
|
||||
if (!arrayFn) {
|
||||
return vm.dump(arg);
|
||||
}
|
||||
|
||||
fromFn = vm.getProp(arrayFn, 'from');
|
||||
if (!fromFn) {
|
||||
return vm.dump(arg);
|
||||
}
|
||||
|
||||
arrayResult = vm.callFunction(fromFn, arrayFn, arg);
|
||||
if (arrayResult.error) {
|
||||
return vm.dump(arg);
|
||||
}
|
||||
|
||||
return {
|
||||
__brunoType: constructorName,
|
||||
__brunoValue: vm.dump(arrayResult.value)
|
||||
};
|
||||
} catch (e) {
|
||||
// Fallback to normal dump
|
||||
return vm.dump(arg);
|
||||
} finally {
|
||||
// Centralized handle disposal - dispose all handles regardless of success or error
|
||||
nameProp?.dispose();
|
||||
constructorProp?.dispose();
|
||||
constructorNameProp?.dispose();
|
||||
toStringFn?.dispose();
|
||||
toStringResult?.value?.dispose();
|
||||
toStringResult?.error?.dispose();
|
||||
arrayFn?.dispose();
|
||||
fromFn?.dispose();
|
||||
arrayResult?.value?.dispose();
|
||||
arrayResult?.error?.dispose();
|
||||
}
|
||||
};
|
||||
|
||||
const consoleHandle = vm.newObject();
|
||||
|
||||
const logHandle = vm.newFunction('log', (...args) => {
|
||||
const nativeArgs = args.map(dumpWithSetMapSupport);
|
||||
const nativeArgs = args.map(dumpWithSerializers);
|
||||
console?.log?.(...nativeArgs);
|
||||
});
|
||||
|
||||
const debugHandle = vm.newFunction('debug', (...args) => {
|
||||
const nativeArgs = args.map(dumpWithSetMapSupport);
|
||||
const nativeArgs = args.map(dumpWithSerializers);
|
||||
console?.debug?.(...nativeArgs);
|
||||
});
|
||||
|
||||
const infoHandle = vm.newFunction('info', (...args) => {
|
||||
const nativeArgs = args.map(dumpWithSetMapSupport);
|
||||
const nativeArgs = args.map(dumpWithSerializers);
|
||||
console?.info?.(...nativeArgs);
|
||||
});
|
||||
|
||||
const warnHandle = vm.newFunction('warn', (...args) => {
|
||||
const nativeArgs = args.map(dumpWithSetMapSupport);
|
||||
const nativeArgs = args.map(dumpWithSerializers);
|
||||
console?.warn?.(...nativeArgs);
|
||||
});
|
||||
|
||||
const errorHandle = vm.newFunction('error', (...args) => {
|
||||
const nativeArgs = args.map(dumpWithSetMapSupport);
|
||||
const nativeArgs = args.map(dumpWithSerializers);
|
||||
console?.error?.(...nativeArgs);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user