Skip to content

Commit c9c2ec5

Browse files
committed
feat(console): minor refactor and extend set and map logging support into developer mode
1 parent 79ce71c commit c9c2ec5

File tree

5 files changed

+275
-221
lines changed

5 files changed

+275
-221
lines changed

packages/bruno-app/src/components/Devtools/Console/index.js

Lines changed: 83 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -64,80 +64,106 @@ const LogTimestamp = ({ timestamp }) => {
6464
return <span className="log-timestamp">{time}</span>;
6565
};
6666

67-
const LogMessage = ({ message, args }) => {
68-
const { displayedTheme } = useTheme();
67+
// Helper function to check if an object is a plain object (not a class instance)
68+
const isPlainObject = (obj) => {
69+
if (typeof obj !== 'object' || obj === null) return false;
70+
const proto = Object.getPrototypeOf(obj);
71+
return proto === null || proto === Object.prototype;
72+
};
6973

70-
// Helper function to transform Bruno special types back to readable format
71-
// Returns { data, metadata } where metadata contains type information
72-
const transformBrunoTypes = (obj, returnMetadata = false) => {
73-
if (typeof obj !== 'object' || obj === null) {
74-
return returnMetadata ? { data: obj, metadata: {} } : obj;
75-
}
74+
// Helper function to transform Bruno special types back to readable format
75+
// Extracted outside component to avoid recreation on every render
76+
const transformBrunoTypes = (obj, seen = new WeakSet()) => {
77+
if (typeof obj !== 'object' || obj === null) {
78+
return obj;
79+
}
7680

77-
// Handle Bruno special types
78-
if (obj.__brunoType) {
79-
switch (obj.__brunoType) {
80-
case 'Set':
81-
// Transform Set to display values at top level with numeric indices
82-
// Convert array of values to object with numeric keys (0, 1, 2, ...)
83-
const setEntries = {};
84-
if (Array.isArray(obj.__brunoValue)) {
85-
obj.__brunoValue.forEach((value, index) => {
86-
setEntries[index] = transformBrunoTypes(value, false);
87-
});
88-
}
89-
return returnMetadata ? { data: setEntries, metadata: { type: 'Set' } } : setEntries;
90-
case 'Map':
91-
// Transform Map to display entries at top level with => notation
92-
// Convert array of [key, value] pairs to object with "key => value" format
81+
// Guard against circular references
82+
if (seen.has(obj)) {
83+
return '[Circular]';
84+
}
85+
seen.add(obj);
86+
87+
// Handle Bruno special types
88+
if (obj.__brunoType) {
89+
switch (obj.__brunoType) {
90+
case 'Set':
91+
// Transform Set to display values at top level with numeric indices
92+
if (Array.isArray(obj.__brunoValue)) {
93+
return Object.fromEntries(
94+
obj.__brunoValue.map((value, index) => [index, transformBrunoTypes(value, seen)])
95+
);
96+
}
97+
return {};
98+
case 'Map':
99+
// Transform Map to display entries at top level with => notation
100+
if (Array.isArray(obj.__brunoValue)) {
93101
const mapEntries = {};
94-
if (Array.isArray(obj.__brunoValue)) {
95-
obj.__brunoValue.forEach(([key, value]) => {
96-
// Use => notation to clearly indicate Map entries
97-
const displayKey = `${String(key)} =>`;
98-
mapEntries[displayKey] = transformBrunoTypes(value, false);
99-
});
102+
for (const entry of obj.__brunoValue) {
103+
// Defensive check: ensure entry is a valid [key, value] pair
104+
if (Array.isArray(entry) && entry.length >= 2) {
105+
const [key, value] = entry;
106+
mapEntries[`${String(key)} =>`] = transformBrunoTypes(value, seen);
107+
}
100108
}
101-
return returnMetadata ? { data: mapEntries, metadata: { type: 'Map' } } : mapEntries;
102-
case 'Function':
103-
const funcData = `[Function: ${obj.__brunoValue.split('\n')[0].substring(0, 50)}...]`;
104-
return returnMetadata ? { data: funcData, metadata: {} } : funcData;
105-
case 'undefined':
106-
return returnMetadata ? { data: 'undefined', metadata: {} } : 'undefined';
107-
default:
108-
return returnMetadata ? { data: obj, metadata: {} } : obj;
109-
}
109+
return mapEntries;
110+
}
111+
return {};
112+
case 'Function':
113+
return `[Function: ${obj.__brunoValue?.split?.('\n')?.[0]?.substring(0, 50) ?? 'anonymous'}...]`;
114+
case 'undefined':
115+
return 'undefined';
116+
default:
117+
return obj;
110118
}
119+
}
111120

112-
// Recursively transform nested objects
113-
if (Array.isArray(obj)) {
114-
const transformed = obj.map((item) => transformBrunoTypes(item, false));
115-
return returnMetadata ? { data: transformed, metadata: {} } : transformed;
116-
}
121+
// Handle arrays - recurse into elements
122+
if (Array.isArray(obj)) {
123+
return obj.map((item) => transformBrunoTypes(item, seen));
124+
}
117125

118-
const transformed = {};
119-
for (const [key, value] of Object.entries(obj)) {
120-
transformed[key] = transformBrunoTypes(value, false);
121-
}
122-
return returnMetadata ? { data: transformed, metadata: {} } : transformed;
123-
};
126+
// Preserve non-plain objects (Date, Error, RegExp, class instances, etc.)
127+
if (!isPlainObject(obj)) {
128+
return obj;
129+
}
130+
131+
// Only deep-clone plain objects
132+
const transformed = {};
133+
for (const [key, value] of Object.entries(obj)) {
134+
transformed[key] = transformBrunoTypes(value, seen);
135+
}
136+
return transformed;
137+
};
138+
139+
// Helper to get metadata about Bruno types for display purposes
140+
const getBrunoTypeMetadata = (obj) => {
141+
if (typeof obj !== 'object' || obj === null) {
142+
return {};
143+
}
144+
if (obj.__brunoType === 'Set' || obj.__brunoType === 'Map') {
145+
return { type: obj.__brunoType };
146+
}
147+
return {};
148+
};
149+
150+
const LogMessage = ({ message, args }) => {
151+
const { displayedTheme } = useTheme();
124152

125153
const formatMessage = (msg, originalArgs) => {
126154
if (originalArgs && originalArgs.length > 0) {
127155
return originalArgs.map((arg, index) => {
128156
if (typeof arg === 'object' && arg !== null) {
129-
const { data: transformedArg, metadata } = transformBrunoTypes(arg, true);
157+
const metadata = getBrunoTypeMetadata(arg);
158+
const transformedArg = transformBrunoTypes(arg);
130159

131160
// Determine the name to display based on the type
132161
let displayName = false;
133162
let shouldCollapse = 1; // Default: collapse at depth 1 for regular objects
134163

135-
if (metadata.type === 'Map') {
136-
displayName = 'Map';
137-
shouldCollapse = true; // Fully collapse Maps by default
138-
} else if (metadata.type === 'Set') {
139-
displayName = 'Set';
140-
shouldCollapse = true; // Fully collapse Sets by default
164+
if (metadata.type === 'Map' || metadata.type === 'Set') {
165+
displayName = metadata.type;
166+
shouldCollapse = true; // Fully collapse Maps/Sets by default
141167
}
142168

143169
return (
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* Gets the type tag of a value using Object.prototype.toString
3+
* This works across VM context boundaries unlike instanceof
4+
* @param {*} value - The value to check
5+
* @returns {string} The type tag (e.g., 'Set', 'Map', 'Array', 'Object')
6+
*/
7+
function getTypeTag(value) {
8+
return Object.prototype.toString.call(value).slice(8, -1);
9+
}
10+
11+
/**
12+
* Transforms a value, converting Set and Map to a special format for display
13+
* Uses Object.prototype.toString for cross-context type detection
14+
* @param {*} value - The value to transform
15+
* @param {WeakSet} seen - Set of already visited objects for circular ref detection
16+
* @returns {*} Transformed value with Set/Map converted to __brunoType format
17+
*/
18+
function transformValue(value, seen = new WeakSet()) {
19+
// Return primitives as-is
20+
if (value === null || value === undefined || typeof value !== 'object' && typeof value !== 'function') {
21+
return value;
22+
}
23+
24+
// Circular reference check for objects
25+
if (typeof value === 'object') {
26+
if (seen.has(value)) {
27+
return '[Circular]';
28+
}
29+
seen.add(value);
30+
}
31+
32+
const typeTag = getTypeTag(value);
33+
34+
if (typeTag === 'Set') {
35+
return {
36+
__brunoType: 'Set',
37+
__brunoValue: Array.from(value).map((item) => transformValue(item, seen))
38+
};
39+
}
40+
41+
if (typeTag === 'Map') {
42+
return {
43+
__brunoType: 'Map',
44+
__brunoValue: Array.from(value.entries()).map(([k, v]) => [
45+
transformValue(k, seen),
46+
transformValue(v, seen)
47+
])
48+
};
49+
}
50+
51+
if (typeTag === 'Array') {
52+
return value.map((item) => transformValue(item, seen));
53+
}
54+
55+
if (typeTag === 'Object') {
56+
const transformed = {};
57+
for (const [key, val] of Object.entries(value)) {
58+
transformed[key] = transformValue(val, seen);
59+
}
60+
return transformed;
61+
}
62+
63+
// Handle functions - show clean wrapper
64+
if (typeTag === 'Function' || typeof value === 'function') {
65+
const name = value.name || 'anonymous';
66+
return `function ${name}() {\n [native code]\n}`;
67+
}
68+
69+
// Handle other built-in types (Date, RegExp, Error, etc.) - convert to string representation
70+
try {
71+
return value?.toString?.() ?? String(value);
72+
} catch {
73+
return `[${typeTag}]`;
74+
}
75+
}
76+
77+
/**
78+
* Wraps a console object to add Set/Map support for logging
79+
* @param {Object} originalConsole - The original console object
80+
* @returns {Object} Wrapped console with Set/Map transformation
81+
*/
82+
function wrapConsoleWithSerializers(originalConsole) {
83+
if (!originalConsole) return originalConsole;
84+
85+
const methodsToWrap = ['log', 'debug', 'info', 'warn', 'error'];
86+
const wrappedConsole = { ...originalConsole };
87+
88+
for (const method of methodsToWrap) {
89+
if (typeof originalConsole[method] === 'function') {
90+
wrappedConsole[method] = (...args) => {
91+
const transformedArgs = args.map((arg) => transformValue(arg));
92+
originalConsole[method](...transformedArgs);
93+
};
94+
}
95+
}
96+
97+
return wrappedConsole;
98+
}
99+
100+
module.exports = {
101+
wrapConsoleWithSerializers
102+
};

packages/bruno-js/src/sandbox/node-vm/index.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const path = require('node:path');
44
const { get } = require('lodash');
55
const lodash = require('lodash');
66
const { mixinTypedArrays } = require('../mixins/typed-arrays');
7+
const { wrapConsoleWithSerializers } = require('./console');
78

89
class ScriptError extends Error {
910
constructor(error, script) {
@@ -47,8 +48,8 @@ async function runScriptInNodeVm({
4748

4849
// Create script context with all necessary variables
4950
const scriptContext = {
50-
// Bruno context
51-
console: context.console,
51+
// Bruno context (wrap console with Set/Map support)
52+
console: wrapConsoleWithSerializers(context.console),
5253
req: context.req,
5354
res: context.res,
5455
bru: context.bru,

packages/bruno-js/src/sandbox/quickjs/index.js

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -164,54 +164,6 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
164164
fn.apply();
165165
}
166166
167-
// Override console.log to handle Sets and Maps properly
168-
const originalConsoleLog = console.log;
169-
console.log = function(...args) {
170-
const processedArgs = args.map(arg => {
171-
if (arg instanceof Set) {
172-
return {
173-
__brunoType: 'Set',
174-
__brunoValue: Array.from(arg),
175-
size: arg.size
176-
};
177-
}
178-
if (arg instanceof Map) {
179-
return {
180-
__brunoType: 'Map',
181-
__brunoValue: Array.from(arg.entries()),
182-
size: arg.size
183-
};
184-
}
185-
return arg;
186-
});
187-
return originalConsoleLog.apply(this, processedArgs);
188-
};
189-
190-
// Also override other console methods
191-
['debug', 'info', 'warn', 'error'].forEach(method => {
192-
const originalMethod = console[method];
193-
console[method] = function(...args) {
194-
const processedArgs = args.map(arg => {
195-
if (arg instanceof Set) {
196-
return {
197-
__brunoType: 'Set',
198-
__brunoValue: Array.from(arg),
199-
size: arg.size
200-
};
201-
}
202-
if (arg instanceof Map) {
203-
return {
204-
__brunoType: 'Map',
205-
__brunoValue: Array.from(arg.entries()),
206-
size: arg.size
207-
};
208-
}
209-
return arg;
210-
});
211-
return originalMethod.apply(this, processedArgs);
212-
};
213-
});
214-
215167
await bru.sleep(0);
216168
try {
217169
${externalScript}

0 commit comments

Comments
 (0)