const std = @import("std"); const bun = @import("root").bun; const JSC = bun.JSC; const Output = bun.Output; const ConsoleObject = @This(); const Shimmer = @import("./bindings/shimmer.zig").Shimmer; const String = bun.String; const JSGlobalObject = JSC.JSGlobalObject; const JSValue = JSC.JSValue; const strings = bun.strings; const is_bindgen = JSC.is_bindgen; const ZigException = JSC.ZigException; const ZigString = JSC.ZigString; const VirtualMachine = JSC.VirtualMachine; const string = bun.string; const JSLexer = bun.js_lexer; const ScriptArguments = opaque {}; const JSPrinter = bun.js_printer; const Environment = bun.Environment; const default_allocator = bun.default_allocator; const JestPrettyFormat = @import("./test/pretty_format.zig").JestPrettyFormat; const JSPromise = JSC.JSPromise; const EventType = JSC.EventType; pub const shim = Shimmer("Bun", "ConsoleObject", @This()); pub const Type = *anyopaque; pub const name = "Bun::ConsoleObject"; pub const include = "\"ConsoleObject.h\""; pub const namespace = shim.namespace; const Counter = std.AutoHashMapUnmanaged(u64, u32); const BufferedWriter = std.io.BufferedWriter(4096, Output.WriterType); error_writer: BufferedWriter, writer: BufferedWriter, default_indent: u16 = 0, counts: Counter = .{}, pub fn format(_: @This(), comptime _: []const u8, _: anytype, _: anytype) !void {} pub fn init(error_writer: Output.WriterType, writer: Output.WriterType) ConsoleObject { return ConsoleObject{ .error_writer = BufferedWriter{ .unbuffered_writer = error_writer }, .writer = BufferedWriter{ .unbuffered_writer = writer }, }; } pub const MessageLevel = enum(u32) { Log = 0, Warning = 1, Error = 2, Debug = 3, Info = 4, _, }; pub const MessageType = enum(u32) { Log = 0, Dir = 1, DirXML = 2, Table = 3, Trace = 4, StartGroup = 5, StartGroupCollapsed = 6, EndGroup = 7, Clear = 8, Assert = 9, Timing = 10, Profile = 11, ProfileEnd = 12, Image = 13, _, }; var stderr_mutex: bun.Lock = .{}; var stdout_mutex: bun.Lock = .{}; threadlocal var stderr_lock_count: u16 = 0; threadlocal var stdout_lock_count: u16 = 0; /// https://console.spec.whatwg.org/#formatter pub fn messageWithTypeAndLevel( //console_: ConsoleObject.Type, ctype: ConsoleObject.Type, message_type: MessageType, //message_level: u32, level: MessageLevel, global: *JSGlobalObject, vals: [*]const JSValue, len: usize, ) callconv(JSC.conv) void { messageWithTypeAndLevel_(ctype, message_type, level, global, vals, len) catch |err| switch (err) { error.JSError => {}, error.OutOfMemory => global.throwOutOfMemory() catch {}, // TODO: properly propagate exception upwards }; } fn messageWithTypeAndLevel_( //console_: ConsoleObject.Type, _: ConsoleObject.Type, message_type: MessageType, //message_level: u32, level: MessageLevel, global: *JSGlobalObject, vals: [*]const JSValue, len: usize, ) bun.JSError!void { if (comptime is_bindgen) { return; } var console = global.bunVM().console; defer console.default_indent +|= @as(u16, @intFromBool(message_type == .StartGroup)); if (message_type == .StartGroup and len == 0) { // undefined is printed if passed explicitly. return; } if (message_type == .EndGroup) { console.default_indent -|= 1; return; } // Lock/unlock a mutex incase two JS threads are console.log'ing at the same time // We do this the slightly annoying way to avoid assigning a pointer if (level == .Warning or level == .Error or message_type == .Assert) { if (stderr_lock_count == 0) { stderr_mutex.lock(); } stderr_lock_count += 1; } else { if (stdout_lock_count == 0) { stdout_mutex.lock(); } stdout_lock_count += 1; } defer { if (level == .Warning or level == .Error or message_type == .Assert) { stderr_lock_count -= 1; if (stderr_lock_count == 0) { stderr_mutex.unlock(); } } else { stdout_lock_count -= 1; if (stdout_lock_count == 0) { stdout_mutex.unlock(); } } } if (message_type == .Clear) { Output.resetTerminal(); return; } if (message_type == .Assert and len == 0) { const text = if (Output.enable_ansi_colors_stderr) Output.prettyFmt("Assertion failed\n", true) else "Assertion failed\n"; console.error_writer.unbuffered_writer.writeAll(text) catch {}; return; } const enable_colors = if (level == .Warning or level == .Error) Output.enable_ansi_colors_stderr else Output.enable_ansi_colors_stdout; var buffered_writer = if (level == .Warning or level == .Error) &console.error_writer else &console.writer; var writer = buffered_writer.writer(); const Writer = @TypeOf(writer); var print_length = len; var print_options: FormatOptions = .{ .enable_colors = enable_colors, .add_newline = true, .flush = true, .default_indent = console.default_indent, }; if (message_type == .Table and len >= 1) { // if value is not an object/array/iterable, don't print a table and just print it var tabular_data = vals[0]; if (tabular_data.isObject()) { const properties = if (len >= 2 and vals[1].jsType().isArray()) vals[1] else JSValue.undefined; var table_printer = TablePrinter.init( global, level, tabular_data, properties, ); table_printer.value_formatter.indent += console.default_indent; switch (enable_colors) { inline else => |colors| table_printer.printTable(Writer, writer, colors) catch return, } buffered_writer.flush() catch {}; return; } } if (message_type == .Dir and len >= 2) { print_length = 1; var opts = vals[1]; if (opts.isObject()) { if (try opts.get(global, "depth")) |depth_prop| { if (depth_prop.isInt32() or depth_prop.isNumber() or depth_prop.isBigInt()) print_options.max_depth = depth_prop.toU16() else if (depth_prop.isNull()) print_options.max_depth = std.math.maxInt(u16); } if (try opts.get(global, "colors")) |colors_prop| { if (colors_prop.isBoolean()) print_options.enable_colors = colors_prop.toBoolean(); } } } if (print_length > 0) try format2( level, global, vals, print_length, @TypeOf(buffered_writer.unbuffered_writer.context), Writer, writer, print_options, ) else if (message_type == .Log) { _ = console.writer.write("\n") catch 0; console.writer.flush() catch {}; } else if (message_type != .Trace) writer.writeAll("undefined\n") catch {}; if (message_type == .Trace) { writeTrace(Writer, writer, global); buffered_writer.flush() catch {}; } } pub const TablePrinter = struct { const Column = struct { name: String, width: u32 = 1, }; const RowKey = union(@This().Type) { str: String, num: u32, const Type = enum { str, num }; }; const PADDING = 1; globalObject: *JSGlobalObject, level: MessageLevel, value_formatter: ConsoleObject.Formatter, tabular_data: JSValue, properties: JSValue, is_iterable: bool, jstype: JSValue.JSType, /// width of the "Values" column. /// This column is not appended to "columns" from the start, because it needs to be the last column. values_col_width: ?u32 = null, values_col_idx: usize = std.math.maxInt(usize), pub fn init( globalObject: *JSGlobalObject, level: MessageLevel, tabular_data: JSValue, properties: JSValue, ) TablePrinter { return TablePrinter{ .level = level, .globalObject = globalObject, .tabular_data = tabular_data, .properties = properties, .is_iterable = tabular_data.isIterable(globalObject), .jstype = tabular_data.jsType(), .value_formatter = ConsoleObject.Formatter{ .remaining_values = &[_]JSValue{}, .globalThis = globalObject, .ordered_properties = false, .quote_strings = false, .single_line = true, .max_depth = 5, .can_throw_stack_overflow = true, .stack_check = bun.StackCheck.init(), }, }; } const VisibleCharacterCounter = struct { width: *usize = undefined, pub const WriteError = error{}; pub const Writer = std.io.Writer( VisibleCharacterCounter, VisibleCharacterCounter.WriteError, VisibleCharacterCounter.write, ); pub fn write(this: VisibleCharacterCounter, bytes: []const u8) WriteError!usize { this.width.* += strings.visible.width.exclude_ansi_colors.utf8(bytes); return bytes.len; } pub fn writeAll(this: VisibleCharacterCounter, bytes: []const u8) WriteError!void { this.width.* += strings.width.exclude_ansi_colors.utf8(bytes); } }; /// Compute how much horizontal space will take a JSValue when printed fn getWidthForValue(this: *TablePrinter, value: JSValue) u32 { var width: usize = 0; var value_formatter = this.value_formatter; const tag = ConsoleObject.Formatter.Tag.get(value, this.globalObject); value_formatter.quote_strings = !(tag.tag == .String or tag.tag == .StringPossiblyFormatted); value_formatter.format( tag, VisibleCharacterCounter.Writer, VisibleCharacterCounter.Writer{ .context = .{ .width = &width, }, }, value, this.globalObject, false, ) catch {}; // TODO: return @truncate(width); } /// Update the sizes of the columns for the values of a given row, and create any additional columns as needed fn updateColumnsForRow(this: *TablePrinter, columns: *std.ArrayList(Column), row_key: RowKey, row_value: JSValue) !void { // update size of "(index)" column const row_key_len: u32 = switch (row_key) { .str => |value| @intCast(value.visibleWidthExcludeANSIColors(false)), .num => |value| @truncate(bun.fmt.fastDigitCount(value)), }; columns.items[0].width = @max(columns.items[0].width, row_key_len); // special handling for Map: column with idx=1 is "Keys" if (this.jstype.isMap()) { const entry_key = row_value.getIndex(this.globalObject, 0); const entry_value = row_value.getIndex(this.globalObject, 1); columns.items[1].width = @max(columns.items[1].width, this.getWidthForValue(entry_key)); this.values_col_width = @max(this.values_col_width orelse 0, this.getWidthForValue(entry_value)); return; } if (row_value.isObject()) { // object -> // - if "properties" arg was provided: iterate the already-created columns (except for the 0-th which is the index) // - otherwise: iterate the object properties, and create the columns on-demand if (!this.properties.isUndefined()) { for (columns.items[1..]) |*column| { if (row_value.getOwn(this.globalObject, column.name)) |value| { column.width = @max(column.width, this.getWidthForValue(value)); } } } else { var cols_iter = JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true, }).init(this.globalObject, row_value); defer cols_iter.deinit(); while (cols_iter.next()) |col_key| { const value = cols_iter.value; // find or create the column for the property const column: *Column = brk: { const col_str = String.init(col_key); for (columns.items[1..]) |*col| { if (col.name.eql(col_str)) { break :brk col; } } // Need to ref this string because JSPropertyIterator // uses `toString` instead of `toStringRef` for property names col_str.ref(); try columns.append(.{ .name = col_str }); break :brk &columns.items[columns.items.len - 1]; }; column.width = @max(column.width, this.getWidthForValue(value)); } } } else if (this.properties.isUndefined()) { // not object -> the value will go to the special "Values" column this.values_col_width = @max(this.values_col_width orelse 1, this.getWidthForValue(row_value)); } } fn writeStringNTimes(comptime Writer: type, writer: Writer, comptime str: []const u8, n: usize) !void { if (comptime str.len == 1) { try writer.writeByteNTimes(str[0], n); return; } for (0..n) |_| { try writer.writeAll(str); } } fn printRow( this: *TablePrinter, comptime Writer: type, writer: Writer, comptime enable_ansi_colors: bool, columns: *std.ArrayList(Column), row_key: RowKey, row_value: JSValue, ) !void { try writer.writeAll("│"); { const len: u32 = switch (row_key) { .str => |value| @truncate(value.visibleWidthExcludeANSIColors(false)), .num => |value| @truncate(bun.fmt.fastDigitCount(value)), }; const needed = columns.items[0].width -| len; // Right-align the number column try writer.writeByteNTimes(' ', needed + PADDING); switch (row_key) { .str => |value| try writer.print("{}", .{value}), .num => |value| try writer.print("{d}", .{value}), } try writer.writeByteNTimes(' ', PADDING); } for (1..columns.items.len) |col_idx| { const col = columns.items[col_idx]; try writer.writeAll("│"); var value = JSValue.zero; if (col_idx == 1 and this.jstype.isMap()) { // is the "Keys" column, when iterating a Map? value = row_value.getIndex(this.globalObject, 0); } else if (col_idx == this.values_col_idx) { // is the "Values" column? if (this.jstype.isMap()) { value = row_value.getIndex(this.globalObject, 1); } else if (!row_value.isObject()) { value = row_value; } } else if (row_value.isObject()) { value = row_value.getOwn(this.globalObject, col.name) orelse JSValue.zero; } if (value == .zero) { try writer.writeByteNTimes(' ', col.width + (PADDING * 2)); } else { const len: u32 = this.getWidthForValue(value); const needed = col.width -| len; try writer.writeByteNTimes(' ', PADDING); const tag = ConsoleObject.Formatter.Tag.get(value, this.globalObject); var value_formatter = this.value_formatter; value_formatter.quote_strings = !(tag.tag == .String or tag.tag == .StringPossiblyFormatted); defer { if (value_formatter.map_node) |node| { this.value_formatter.map_node = null; if (node.data.capacity() > 512) { node.data.clearAndFree(); } else { node.data.clearRetainingCapacity(); } node.release(); } } try value_formatter.format( tag, Writer, writer, value, this.globalObject, enable_ansi_colors, ); try writer.writeByteNTimes(' ', needed + PADDING); } } try writer.writeAll("│\n"); } pub fn printTable( this: *TablePrinter, comptime Writer: type, writer: Writer, comptime enable_ansi_colors: bool, ) !void { const globalObject = this.globalObject; var stack_fallback = std.heap.stackFallback(@sizeOf(Column) * 16, this.globalObject.allocator()); var columns = try std.ArrayList(Column).initCapacity(stack_fallback.get(), 16); defer { for (columns.items) |*col| { col.name.deref(); } columns.deinit(); } // create the first column " " which is always present columns.appendAssumeCapacity(.{ .name = String.static(" "), .width = 1, }); // special case for Map: create the special "Key" column at index 1 if (this.jstype.isMap()) { columns.appendAssumeCapacity(.{ .name = String.static("Key"), }); } // if the "properties" arg was provided, pre-populate the columns if (!this.properties.isUndefined()) { var properties_iter = JSC.JSArrayIterator.init(this.properties, globalObject); while (properties_iter.next()) |value| { try columns.append(.{ .name = value.toBunString(globalObject), }); } } // rows first pass - calculate the column widths { if (this.is_iterable) { var ctx_: struct { this: *TablePrinter, columns: *@TypeOf(columns), idx: u32 = 0, err: bool = false } = .{ .this = this, .columns = &columns }; this.tabular_data.forEachWithContext(globalObject, &ctx_, struct { fn callback(_: *JSC.VM, _: *JSGlobalObject, ctx: *@TypeOf(ctx_), value: JSValue) callconv(.C) void { updateColumnsForRow(ctx.this, ctx.columns, .{ .num = ctx.idx }, value) catch { ctx.err = true; }; ctx.idx += 1; } }.callback); if (ctx_.err) return error.JSError; } else { var rows_iter = JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true, }).init(globalObject, this.tabular_data); defer rows_iter.deinit(); while (rows_iter.next()) |row_key| { try this.updateColumnsForRow(&columns, .{ .str = String.init(row_key) }, rows_iter.value); } } } // append the special "Values" column as the last one, if it is present if (this.values_col_width) |width| { this.values_col_idx = columns.items.len; try columns.append(.{ .name = String.static("Values"), .width = width, }); } // print the table header (border line + column names line + border line) { for (columns.items) |*col| { // also update the col width with the length of the column name itself col.width = @max(col.width, @as(u32, @intCast(col.name.visibleWidthExcludeANSIColors(false)))); } try writer.writeAll("┌"); for (columns.items, 0..) |*col, i| { if (i > 0) try writer.writeAll("┬"); try writeStringNTimes(Writer, writer, "─", col.width + (PADDING * 2)); } try writer.writeAll("┐\n│"); for (columns.items, 0..) |col, i| { if (i > 0) try writer.writeAll("│"); const len = col.name.visibleWidthExcludeANSIColors(false); const needed = col.width -| len; try writer.writeByteNTimes(' ', 1); if (comptime enable_ansi_colors) { try writer.writeAll(Output.prettyFmt("", true)); } try writer.print("{}", .{col.name}); if (comptime enable_ansi_colors) { try writer.writeAll(Output.prettyFmt("", true)); } try writer.writeByteNTimes(' ', needed + PADDING); } try writer.writeAll("│\n├"); for (columns.items, 0..) |col, i| { if (i > 0) try writer.writeAll("┼"); try writeStringNTimes(Writer, writer, "─", col.width + (PADDING * 2)); } try writer.writeAll("┤\n"); } // rows second pass - print the actual table rows { if (this.is_iterable) { var ctx_: struct { this: *TablePrinter, columns: *@TypeOf(columns), writer: Writer, idx: u32 = 0, err: bool = false } = .{ .this = this, .columns = &columns, .writer = writer }; this.tabular_data.forEachWithContext(globalObject, &ctx_, struct { fn callback(_: *JSC.VM, _: *JSGlobalObject, ctx: *@TypeOf(ctx_), value: JSValue) callconv(.C) void { printRow(ctx.this, Writer, ctx.writer, enable_ansi_colors, ctx.columns, .{ .num = ctx.idx }, value) catch { ctx.err = true; }; ctx.idx += 1; } }.callback); if (ctx_.err) return error.JSError; } else { var rows_iter = JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true, }).init(globalObject, this.tabular_data); defer rows_iter.deinit(); while (rows_iter.next()) |row_key| { try this.printRow(Writer, writer, enable_ansi_colors, &columns, .{ .str = String.init(row_key) }, rows_iter.value); } } } // print the table bottom border { try writer.writeAll("└"); try writeStringNTimes(Writer, writer, "─", columns.items[0].width + (PADDING * 2)); for (columns.items[1..]) |*column| { try writer.writeAll("┴"); try writeStringNTimes(Writer, writer, "─", column.width + (PADDING * 2)); } try writer.writeAll("┘\n"); } } }; pub fn writeTrace(comptime Writer: type, writer: Writer, global: *JSGlobalObject) void { var holder = ZigException.Holder.init(); var vm = VirtualMachine.get(); defer holder.deinit(vm); const exception = holder.zigException(); var source_code_slice: ?ZigString.Slice = null; defer if (source_code_slice) |slice| slice.deinit(); var err = ZigString.init("trace output").toErrorInstance(global); err.toZigException(global, exception); vm.remapZigException( exception, err, null, &holder.need_to_clear_parser_arena_on_deinit, &source_code_slice, ); if (Output.enable_ansi_colors_stderr) VirtualMachine.printStackTrace( Writer, writer, exception.stack, true, ) catch {} else VirtualMachine.printStackTrace( Writer, writer, exception.stack, false, ) catch {}; } pub const FormatOptions = struct { enable_colors: bool, add_newline: bool, flush: bool, ordered_properties: bool = false, quote_strings: bool = false, max_depth: u16 = 2, single_line: bool = false, default_indent: u16 = 0, pub fn fromJS(formatOptions: *FormatOptions, globalThis: *JSC.JSGlobalObject, arguments: []const JSC.JSValue) bun.JSError!void { const arg1 = arguments[0]; if (arg1.isObject()) { if (try arg1.getTruthy(globalThis, "depth")) |opt| { if (opt.isInt32()) { const arg = opt.toInt32(); if (arg < 0) { return globalThis.throwInvalidArguments("expected depth to be greater than or equal to 0, got {d}", .{arg}); } formatOptions.max_depth = @as(u16, @truncate(@as(u32, @intCast(@min(arg, std.math.maxInt(u16)))))); } else if (opt.isNumber()) { const v = opt.coerce(f64, globalThis); if (std.math.isInf(v)) { formatOptions.max_depth = std.math.maxInt(u16); } else { return globalThis.throwInvalidArguments("expected depth to be an integer, got {d}", .{v}); } } } if (try arg1.getBooleanLoose(globalThis, "colors")) |opt| { formatOptions.enable_colors = opt; } if (try arg1.getBooleanLoose(globalThis, "sorted")) |opt| { formatOptions.ordered_properties = opt; } if (try arg1.getBooleanLoose(globalThis, "compact")) |opt| { formatOptions.single_line = opt; } } else { // formatOptions.show_hidden = arg1.toBoolean(); if (arguments.len > 0) { var depthArg = arg1; if (depthArg.isInt32()) { const arg = depthArg.toInt32(); if (arg < 0) { return globalThis.throwInvalidArguments("expected depth to be greater than or equal to 0, got {d}", .{arg}); } formatOptions.max_depth = @as(u16, @truncate(@as(u32, @intCast(@min(arg, std.math.maxInt(u16)))))); } else if (depthArg.isNumber()) { const v = depthArg.coerce(f64, globalThis); if (std.math.isInf(v)) { formatOptions.max_depth = std.math.maxInt(u16); } else { return globalThis.throwInvalidArguments("expected depth to be an integer, got {d}", .{v}); } } if (arguments.len > 1 and !arguments[1].isEmptyOrUndefinedOrNull()) { formatOptions.enable_colors = arguments[1].coerce(bool, globalThis); if (globalThis.hasException()) { return error.JSError; } } } } } }; pub fn format2( level: MessageLevel, global: *JSGlobalObject, vals: [*]const JSValue, len: usize, comptime RawWriter: type, comptime Writer: type, writer: Writer, options: FormatOptions, ) bun.JSError!void { if (len == 1) { // initialized later in this function. var fmt = ConsoleObject.Formatter{ .remaining_values = &[_]JSValue{}, .globalThis = global, .ordered_properties = options.ordered_properties, .quote_strings = options.quote_strings, .max_depth = options.max_depth, .single_line = options.single_line, .indent = options.default_indent, .stack_check = bun.StackCheck.init(), .can_throw_stack_overflow = true, }; defer fmt.deinit(); const tag = ConsoleObject.Formatter.Tag.get(vals[0], global); fmt.writeIndent(Writer, writer) catch return; if (tag.tag == .String) { if (options.enable_colors) { if (level == .Error) { writer.writeAll(comptime Output.prettyFmt("", true)) catch {}; } try fmt.format( tag, Writer, writer, vals[0], global, true, ); if (level == .Error) { writer.writeAll(comptime Output.prettyFmt("", true)) catch {}; } } else { try fmt.format( tag, Writer, writer, vals[0], global, false, ); } if (options.add_newline) { _ = writer.write("\n") catch 0; } writer.context.flush() catch {}; } else { defer { if (comptime Writer != RawWriter) { if (options.flush) writer.context.flush() catch {}; } } if (options.enable_colors) { try fmt.format( tag, Writer, writer, vals[0], global, true, ); } else { try fmt.format( tag, Writer, writer, vals[0], global, false, ); } if (options.add_newline) _ = writer.write("\n") catch 0; } return; } defer { if (comptime Writer != RawWriter) { if (options.flush) writer.context.flush() catch {}; } } var this_value: JSValue = vals[0]; var fmt = ConsoleObject.Formatter{ .remaining_values = vals[0..len][1..], .globalThis = global, .ordered_properties = options.ordered_properties, .quote_strings = options.quote_strings, .single_line = options.single_line, .indent = options.default_indent, .stack_check = bun.StackCheck.init(), .can_throw_stack_overflow = true, }; defer fmt.deinit(); var tag: ConsoleObject.Formatter.Tag.Result = undefined; fmt.writeIndent(Writer, writer) catch return; var any = false; if (options.enable_colors) { if (level == .Error) { writer.writeAll(comptime Output.prettyFmt("", true)) catch {}; } while (true) { if (any) { _ = writer.write(" ") catch 0; } any = true; tag = ConsoleObject.Formatter.Tag.get(this_value, global); if (tag.tag == .String and fmt.remaining_values.len > 0) { tag.tag = .{ .StringPossiblyFormatted = {} }; } try fmt.format(tag, Writer, writer, this_value, global, true); if (fmt.remaining_values.len == 0) { break; } this_value = fmt.remaining_values[0]; fmt.remaining_values = fmt.remaining_values[1..]; } if (level == .Error) { writer.writeAll(comptime Output.prettyFmt("", true)) catch {}; } } else { while (true) { if (any) { _ = writer.write(" ") catch 0; } any = true; tag = ConsoleObject.Formatter.Tag.get(this_value, global); if (tag.tag == .String and fmt.remaining_values.len > 0) { tag.tag = .{ .StringPossiblyFormatted = {} }; } try fmt.format(tag, Writer, writer, this_value, global, false); if (fmt.remaining_values.len == 0) break; this_value = fmt.remaining_values[0]; fmt.remaining_values = fmt.remaining_values[1..]; } } if (options.add_newline) _ = writer.write("\n") catch 0; } const CustomFormattedObject = struct { function: JSValue = .zero, this: JSValue = .zero, }; pub const Formatter = struct { remaining_values: []const JSValue = &[_]JSValue{}, map: Visited.Map = undefined, map_node: ?*Visited.Pool.Node = null, hide_native: bool = false, globalThis: *JSGlobalObject, indent: u32 = 0, depth: u16 = 0, max_depth: u16 = 8, quote_strings: bool = false, quote_keys: bool = false, failed: bool = false, estimated_line_length: usize = 0, always_newline_scope: bool = false, single_line: bool = false, ordered_properties: bool = false, custom_formatted_object: CustomFormattedObject = .{}, disable_inspect_custom: bool = false, stack_check: bun.StackCheck = .{ .cached_stack_end = std.math.maxInt(usize) }, can_throw_stack_overflow: bool = false, pub fn deinit(this: *Formatter) void { if (bun.take(&this.map_node)) |node| { node.data = this.map; if (node.data.capacity() > 512) { node.data.clearAndFree(); } else { node.data.clearRetainingCapacity(); } node.release(); } } pub fn goodTimeForANewLine(this: *@This()) bool { if (this.estimated_line_length > 80) { this.resetLine(); return true; } return false; } pub fn resetLine(this: *@This()) void { this.estimated_line_length = this.indent * 2; } pub fn addForNewLine(this: *@This(), len: usize) void { this.estimated_line_length +|= len; } pub const ZigFormatter = struct { formatter: *ConsoleObject.Formatter, value: JSValue, pub const WriteError = error{UhOh}; pub fn format(self: ZigFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { self.formatter.remaining_values = &[_]JSValue{self.value}; defer { self.formatter.remaining_values = &[_]JSValue{}; } try self.formatter.format( Tag.get(self.value, self.formatter.globalThis), @TypeOf(writer), writer, self.value, self.formatter.globalThis, false, ); } }; // For detecting circular references pub const Visited = struct { const ObjectPool = @import("../pool.zig").ObjectPool; pub const Map = std.AutoHashMap(JSValue, void); pub const Pool = ObjectPool( Map, struct { pub fn init(allocator: std.mem.Allocator) anyerror!Map { return Map.init(allocator); } }.init, true, 16, ); }; pub const Tag = enum { StringPossiblyFormatted, String, Undefined, Double, Integer, Null, Boolean, Array, Object, Function, Class, Error, TypedArray, Map, MapIterator, SetIterator, Set, BigInt, Symbol, CustomFormattedObject, GlobalObject, Private, Promise, JSON, toJSON, NativeCode, JSX, Event, GetterSetter, CustomGetterSetter, Proxy, RevokedProxy, pub fn isPrimitive(this: Tag) bool { return switch (this) { .String, .StringPossiblyFormatted, .Undefined, .Double, .Integer, .Null, .Boolean, .Symbol, .BigInt, => true, else => false, }; } pub fn canHaveCircularReferences(tag: Tag) bool { return switch (tag) { .Function, .Array, .Object, .Map, .Set, .Error, .Class, .Event => true, else => false, }; } const Result = struct { tag: union(Tag) { StringPossiblyFormatted: void, String: void, Undefined: void, Double: void, Integer: void, Null: void, Boolean: void, Array: void, Object: void, Function: void, Class: void, Error: void, TypedArray: void, Map: void, MapIterator: void, SetIterator: void, Set: void, BigInt: void, Symbol: void, CustomFormattedObject: CustomFormattedObject, GlobalObject: void, Private: void, Promise: void, JSON: void, toJSON: void, NativeCode: void, JSX: void, Event: void, GetterSetter: void, CustomGetterSetter: void, Proxy: void, RevokedProxy: void, pub fn isPrimitive(this: @This()) bool { return @as(Tag, this).isPrimitive(); } pub fn tag(this: @This()) Tag { return @as(Tag, this); } }, cell: JSValue.JSType = JSValue.JSType.Cell, }; pub fn get(value: JSValue, globalThis: *JSGlobalObject) Result { return getAdvanced(value, globalThis, .{ .hide_global = false }); } // It sounds silly to make this packed, but Tag.getAdvanced is extremely recursive. pub const Options = packed struct { hide_global: bool = false, disable_inspect_custom: bool = false, }; pub fn getAdvanced(value: JSValue, globalThis: *JSGlobalObject, opts: Options) Result { switch (@intFromEnum(value)) { 0, 0xa => return Result{ .tag = .{ .Undefined = {} }, }, 0x2 => return Result{ .tag = .{ .Null = {} }, }, else => {}, } if (value.isInt32()) { return .{ .tag = .{ .Integer = {} }, }; } else if (value.isNumber()) { return .{ .tag = .{ .Double = {} }, }; } else if (value.isBoolean()) { return .{ .tag = .{ .Boolean = {} }, }; } if (!value.isCell()) return .{ .tag = .{ .NativeCode = {} }, }; const js_type = value.jsType(); if (js_type.isHidden()) return .{ .tag = .{ .NativeCode = {} }, .cell = js_type, }; if (js_type == .Cell) { return .{ .tag = .{ .NativeCode = {} }, .cell = js_type, }; } if (js_type.canGet() and js_type != .ProxyObject and !opts.disable_inspect_custom) { // Attempt to get custom formatter if (value.fastGet(globalThis, .inspectCustom)) |callback_value| { if (callback_value.isCallable(globalThis.vm())) { return .{ .tag = .{ .CustomFormattedObject = .{ .function = callback_value, .this = value, }, }, .cell = js_type, }; } } if (globalThis.hasException()) return .{ .tag = .RevokedProxy }; } if (js_type == .DOMWrapper) { return .{ .tag = .{ .Private = {} }, .cell = js_type, }; } // If we check an Object has a method table and it does not // it will crash if (js_type != .Object and js_type != .ProxyObject and value.isCallable(globalThis.vm())) { if (value.isClass(globalThis)) { return .{ .tag = .{ .Class = {} }, .cell = js_type, }; } return .{ // TODO: we print InternalFunction as Object because we have a lot of // callable namespaces and printing the contents of it is better than [Function: namespace] // ideally, we would print [Function: namespace] { ... } on all functions, internal and js. // what we'll do later is rid of .Function and .Class and handle the prefix in the .Object formatter .tag = if (js_type == .InternalFunction) .Object else .Function, .cell = js_type, }; } if (js_type == .GlobalProxy) { if (!opts.hide_global) { return Tag.get( JSC.JSValue.c(JSC.C.JSObjectGetProxyTarget(value.asObjectRef())), globalThis, ); } return .{ .tag = .{ .GlobalObject = {} }, .cell = js_type, }; } // Is this a react element? if (js_type.isObject() and js_type != .ProxyObject) { if (value.getOwnTruthy(globalThis, "$$typeof")) |typeof_symbol| { var reactElement = ZigString.init("react.element"); var react_fragment = ZigString.init("react.fragment"); if (JSValue.isSameValue(typeof_symbol, JSValue.symbolFor(globalThis, &reactElement), globalThis) or JSValue.isSameValue(typeof_symbol, JSValue.symbolFor(globalThis, &react_fragment), globalThis)) { return .{ .tag = .{ .JSX = {} }, .cell = js_type }; } } } return .{ .tag = switch (js_type) { .ErrorInstance => .Error, .NumberObject => .Double, .DerivedArray, JSValue.JSType.Array, .DirectArguments, .ScopedArguments, .ClonedArguments => .Array, .DerivedStringObject, JSValue.JSType.String, JSValue.JSType.StringObject => .String, .RegExpObject => .String, .Symbol => .Symbol, .BooleanObject => .Boolean, .JSFunction => .Function, .WeakMap, JSValue.JSType.Map => .Map, .MapIterator => .MapIterator, .SetIterator => .SetIterator, .WeakSet, JSValue.JSType.Set => .Set, .JSDate => .JSON, .JSPromise => .Promise, .WrapForValidIterator, .RegExpStringIterator, .JSArrayIterator, .Iterator, .IteratorHelper, .Object, .FinalObject, .ModuleNamespaceObject, => .Object, .ProxyObject => tag: { const handler = value.getProxyInternalField(.handler); if (handler == .zero or handler == .undefined or handler == .null) { break :tag .RevokedProxy; } break :tag .Proxy; }, .GlobalObject => if (!opts.hide_global) .Object else .GlobalObject, .ArrayBuffer, .Int8Array, .Uint8Array, .Uint8ClampedArray, .Int16Array, .Uint16Array, .Int32Array, .Uint32Array, .Float16Array, .Float32Array, .Float64Array, .BigInt64Array, .BigUint64Array, .DataView, => .TypedArray, .HeapBigInt => .BigInt, // None of these should ever exist here // But we're going to check anyway .APIValueWrapper, .NativeExecutable, .ProgramExecutable, .ModuleProgramExecutable, .EvalExecutable, .FunctionExecutable, .UnlinkedFunctionExecutable, .UnlinkedProgramCodeBlock, .UnlinkedModuleProgramCodeBlock, .UnlinkedEvalCodeBlock, .UnlinkedFunctionCodeBlock, .CodeBlock, .JSImmutableButterfly, .JSSourceCode, .JSScriptFetcher, .JSScriptFetchParameters, .JSCallee, .GlobalLexicalEnvironment, .LexicalEnvironment, .ModuleEnvironment, .StrictEvalActivation, .WithScope, => .NativeCode, .Event => .Event, .GetterSetter => .GetterSetter, .CustomGetterSetter => .CustomGetterSetter, .JSAsJSONType => .toJSON, else => .JSON, }, .cell = js_type, }; } }; const CellType = JSC.C.CellType; threadlocal var name_buf: [512]u8 = undefined; /// https://console.spec.whatwg.org/#formatter const PercentTag = enum { s, // s i, // i or d f, // f o, // o O, // O c, // c }; fn writeWithFormatting( this: *ConsoleObject.Formatter, comptime Writer: type, writer_: Writer, comptime Slice: type, slice_: Slice, global: *JSGlobalObject, comptime enable_ansi_colors: bool, ) bun.JSError!void { var writer = WrappedWriter(Writer){ .ctx = writer_, .estimated_line_length = &this.estimated_line_length, }; var slice = slice_; var i: u32 = 0; var len: u32 = @as(u32, @truncate(slice.len)); var hit_percent = false; while (i < len) : (i += 1) { if (hit_percent) { i = 0; hit_percent = false; } switch (slice[i]) { '%' => { i += 1; if (i >= len) break; if (this.remaining_values.len == 0) break; const token: PercentTag = switch (slice[i]) { 's' => .s, 'f' => .f, 'o' => .o, 'O' => .O, 'd', 'i' => .i, 'c' => .c, '%' => { // print up to and including the first % const end = slice[0..i]; writer.writeAll(end); // then skip the second % so we dont hit it again slice = slice[@min(slice.len, i + 1)..]; len = @truncate(slice.len); i = 0; continue; }, else => continue, }; // Flush everything up to the % const end = slice[0 .. i - 1]; writer.writeAll(end); slice = slice[@min(slice.len, i + 1)..]; i = 0; hit_percent = true; len = @truncate(slice.len); const next_value = this.remaining_values[0]; this.remaining_values = this.remaining_values[1..]; // https://console.spec.whatwg.org/#formatter const max_before_e_notation = 1000000000000000000000; const min_before_e_notation = 0.000001; switch (token) { .s => try this.printAs(Tag.String, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors), .i => { // 1. If Type(current) is Symbol, let converted be NaN // 2. Otherwise, let converted be the result of Call(%parseInt%, undefined, current, 10) const int: i64 = brk: { // This logic is convoluted because %parseInt% will coerce the argument to a string // first. As an optimization, we can check if the argument is a number and // skip such coercion. if (next_value.isInt32()) { // Already an int, parseInt will parse to itself. break :brk next_value.asInt32(); } if (next_value.isNumber() or !next_value.isSymbol()) double_convert: { var value = next_value.coerceToDouble(global); if (!std.math.isFinite(value)) { // for NaN and the string Infinity and -Infinity, parseInt returns NaN break :double_convert; } // simulate parseInt, which converts the argument to a string and // then back to a number, without converting it to a string if (value == 0) { break :brk 0; } const sign: i64 = if (value < 0) -1 else 1; value = @abs(value); if (value >= max_before_e_notation) { // toString prints 1.000+e0, which parseInt will stop at // the '.' or the '+', this gives us a single digit value. while (value >= 10) value /= 10; break :brk @as(i64, @intFromFloat(@floor(value))) * sign; } else if (value < min_before_e_notation) { // toString prints 1.000-e0, which parseInt will stop at // the '.' or the '-', this gives us a single digit value. while (value < 1) value *= 10; break :brk @as(i64, @intFromFloat(@floor(value))) * sign; } // parsing stops at '.', so this is equal to @floor break :brk @as(i64, @intFromFloat(@floor(value))) * sign; } // for NaN and the string Infinity and -Infinity, parseInt returns NaN this.addForNewLine("NaN".len); writer.print("NaN", .{}); continue; }; if (int < std.math.maxInt(u32)) { const is_negative = int < 0; const digits = if (i != 0) bun.fmt.fastDigitCount(@as(u64, @intCast(@abs(int)))) + @as(u64, @intFromBool(is_negative)) else 1; this.addForNewLine(digits); } else { this.addForNewLine(bun.fmt.count("{d}", .{int})); } writer.print("{d}", .{int}); }, .f => { // 1. If Type(current) is Symbol, let converted be NaN // 2. Otherwise, let converted be the result of Call(%parseFloat%, undefined, [current]). const converted: f64 = brk: { if (next_value.isInt32()) { const int = next_value.asInt32(); const is_negative = int < 0; const digits = if (i != 0) bun.fmt.fastDigitCount(@as(u64, @intCast(@abs(int)))) + @as(u64, @intFromBool(is_negative)) else 1; this.addForNewLine(digits); writer.print("{d}", .{int}); continue; } if (next_value.isNumber()) { break :brk next_value.asNumber(); } if (next_value.isSymbol()) { break :brk std.math.nan(f64); } // TODO: this is not perfectly emulating parseFloat, // because spec says to convert the value to a string // and then parse as a number, but we are just coercing // a number. break :brk next_value.coerceToDouble(global); }; const abs = @abs(converted); if (abs < max_before_e_notation and abs >= min_before_e_notation) { this.addForNewLine(bun.fmt.count("{d}", .{converted})); writer.print("{d}", .{converted}); } else if (std.math.isNan(converted)) { this.addForNewLine("NaN".len); writer.writeAll("NaN"); } else if (std.math.isInf(converted)) { this.addForNewLine("Infinity".len + @as(usize, @intFromBool(converted < 0))); if (converted < 0) { writer.writeAll("-"); } writer.writeAll("Infinity"); } else { var buf: [124]u8 = undefined; const formatted = bun.fmt.FormatDouble.dtoa(&buf, converted); this.addForNewLine(formatted.len); writer.print("{s}", .{formatted}); } }, inline .o, .O => |t| { if (t == .o) { // TODO: Node.js applies the following extra formatter options. // // this.max_depth = 4; // this.show_proxy = true; // this.show_hidden = true; // // Spec defines %o as: // > An object with optimally useful formatting is an // > implementation-specific, potentially-interactive representation // > of an object judged to be maximally useful and informative. } try this.format(Tag.get(next_value, global), Writer, writer_, next_value, global, enable_ansi_colors); }, .c => { // TODO: Implement %c }, } if (this.remaining_values.len == 0) break; }, else => {}, } } if (slice.len > 0) writer.writeAll(slice); } pub fn WrappedWriter(comptime Writer: type) type { if (@hasDecl(Writer, "is_wrapped_writer")) { @compileError("Do not nest WrappedWriter"); } return struct { ctx: Writer, failed: bool = false, estimated_line_length: *usize, pub const is_wrapped_writer = true; pub fn print(self: *@This(), comptime fmt: string, args: anytype) void { self.ctx.print(fmt, args) catch { self.failed = true; }; } pub fn space(self: *@This()) void { self.estimated_line_length.* += 1; self.ctx.writeAll(" ") catch { self.failed = true; }; } pub fn pretty(self: *@This(), comptime fmt: string, comptime enable_ansi_color: bool, args: anytype) void { const length_ignoring_formatted_values = comptime brk: { const fmt_str = Output.prettyFmt(fmt, false); var length: usize = 0; var i: usize = 0; while (i < fmt_str.len) : (i += 1) { switch (fmt_str[i]) { '{' => { i += 1; if (i >= fmt_str.len) break; if (fmt_str[i] == '{') { @compileError("Format string too complicated for pretty() right now"); } while (i < fmt_str.len) : (i += 1) { if (fmt_str[i] == '}') { break; } } if (i >= fmt_str.len) break; }, 128...255 => { @compileError("Format string too complicated for pretty() right now"); }, else => {}, } length += 1; } break :brk length; }; self.estimated_line_length.* += length_ignoring_formatted_values; self.ctx.print(comptime Output.prettyFmt(fmt, enable_ansi_color), args) catch { self.failed = true; }; } pub fn writeLatin1(self: *@This(), buf: []const u8) void { var remain = buf; while (remain.len > 0) { if (strings.firstNonASCII(remain)) |i| { if (i > 0) { self.ctx.writeAll(remain[0..i]) catch { self.failed = true; return; }; } self.ctx.writeAll(&strings.latin1ToCodepointBytesAssumeNotASCII(remain[i])) catch { self.failed = true; }; remain = remain[i + 1 ..]; } else { break; } } self.ctx.writeAll(remain) catch return; } pub inline fn writeAll(self: *@This(), buf: []const u8) void { self.ctx.writeAll(buf) catch { self.failed = true; }; } pub inline fn writeString(self: *@This(), str: ZigString) void { self.print("{}", .{str}); } pub inline fn write16Bit(self: *@This(), input: []const u16) void { bun.fmt.formatUTF16Type([]const u16, input, self.ctx) catch { self.failed = true; }; } }; } const indentation_buf = [_]u8{' '} ** 64; pub fn writeIndent( this: *ConsoleObject.Formatter, comptime Writer: type, writer: Writer, ) !void { var total_remain: u32 = this.indent; while (total_remain > 0) { const written: u8 = @min(32, total_remain); try writer.writeAll(indentation_buf[0 .. written * 2]); total_remain -|= written; } } pub fn printComma(this: *ConsoleObject.Formatter, comptime Writer: type, writer: Writer, comptime enable_ansi_colors: bool) !void { try writer.writeAll(comptime Output.prettyFmt(",", enable_ansi_colors)); this.estimated_line_length += 1; } pub fn MapIterator(comptime Writer: type, comptime enable_ansi_colors: bool, comptime is_iterator: bool, comptime single_line: bool) type { return struct { formatter: *ConsoleObject.Formatter, writer: Writer, count: usize = 0, pub fn forEach(_: [*c]JSC.VM, globalObject: *JSGlobalObject, ctx: ?*anyopaque, nextValue: JSValue) callconv(.C) void { var this: *@This() = bun.cast(*@This(), ctx orelse return); if (this.formatter.failed) return; if (single_line and this.count > 0) { this.formatter.printComma(Writer, this.writer, enable_ansi_colors) catch unreachable; this.writer.writeAll(" ") catch unreachable; } if (!is_iterator) { const key = nextValue.getIndex(globalObject, 0); const value = nextValue.getIndex(globalObject, 1); if (!single_line) { this.formatter.writeIndent(Writer, this.writer) catch unreachable; } const key_tag = Tag.getAdvanced(key, globalObject, .{ .hide_global = true, .disable_inspect_custom = this.formatter.disable_inspect_custom, }); this.formatter.format( key_tag, Writer, this.writer, key, this.formatter.globalThis, enable_ansi_colors, ) catch {}; // TODO: this.writer.writeAll(": ") catch unreachable; const value_tag = Tag.getAdvanced(value, globalObject, .{ .hide_global = true, .disable_inspect_custom = this.formatter.disable_inspect_custom, }); this.formatter.format( value_tag, Writer, this.writer, value, this.formatter.globalThis, enable_ansi_colors, ) catch {}; // TODO: } else { if (!single_line) { this.writer.writeAll("\n") catch unreachable; this.formatter.writeIndent(Writer, this.writer) catch unreachable; } const tag = Tag.getAdvanced(nextValue, globalObject, .{ .hide_global = true, .disable_inspect_custom = this.formatter.disable_inspect_custom, }); this.formatter.format( tag, Writer, this.writer, nextValue, this.formatter.globalThis, enable_ansi_colors, ) catch {}; // TODO: } this.count += 1; if (!single_line) { this.formatter.printComma(Writer, this.writer, enable_ansi_colors) catch unreachable; if (!is_iterator) { this.writer.writeAll("\n") catch unreachable; } } } }; } pub fn SetIterator(comptime Writer: type, comptime enable_ansi_colors: bool, comptime single_line: bool) type { return struct { formatter: *ConsoleObject.Formatter, writer: Writer, is_first: bool = true, pub fn forEach(_: [*c]JSC.VM, globalObject: *JSGlobalObject, ctx: ?*anyopaque, nextValue: JSValue) callconv(.C) void { var this: *@This() = bun.cast(*@This(), ctx orelse return); if (this.formatter.failed) return; if (single_line) { if (!this.is_first) { this.formatter.printComma(Writer, this.writer, enable_ansi_colors) catch unreachable; this.writer.writeAll(" ") catch unreachable; } this.is_first = false; } else { this.formatter.writeIndent(Writer, this.writer) catch {}; } const key_tag = Tag.getAdvanced(nextValue, globalObject, .{ .hide_global = true, .disable_inspect_custom = this.formatter.disable_inspect_custom, }); this.formatter.format( key_tag, Writer, this.writer, nextValue, this.formatter.globalThis, enable_ansi_colors, ) catch {}; // TODO: if (!single_line) { this.formatter.printComma(Writer, this.writer, enable_ansi_colors) catch unreachable; this.writer.writeAll("\n") catch unreachable; } } }; } pub fn PropertyIterator(comptime Writer: type, comptime enable_ansi_colors_: bool) type { return struct { formatter: *ConsoleObject.Formatter, writer: Writer, i: usize = 0, single_line: bool, always_newline: bool = false, parent: JSValue, const enable_ansi_colors = enable_ansi_colors_; pub fn handleFirstProperty(this: *@This(), globalThis: *JSC.JSGlobalObject, value: JSValue) void { if (value.isCell() and !value.jsType().isFunction()) { var writer = WrappedWriter(Writer){ .ctx = this.writer, .failed = false, .estimated_line_length = &this.formatter.estimated_line_length, }; if (getObjectName(globalThis, value)) |name_str| { writer.print("{} ", .{name_str}); } } if (!this.single_line) { this.always_newline = true; } this.formatter.estimated_line_length = this.formatter.indent * 2 + 1; this.formatter.indent += 1; this.formatter.depth += 1; if (this.single_line) { this.writer.writeAll("{ ") catch {}; } else { this.writer.writeAll("{\n") catch {}; this.formatter.writeIndent(Writer, this.writer) catch {}; } } pub fn forEach( globalThis: *JSGlobalObject, ctx_ptr: ?*anyopaque, key: *ZigString, value: JSValue, is_symbol: bool, is_private_symbol: bool, ) callconv(.C) void { if (key.eqlComptime("constructor")) return; var ctx: *@This() = bun.cast(*@This(), ctx_ptr orelse return); var this = ctx.formatter; if (this.failed) return; const writer_ = ctx.writer; var writer = WrappedWriter(Writer){ .ctx = writer_, .failed = false, .estimated_line_length = &this.estimated_line_length, }; const tag = Tag.getAdvanced(value, globalThis, .{ .hide_global = true, .disable_inspect_custom = this.disable_inspect_custom, }); if (tag.cell.isHidden()) return; if (ctx.i == 0) { handleFirstProperty(ctx, globalThis, ctx.parent); } else { this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; } defer ctx.i += 1; if (ctx.i > 0) { if (!this.single_line and (ctx.always_newline or this.always_newline_scope or this.goodTimeForANewLine())) { writer.writeAll("\n"); this.writeIndent(Writer, writer_) catch {}; this.resetLine(); } else { writer.space(); } } if (!is_symbol) { // TODO: make this one pass? if (!key.is16Bit() and (!this.quote_keys and JSLexer.isLatin1Identifier(@TypeOf(key.slice()), key.slice()))) { this.addForNewLine(key.len + 1); writer.print( comptime Output.prettyFmt("{}: ", enable_ansi_colors), .{key}, ); } else if (key.is16Bit() and (!this.quote_keys and JSLexer.isLatin1Identifier(@TypeOf(key.utf16SliceAligned()), key.utf16SliceAligned()))) { this.addForNewLine(key.len + 1); writer.print( comptime Output.prettyFmt("{}: ", enable_ansi_colors), .{key}, ); } else if (key.is16Bit()) { var utf16Slice = key.utf16SliceAligned(); this.addForNewLine(utf16Slice.len + 2); if (comptime enable_ansi_colors) { writer.writeAll(comptime Output.prettyFmt("", true)); } writer.writeAll("\""); while (strings.indexOfAny16(utf16Slice, "\"")) |j| { writer.write16Bit(utf16Slice[0..j]); writer.writeAll("\""); utf16Slice = utf16Slice[j + 1 ..]; } writer.write16Bit(utf16Slice); writer.print( comptime Output.prettyFmt("\": ", enable_ansi_colors), .{}, ); } else { this.addForNewLine(key.len + 2); writer.print( comptime Output.prettyFmt("{s}: ", enable_ansi_colors), .{bun.fmt.formatJSONStringLatin1(key.slice())}, ); } } else if (Environment.isDebug and is_private_symbol) { this.addForNewLine(1 + "$:".len + key.len); writer.print( comptime Output.prettyFmt("{s}{any}: ", enable_ansi_colors), .{ if (key.len > 0 and key.charAt(0) == '#') "" else "$", key, }, ); } else { this.addForNewLine(1 + "[Symbol()]:".len + key.len); writer.print( comptime Output.prettyFmt("[Symbol({any})]: ", enable_ansi_colors), .{key}, ); } if (tag.cell.isStringLike()) { if (comptime enable_ansi_colors) { writer.writeAll(comptime Output.prettyFmt("", true)); } } this.format(tag, Writer, ctx.writer, value, globalThis, enable_ansi_colors) catch {}; // TODO: if (tag.cell.isStringLike()) { if (comptime enable_ansi_colors) { writer.writeAll(comptime Output.prettyFmt("", true)); } } } }; } fn getObjectName(globalThis: *JSC.JSGlobalObject, value: JSValue) ?ZigString { var name_str = ZigString.init(""); value.getClassName(globalThis, &name_str); if (!name_str.eqlComptime("Object")) { return name_str; } else if (value.getPrototype(globalThis).eqlValue(JSValue.null)) { return ZigString.static("[Object: null prototype]").*; } return null; } extern fn JSC__JSValue__callCustomInspectFunction( *JSC.JSGlobalObject, JSValue, JSValue, depth: u32, max_depth: u32, colors: bool, is_exception: *bool, ) JSValue; pub fn printAs( this: *ConsoleObject.Formatter, comptime Format: ConsoleObject.Formatter.Tag, comptime Writer: type, writer_: Writer, value: JSValue, jsType: JSValue.JSType, comptime enable_ansi_colors: bool, ) bun.JSError!void { if (this.failed) return; if (this.globalThis.hasException()) { return error.JSError; } var writer = WrappedWriter(Writer){ .ctx = writer_, .estimated_line_length = &this.estimated_line_length }; defer { if (writer.failed) { this.failed = true; } } if (comptime Format.canHaveCircularReferences()) { if (!this.stack_check.isSafeToRecurse()) { this.failed = true; if (this.can_throw_stack_overflow) { this.globalThis.throwStackOverflow(); } return; } if (this.map_node == null) { this.map_node = Visited.Pool.get(default_allocator); this.map_node.?.data.clearRetainingCapacity(); this.map = this.map_node.?.data; } const entry = this.map.getOrPut(value) catch unreachable; if (entry.found_existing) { writer.writeAll(comptime Output.prettyFmt("[Circular]", enable_ansi_colors)); return; } } defer { if (comptime Format.canHaveCircularReferences()) { _ = this.map.remove(value); } } switch (comptime Format) { .StringPossiblyFormatted => { var str = value.toSlice(this.globalThis, bun.default_allocator); defer str.deinit(); this.addForNewLine(str.len); const slice = str.slice(); try this.writeWithFormatting(Writer, writer_, @TypeOf(slice), slice, this.globalThis, enable_ansi_colors); }, .String => { // This is called from the '%s' formatter, so it can actually be any value const str: bun.String = try bun.String.fromJS2(value, this.globalThis); defer str.deref(); this.addForNewLine(str.length()); if (this.quote_strings and jsType != .RegExpObject) { if (str.isEmpty()) { writer.writeAll("\"\""); return; } if (comptime enable_ansi_colors) { writer.writeAll(Output.prettyFmt("", true)); } defer if (comptime enable_ansi_colors) writer.writeAll(Output.prettyFmt("", true)); if (str.isUTF16()) { try this.printAs(.JSON, Writer, writer_, value, .StringObject, enable_ansi_colors); return; } JSPrinter.writeJSONString(str.latin1(), Writer, writer_, .latin1) catch unreachable; return; } if (jsType == .RegExpObject and enable_ansi_colors) { writer.print(comptime Output.prettyFmt("", enable_ansi_colors), .{}); } if (str.isUTF16()) { // streaming print writer.print("{}", .{str}); } else if (str.asUTF8()) |slice| { // fast path writer.writeAll(slice); } else if (!str.isEmpty()) { // slow path const buf = strings.allocateLatin1IntoUTF8(bun.default_allocator, []const u8, str.latin1()) catch &[_]u8{}; if (buf.len > 0) { defer bun.default_allocator.free(buf); writer.writeAll(buf); } } if (jsType == .RegExpObject and enable_ansi_colors) { writer.print(comptime Output.prettyFmt("", enable_ansi_colors), .{}); } }, .Integer => { const int = value.coerce(i64, this.globalThis); if (int < std.math.maxInt(u32)) { var i = int; const is_negative = i < 0; if (is_negative) { i = -i; } const digits = if (i != 0) bun.fmt.fastDigitCount(@as(usize, @intCast(i))) + @as(usize, @intFromBool(is_negative)) else 1; this.addForNewLine(digits); } else { this.addForNewLine(bun.fmt.count("{d}", .{int})); } writer.print(comptime Output.prettyFmt("{d}", enable_ansi_colors), .{int}); }, .BigInt => { const out_str = value.getZigString(this.globalThis).slice(); this.addForNewLine(out_str.len); writer.print(comptime Output.prettyFmt("{s}n", enable_ansi_colors), .{out_str}); }, .Double => { if (value.isCell()) { var number_name = ZigString.Empty; value.getClassName(this.globalThis, &number_name); var number_value = ZigString.Empty; value.toZigString(&number_value, this.globalThis); if (!strings.eqlComptime(number_name.slice(), "Number")) { this.addForNewLine(number_name.len + number_value.len + "[Number ():]".len); writer.print(comptime Output.prettyFmt("[Number ({s}): {s}]", enable_ansi_colors), .{ number_name, number_value, }); return; } this.addForNewLine(number_name.len + number_value.len + 4); writer.print(comptime Output.prettyFmt("[{s}: {s}]", enable_ansi_colors), .{ number_name, number_value, }); return; } const num = value.asNumber(); if (std.math.isPositiveInf(num)) { this.addForNewLine("Infinity".len); writer.print(comptime Output.prettyFmt("Infinity", enable_ansi_colors), .{}); } else if (std.math.isNegativeInf(num)) { this.addForNewLine("-Infinity".len); writer.print(comptime Output.prettyFmt("-Infinity", enable_ansi_colors), .{}); } else if (std.math.isNan(num)) { this.addForNewLine("NaN".len); writer.print(comptime Output.prettyFmt("NaN", enable_ansi_colors), .{}); } else { var buf: [124]u8 = undefined; const formatted = bun.fmt.FormatDouble.dtoaWithNegativeZero(&buf, num); this.addForNewLine(formatted.len); writer.print(comptime Output.prettyFmt("{s}", enable_ansi_colors), .{formatted}); } }, .Undefined => { this.addForNewLine(9); writer.print(comptime Output.prettyFmt("undefined", enable_ansi_colors), .{}); }, .Null => { this.addForNewLine(4); writer.print(comptime Output.prettyFmt("null", enable_ansi_colors), .{}); }, .CustomFormattedObject => { var is_exception = false; // Call custom inspect function. Will return the error if there is one // we'll need to pass the callback through to the "this" value in here const result = JSC__JSValue__callCustomInspectFunction( this.globalThis, this.custom_formatted_object.function, this.custom_formatted_object.this, this.max_depth -| this.depth, this.max_depth, enable_ansi_colors, &is_exception, ); if (is_exception) { return error.JSError; } // Strings are printed directly, otherwise we recurse. It is possible to end up in an infinite loop. if (result.isString()) { writer.print("{}", .{result.fmtString(this.globalThis)}); } else { try this.format(ConsoleObject.Formatter.Tag.get(result, this.globalThis), Writer, writer_, result, this.globalThis, enable_ansi_colors); } }, .Symbol => { const description = value.getDescription(this.globalThis); this.addForNewLine("Symbol".len); if (description.len > 0) { this.addForNewLine(description.len + "()".len); writer.print(comptime Output.prettyFmt("Symbol({any})", enable_ansi_colors), .{description}); } else { writer.print(comptime Output.prettyFmt("Symbol", enable_ansi_colors), .{}); } }, .Error => { VirtualMachine.get().printErrorlikeObject( value, null, null, this, Writer, writer_, enable_ansi_colors, false, ); }, .Class => { var printable = ZigString.init(&name_buf); value.getClassName(this.globalThis, &printable); this.addForNewLine(printable.len); const proto = value.getPrototype(this.globalThis); var printable_proto = ZigString.init(&name_buf); proto.getClassName(this.globalThis, &printable_proto); this.addForNewLine(printable_proto.len); if (printable.len == 0) { if (printable_proto.isEmpty()) { writer.print(comptime Output.prettyFmt("[class (anonymous)]", enable_ansi_colors), .{}); } else { writer.print(comptime Output.prettyFmt("[class (anonymous) extends {}]", enable_ansi_colors), .{printable_proto}); } } else { if (printable_proto.isEmpty()) { writer.print(comptime Output.prettyFmt("[class {}]", enable_ansi_colors), .{printable}); } else { writer.print(comptime Output.prettyFmt("[class {} extends {}]", enable_ansi_colors), .{ printable, printable_proto }); } } }, .Function => { var printable = value.getName(this.globalThis); defer printable.deref(); const proto = value.getPrototype(this.globalThis); const func_name = proto.getName(this.globalThis); // "Function" | "AsyncFunction" | "GeneratorFunction" | "AsyncGeneratorFunction" defer func_name.deref(); if (printable.isEmpty() or func_name.eql(printable)) { if (func_name.isEmpty()) { writer.print(comptime Output.prettyFmt("[Function]", enable_ansi_colors), .{}); } else { writer.print(comptime Output.prettyFmt("[{}]", enable_ansi_colors), .{func_name}); } } else { if (func_name.isEmpty()) { writer.print(comptime Output.prettyFmt("[Function: {}]", enable_ansi_colors), .{printable}); } else { writer.print(comptime Output.prettyFmt("[{}: {}]", enable_ansi_colors), .{ func_name, printable }); } } }, .GetterSetter => { const cell = value.asCell(); const getterSetter = cell.getGetterSetter(); const hasGetter = !getterSetter.isGetterNull(); const hasSetter = !getterSetter.isSetterNull(); if (hasGetter and hasSetter) { writer.print(comptime Output.prettyFmt("[Getter/Setter]", enable_ansi_colors), .{}); } else if (hasGetter) { writer.print(comptime Output.prettyFmt("[Getter]", enable_ansi_colors), .{}); } else if (hasSetter) { writer.print(comptime Output.prettyFmt("[Setter]", enable_ansi_colors), .{}); } }, .CustomGetterSetter => { const cell = value.asCell(); const getterSetter = cell.getCustomGetterSetter(); const hasGetter = !getterSetter.isGetterNull(); const hasSetter = !getterSetter.isSetterNull(); if (hasGetter and hasSetter) { writer.print(comptime Output.prettyFmt("[Getter/Setter]", enable_ansi_colors), .{}); } else if (hasGetter) { writer.print(comptime Output.prettyFmt("[Getter]", enable_ansi_colors), .{}); } else if (hasSetter) { writer.print(comptime Output.prettyFmt("[Setter]", enable_ansi_colors), .{}); } }, .Array => { const len = value.getLength(this.globalThis); // TODO: DerivedArray does not get passed along in JSType, and it's not clear why. // if (jsType == .DerivedArray) { // var printable = value.className(this.globalThis); // if (!printable.isEmpty()) { // writer.print(comptime Output.prettyFmt("{}({d}) ", enable_ansi_colors), .{ printable, len }); // } // } if (len == 0) { writer.writeAll("[]"); this.addForNewLine(2); return; } var was_good_time = this.always_newline_scope or // heuristic: more than 10, probably should have a newline before it len > 10; { this.indent += 1; this.depth += 1; defer this.depth -|= 1; defer this.indent -|= 1; this.addForNewLine(2); const prev_quote_strings = this.quote_strings; this.quote_strings = true; defer this.quote_strings = prev_quote_strings; var empty_start: ?u32 = null; first: { const element = value.getDirectIndex(this.globalThis, 0); const tag = Tag.getAdvanced(element, this.globalThis, .{ .hide_global = true, .disable_inspect_custom = this.disable_inspect_custom, }); was_good_time = was_good_time or !tag.tag.isPrimitive() or this.goodTimeForANewLine(); if (!this.single_line and (this.ordered_properties or was_good_time)) { this.resetLine(); writer.writeAll("["); writer.writeAll("\n"); this.writeIndent(Writer, writer_) catch unreachable; this.addForNewLine(1); } else { writer.writeAll("[ "); this.addForNewLine(2); } if (element == .zero) { empty_start = 0; break :first; } try this.format(tag, Writer, writer_, element, this.globalThis, enable_ansi_colors); if (tag.cell.isStringLike()) { if (comptime enable_ansi_colors) { writer.writeAll(comptime Output.prettyFmt("", true)); } } } var i: u32 = 1; var nonempty_count: u32 = 1; while (i < len) : (i += 1) { const element = value.getDirectIndex(this.globalThis, i); if (element == .zero) { if (empty_start == null) { empty_start = i; } continue; } if (nonempty_count >= 100) { this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; writer.writeAll("\n"); // we want the line break to be unconditional here this.estimated_line_length = 0; this.writeIndent(Writer, writer_) catch unreachable; writer.pretty("... {d} more items", enable_ansi_colors, .{len - i}); break; } nonempty_count += 1; if (empty_start) |empty| { if (empty > 0) { this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; if (!this.single_line and (this.ordered_properties or this.goodTimeForANewLine())) { was_good_time = true; writer.writeAll("\n"); this.writeIndent(Writer, writer_) catch unreachable; } else { writer.space(); } } const empty_count = i - empty; if (empty_count == 1) { writer.pretty("empty item", enable_ansi_colors, .{}); } else { this.estimated_line_length += bun.fmt.fastDigitCount(empty_count); writer.pretty("{d} x empty items", enable_ansi_colors, .{empty_count}); } empty_start = null; } this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; if (!this.single_line and (this.ordered_properties or this.goodTimeForANewLine())) { writer.writeAll("\n"); was_good_time = true; this.writeIndent(Writer, writer_) catch unreachable; } else { writer.space(); } const tag = Tag.getAdvanced(element, this.globalThis, .{ .hide_global = true, .disable_inspect_custom = this.disable_inspect_custom, }); try this.format(tag, Writer, writer_, element, this.globalThis, enable_ansi_colors); if (tag.cell.isStringLike()) { if (comptime enable_ansi_colors) { writer.writeAll(comptime Output.prettyFmt("", true)); } } } if (empty_start) |empty| { if (empty > 0) { this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; if (!this.single_line and (this.ordered_properties or this.goodTimeForANewLine())) { writer.writeAll("\n"); was_good_time = true; this.writeIndent(Writer, writer_) catch unreachable; } else { writer.space(); } } empty_start = null; const empty_count = len - empty; if (empty_count == 1) { writer.pretty("empty item", enable_ansi_colors, .{}); } else { this.estimated_line_length += bun.fmt.fastDigitCount(empty_count); writer.pretty("{d} x empty items", enable_ansi_colors, .{empty_count}); } } if (!jsType.isArguments()) { const Iterator = PropertyIterator(Writer, enable_ansi_colors); var iter = Iterator{ .formatter = this, .writer = writer_, .always_newline = !this.single_line and (this.always_newline_scope or this.goodTimeForANewLine()), .single_line = this.single_line, .parent = value, .i = i, }; value.forEachPropertyNonIndexed(this.globalThis, &iter, Iterator.forEach); if (this.globalThis.hasException()) { return error.JSError; } if (this.failed) return; } } if (!this.single_line and (this.ordered_properties or was_good_time or this.goodTimeForANewLine())) { this.resetLine(); writer.writeAll("\n"); this.writeIndent(Writer, writer_) catch {}; writer.writeAll("]"); this.resetLine(); this.addForNewLine(1); } else { writer.writeAll(" ]"); this.addForNewLine(2); } }, .Private => { if (value.as(JSC.WebCore.Response)) |response| { response.writeFormat(ConsoleObject.Formatter, this, writer_, enable_ansi_colors) catch {}; return; } else if (value.as(JSC.WebCore.Request)) |request| { request.writeFormat(ConsoleObject.Formatter, this, writer_, enable_ansi_colors) catch {}; return; } else if (value.as(JSC.API.BuildArtifact)) |build| { build.writeFormat(ConsoleObject.Formatter, this, writer_, enable_ansi_colors) catch {}; return; } else if (value.as(JSC.WebCore.Blob)) |blob| { blob.writeFormat(ConsoleObject.Formatter, this, writer_, enable_ansi_colors) catch {}; return; } else if (value.as(JSC.FetchHeaders) != null) { if (value.get_unsafe(this.globalThis, "toJSON")) |toJSONFunction| { this.addForNewLine("Headers ".len); writer.writeAll(comptime Output.prettyFmt("Headers ", enable_ansi_colors)); const prev_quote_keys = this.quote_keys; this.quote_keys = true; defer this.quote_keys = prev_quote_keys; return try this.printAs( .Object, Writer, writer_, toJSONFunction.call(this.globalThis, value, &.{}) catch |err| this.globalThis.takeException(err), .Object, enable_ansi_colors, ); } } else if (value.as(JSC.DOMFormData) != null) { if (value.get_unsafe(this.globalThis, "toJSON")) |toJSONFunction| { const prev_quote_keys = this.quote_keys; this.quote_keys = true; defer this.quote_keys = prev_quote_keys; return try this.printAs( .Object, Writer, writer_, toJSONFunction.call(this.globalThis, value, &.{}) catch |err| this.globalThis.takeException(err), .Object, enable_ansi_colors, ); } // this case should never happen return try this.printAs(.Undefined, Writer, writer_, .undefined, .Cell, enable_ansi_colors); } else if (value.as(JSC.API.Bun.Timer.TimerObject)) |timer| { this.addForNewLine("Timeout(# ) ".len + bun.fmt.fastDigitCount(@as(u64, @intCast(@max(timer.id, 0))))); if (timer.kind == .setInterval) { this.addForNewLine("repeats ".len + bun.fmt.fastDigitCount(@as(u64, @intCast(@max(timer.id, 0))))); writer.print(comptime Output.prettyFmt("Timeout (#{d}, repeats)", enable_ansi_colors), .{ timer.id, }); } else { writer.print(comptime Output.prettyFmt("Timeout (#{d})", enable_ansi_colors), .{ timer.id, }); } return; } else if (value.as(JSC.BuildMessage)) |build_log| { build_log.msg.writeFormat(writer_, enable_ansi_colors) catch {}; return; } else if (value.as(JSC.ResolveMessage)) |resolve_log| { resolve_log.msg.writeFormat(writer_, enable_ansi_colors) catch {}; return; } else if (JestPrettyFormat.printAsymmetricMatcher(this, Format, &writer, writer_, name_buf, value, enable_ansi_colors)) { return; } else if (jsType != .DOMWrapper) { if (value.isCallable(this.globalThis.vm())) { return try this.printAs(.Function, Writer, writer_, value, jsType, enable_ansi_colors); } return try this.printAs(.Object, Writer, writer_, value, jsType, enable_ansi_colors); } return try this.printAs(.Object, Writer, writer_, value, .Event, enable_ansi_colors); }, .NativeCode => { if (value.getClassInfoName()) |class_name| { this.addForNewLine("[native code: ]".len + class_name.len); writer.writeAll("[native code: "); writer.writeAll(class_name); writer.writeAll("]"); } else { this.addForNewLine("[native code]".len); writer.writeAll("[native code]"); } }, .Promise => { if (!this.single_line and this.goodTimeForANewLine()) { writer.writeAll("\n"); this.writeIndent(Writer, writer_) catch {}; } writer.writeAll("Promise { " ++ comptime Output.prettyFmt("", enable_ansi_colors)); switch (JSPromise.status(@as(*JSPromise, @ptrCast(value.asObjectRef().?)), this.globalThis.vm())) { .pending => writer.writeAll(""), .fulfilled => writer.writeAll(""), .rejected => writer.writeAll(""), } writer.writeAll(comptime Output.prettyFmt("", enable_ansi_colors) ++ " }"); }, .Boolean => { if (value.isCell()) { var bool_name = ZigString.Empty; value.getClassName(this.globalThis, &bool_name); var bool_value = ZigString.Empty; value.toZigString(&bool_value, this.globalThis); if (!strings.eqlComptime(bool_name.slice(), "Boolean")) { this.addForNewLine(bool_value.len + bool_name.len + "[Boolean (): ]".len); writer.print(comptime Output.prettyFmt("[Boolean ({s}): {s}]", enable_ansi_colors), .{ bool_name, bool_value, }); return; } this.addForNewLine(bool_value.len + "[Boolean: ]".len); writer.print(comptime Output.prettyFmt("[Boolean: {s}]", enable_ansi_colors), .{bool_value}); return; } if (value.toBoolean()) { this.addForNewLine(4); writer.writeAll(comptime Output.prettyFmt("true", enable_ansi_colors)); } else { this.addForNewLine(5); writer.writeAll(comptime Output.prettyFmt("false", enable_ansi_colors)); } }, .GlobalObject => { const fmt = "[Global Object]"; this.addForNewLine(fmt.len); writer.writeAll(comptime Output.prettyFmt("" ++ fmt ++ "", enable_ansi_colors)); }, .Map => { const length_value = value.get_unsafe(this.globalThis, "size") orelse JSC.JSValue.jsNumberFromInt32(0); const length = length_value.toInt32(); const prev_quote_strings = this.quote_strings; this.quote_strings = true; defer this.quote_strings = prev_quote_strings; const map_name = if (value.jsType() == .WeakMap) "WeakMap" else "Map"; if (length == 0) { return writer.print("{s} {{}}", .{map_name}); } switch (this.single_line) { inline else => |single_line| { writer.print("{s}({d}) {{" ++ (if (single_line) " " else "\n"), .{ map_name, length }); }, } { this.indent += 1; this.depth +|= 1; defer this.indent -|= 1; defer this.depth -|= 1; switch (this.single_line) { inline else => |single_line| { var iter = MapIterator(Writer, enable_ansi_colors, false, single_line){ .formatter = this, .writer = writer_, }; value.forEach(this.globalThis, &iter, @TypeOf(iter).forEach); if (this.failed) return; if (single_line and iter.count > 0) { writer.writeAll(" "); } }, } } if (!this.single_line) { this.writeIndent(Writer, writer_) catch {}; } writer.writeAll("}"); }, .MapIterator => { const prev_quote_strings = this.quote_strings; this.quote_strings = true; defer this.quote_strings = prev_quote_strings; writer.print("MapIterator {{ ", .{}); { this.indent += 1; this.depth +|= 1; defer this.indent -|= 1; defer this.depth -|= 1; switch (this.single_line) { inline else => |single_line| { var iter = MapIterator(Writer, enable_ansi_colors, true, single_line){ .formatter = this, .writer = writer_, }; value.forEach(this.globalThis, &iter, @TypeOf(iter).forEach); if (this.failed) return; if (iter.count > 0) { if (single_line) { writer.writeAll(" "); } else { writer.writeAll("\n"); } } }, } } if (!this.single_line) { this.writeIndent(Writer, writer_) catch {}; } writer.writeAll("}"); }, .SetIterator => { const prev_quote_strings = this.quote_strings; this.quote_strings = true; defer this.quote_strings = prev_quote_strings; writer.print("SetIterator {{ ", .{}); { this.indent += 1; this.depth +|= 1; defer this.indent -|= 1; defer this.depth -|= 1; switch (this.single_line) { inline else => |single_line| { var iter = MapIterator(Writer, enable_ansi_colors, true, single_line){ .formatter = this, .writer = writer_, }; value.forEach(this.globalThis, &iter, @TypeOf(iter).forEach); if (this.failed) return; if (iter.count > 0 and !single_line) { writer.writeAll("\n"); } }, } } if (!this.single_line) { this.writeIndent(Writer, writer_) catch {}; } writer.writeAll("}"); }, .Set => { const length_value = value.get_unsafe(this.globalThis, "size") orelse JSC.JSValue.jsNumberFromInt32(0); const length = length_value.toInt32(); const prev_quote_strings = this.quote_strings; this.quote_strings = true; defer this.quote_strings = prev_quote_strings; const set_name = if (value.jsType() == .WeakSet) "WeakSet" else "Set"; if (length == 0) { return writer.print("{s} {{}}", .{set_name}); } switch (this.single_line) { inline else => |single_line| { writer.print("{s}({d}) {{" ++ (if (single_line) " " else "\n"), .{ set_name, length }); }, } { this.indent += 1; this.depth +|= 1; defer this.indent -|= 1; defer this.depth -|= 1; switch (this.single_line) { inline else => |single_line| { var iter = SetIterator(Writer, enable_ansi_colors, single_line){ .formatter = this, .writer = writer_, }; value.forEach(this.globalThis, &iter, @TypeOf(iter).forEach); if (this.failed) return; if (single_line and !iter.is_first) { writer.writeAll(" "); } }, } } if (!this.single_line) { this.writeIndent(Writer, writer_) catch {}; } writer.writeAll("}"); }, .toJSON => { if (value.get_unsafe(this.globalThis, "toJSON")) |func| brk: { const result = func.call(this.globalThis, value, &.{}) catch { this.globalThis.clearException(); break :brk; }; const prev_quote_keys = this.quote_keys; this.quote_keys = true; defer this.quote_keys = prev_quote_keys; try this.printAs(.Object, Writer, writer_, result, value.jsType(), enable_ansi_colors); return; } writer.writeAll("{}"); }, .JSON => { var str = bun.String.empty; defer str.deref(); value.jsonStringify(this.globalThis, this.indent, &str); this.addForNewLine(str.length()); if (jsType == JSValue.JSType.JSDate) { // in the code for printing dates, it never exceeds this amount var iso_string_buf: [36]u8 = undefined; var out_buf: []const u8 = std.fmt.bufPrint(&iso_string_buf, "{}", .{str}) catch ""; if (strings.eql(out_buf, "null")) { out_buf = "Invalid Date"; } else if (out_buf.len > 2) { // trim the quotes out_buf = out_buf[1 .. out_buf.len - 1]; } writer.print(comptime Output.prettyFmt("{s}", enable_ansi_colors), .{out_buf}); return; } writer.print("{}", .{str}); }, .Event => { const event_type_value = brk: { const value_ = value.get_unsafe(this.globalThis, "type") orelse break :brk JSValue.undefined; if (value_.isString()) { break :brk value_; } break :brk JSValue.undefined; }; const event_type = switch (EventType.map.fromJS(this.globalThis, event_type_value) orelse .unknown) { .MessageEvent, .ErrorEvent => |evt| evt, else => { return try this.printAs(.Object, Writer, writer_, value, .Event, enable_ansi_colors); }, }; writer.print( comptime Output.prettyFmt("{s} {{\n", enable_ansi_colors), .{ @tagName(event_type), }, ); { this.indent += 1; this.depth +|= 1; defer this.indent -|= 1; defer this.depth -|= 1; const old_quote_strings = this.quote_strings; this.quote_strings = true; defer this.quote_strings = old_quote_strings; this.writeIndent(Writer, writer_) catch unreachable; switch (this.single_line) { inline else => |single_line| { writer.print( comptime Output.prettyFmt("type: \"{s}\"," ++ (if (single_line) " " else "\n"), enable_ansi_colors), .{ event_type.label(), }, ); }, } if (value.fastGet(this.globalThis, .message)) |message_value| { if (message_value.isString()) { if (!this.single_line) { this.writeIndent(Writer, writer_) catch unreachable; } writer.print( comptime Output.prettyFmt("message: ", enable_ansi_colors), .{}, ); const tag = Tag.getAdvanced(message_value, this.globalThis, .{ .hide_global = true, .disable_inspect_custom = this.disable_inspect_custom, }); try this.format(tag, Writer, writer_, message_value, this.globalThis, enable_ansi_colors); if (this.failed) return; this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; if (!this.single_line) { writer.writeAll("\n"); } } } switch (event_type) { .MessageEvent => { if (!this.single_line) { this.writeIndent(Writer, writer_) catch unreachable; } writer.print( comptime Output.prettyFmt("data: ", enable_ansi_colors), .{}, ); const data = value.fastGet(this.globalThis, .data) orelse JSValue.undefined; const tag = Tag.getAdvanced(data, this.globalThis, .{ .hide_global = true, .disable_inspect_custom = this.disable_inspect_custom, }); try this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); if (this.failed) return; this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; if (!this.single_line) { writer.writeAll("\n"); } }, .ErrorEvent => { if (value.fastGet(this.globalThis, .@"error")) |error_value| { if (!this.single_line) { this.writeIndent(Writer, writer_) catch unreachable; } writer.print( comptime Output.prettyFmt("error: ", enable_ansi_colors), .{}, ); const tag = Tag.getAdvanced(error_value, this.globalThis, .{ .hide_global = true, .disable_inspect_custom = this.disable_inspect_custom, }); try this.format(tag, Writer, writer_, error_value, this.globalThis, enable_ansi_colors); if (this.failed) return; this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; if (!this.single_line) { writer.writeAll("\n"); } } }, else => unreachable, } } if (!this.single_line) { this.writeIndent(Writer, writer_) catch unreachable; } writer.writeAll("}"); }, .JSX => { writer.writeAll(comptime Output.prettyFmt("", enable_ansi_colors)); writer.writeAll("<"); var needs_space = false; var tag_name_str = ZigString.init(""); var tag_name_slice: ZigString.Slice = ZigString.Slice.empty; var is_tag_kind_primitive = false; defer if (tag_name_slice.isAllocated()) tag_name_slice.deinit(); if (value.get_unsafe(this.globalThis, "type")) |type_value| { const _tag = Tag.getAdvanced(type_value, this.globalThis, .{ .hide_global = true, .disable_inspect_custom = this.disable_inspect_custom, }); if (_tag.cell == .Symbol) {} else if (_tag.cell.isStringLike()) { type_value.toZigString(&tag_name_str, this.globalThis); is_tag_kind_primitive = true; } else if (_tag.cell.isObject() or type_value.isCallable(this.globalThis.vm())) { type_value.getNameProperty(this.globalThis, &tag_name_str); if (tag_name_str.len == 0) { tag_name_str = ZigString.init("NoName"); } } else { type_value.toZigString(&tag_name_str, this.globalThis); } tag_name_slice = tag_name_str.toSlice(default_allocator); needs_space = true; } else { tag_name_slice = ZigString.init("unknown").toSlice(default_allocator); needs_space = true; } if (!is_tag_kind_primitive) writer.writeAll(comptime Output.prettyFmt("", enable_ansi_colors)) else writer.writeAll(comptime Output.prettyFmt("", enable_ansi_colors)); writer.writeAll(tag_name_slice.slice()); if (enable_ansi_colors) writer.writeAll(comptime Output.prettyFmt("", enable_ansi_colors)); if (value.get_unsafe(this.globalThis, "key")) |key_value| { if (!key_value.isUndefinedOrNull()) { if (needs_space) writer.writeAll(" key=") else writer.writeAll("key="); const old_quote_strings = this.quote_strings; this.quote_strings = true; defer this.quote_strings = old_quote_strings; try this.format(Tag.getAdvanced(key_value, this.globalThis, .{ .hide_global = true, .disable_inspect_custom = this.disable_inspect_custom, }), Writer, writer_, key_value, this.globalThis, enable_ansi_colors); needs_space = true; } } if (value.get_unsafe(this.globalThis, "props")) |props| { const prev_quote_strings = this.quote_strings; this.quote_strings = true; defer this.quote_strings = prev_quote_strings; var props_iter = JSC.JSPropertyIterator(.{ .skip_empty_name = true, .include_value = true, }).init(this.globalThis, props); defer props_iter.deinit(); const children_prop = props.get_unsafe(this.globalThis, "children"); if (props_iter.len > 0) { { this.indent += 1; defer this.indent -|= 1; const count_without_children = props_iter.len - @as(usize, @intFromBool(children_prop != null)); while (props_iter.next()) |prop| { if (prop.eqlComptime("children")) continue; const property_value = props_iter.value; const tag = Tag.getAdvanced(property_value, this.globalThis, .{ .hide_global = true, .disable_inspect_custom = this.disable_inspect_custom, }); if (tag.cell.isHidden()) continue; if (needs_space) writer.space(); needs_space = false; writer.print( comptime Output.prettyFmt("{s}=", enable_ansi_colors), .{prop.trunc(128)}, ); if (tag.cell.isStringLike()) { if (comptime enable_ansi_colors) { writer.writeAll(comptime Output.prettyFmt("", true)); } } try this.format(tag, Writer, writer_, property_value, this.globalThis, enable_ansi_colors); if (tag.cell.isStringLike()) { if (comptime enable_ansi_colors) { writer.writeAll(comptime Output.prettyFmt("", true)); } } if (!this.single_line and ( // count_without_children is necessary to prevent printing an extra newline // if there are children and one prop and the child prop is the last prop props_iter.i + 1 < count_without_children and // 3 is arbitrary but basically // // ^ should be one line // // ^ should be multiple lines props_iter.i > 3)) { writer.writeAll("\n"); this.writeIndent(Writer, writer_) catch unreachable; } else if (props_iter.i + 1 < count_without_children) { writer.space(); } } } if (children_prop) |children| { const tag = Tag.get(children, this.globalThis); const print_children = switch (tag.tag) { .String, .JSX, .Array => true, else => false, }; if (print_children and !this.single_line) { print_children: { switch (tag.tag) { .String => { const children_string = children.getZigString(this.globalThis); if (children_string.len == 0) break :print_children; if (comptime enable_ansi_colors) writer.writeAll(comptime Output.prettyFmt("", true)); writer.writeAll(">"); if (children_string.len < 128) { writer.writeString(children_string); } else { this.indent += 1; writer.writeAll("\n"); this.writeIndent(Writer, writer_) catch unreachable; this.indent -|= 1; writer.writeString(children_string); writer.writeAll("\n"); this.writeIndent(Writer, writer_) catch unreachable; } }, .JSX => { writer.writeAll(">\n"); { this.indent += 1; this.writeIndent(Writer, writer_) catch unreachable; defer this.indent -|= 1; try this.format(Tag.get(children, this.globalThis), Writer, writer_, children, this.globalThis, enable_ansi_colors); } writer.writeAll("\n"); this.writeIndent(Writer, writer_) catch unreachable; }, .Array => { const length = children.getLength(this.globalThis); if (length == 0) break :print_children; writer.writeAll(">\n"); { this.indent += 1; this.writeIndent(Writer, writer_) catch unreachable; const _prev_quote_strings = this.quote_strings; this.quote_strings = false; defer this.quote_strings = _prev_quote_strings; defer this.indent -|= 1; var j: usize = 0; while (j < length) : (j += 1) { const child = children.getIndex(this.globalThis, @as(u32, @intCast(j))); try this.format(Tag.getAdvanced(child, this.globalThis, .{ .hide_global = true, .disable_inspect_custom = this.disable_inspect_custom, }), Writer, writer_, child, this.globalThis, enable_ansi_colors); if (j + 1 < length) { writer.writeAll("\n"); this.writeIndent(Writer, writer_) catch unreachable; } } } writer.writeAll("\n"); this.writeIndent(Writer, writer_) catch unreachable; }, else => unreachable, } writer.writeAll("", enable_ansi_colors)) else writer.writeAll(comptime Output.prettyFmt("", enable_ansi_colors)); writer.writeAll(tag_name_slice.slice()); if (enable_ansi_colors) writer.writeAll(comptime Output.prettyFmt("", enable_ansi_colors)); writer.writeAll(">"); } return; } } } } writer.writeAll(" />"); }, .Object => { bun.assert(value.isCell()); const prev_quote_strings = this.quote_strings; this.quote_strings = true; defer this.quote_strings = prev_quote_strings; const Iterator = PropertyIterator(Writer, enable_ansi_colors); // We want to figure out if we should print this object // on one line or multiple lines // // The 100% correct way would be to print everything to // a temporary buffer and then check how long each line was // // But it's important that console.log() is fast. So we // do a small compromise to avoid multiple passes over input // // We say: // // If the object has at least 2 properties and ANY of the following conditions are met: // - total length of all the property names is more than // 14 characters // - the parent object is printing each property on a new line // - The first property is a DOM object, ESM namespace, Map, Set, or Blob // // Then, we print it each property on a new line, recursively. // const prev_always_newline_scope = this.always_newline_scope; defer this.always_newline_scope = prev_always_newline_scope; var iter = Iterator{ .formatter = this, .writer = writer_, .always_newline = !this.single_line and (this.always_newline_scope or this.goodTimeForANewLine()), .single_line = this.single_line, .parent = value, }; if (this.depth > this.max_depth) { if (this.single_line) { writer.writeAll(" "); } else if (this.always_newline_scope or this.goodTimeForANewLine()) { writer.writeAll("\n"); this.writeIndent(Writer, writer_) catch {}; this.resetLine(); } var display_name = value.getName(this.globalThis); if (display_name.isEmpty()) { display_name = String.static("Object"); } writer.print(comptime Output.prettyFmt("[{} ...]", enable_ansi_colors), .{ display_name, }); return; } else if (this.ordered_properties) { value.forEachPropertyOrdered(this.globalThis, &iter, Iterator.forEach); } else { value.forEachProperty(this.globalThis, &iter, Iterator.forEach); } if (this.globalThis.hasException()) { return error.JSError; } if (this.failed) return; if (iter.i == 0) { if (value.isClass(this.globalThis)) try this.printAs(.Class, Writer, writer_, value, jsType, enable_ansi_colors) else if (value.isCallable(this.globalThis.vm())) try this.printAs(.Function, Writer, writer_, value, jsType, enable_ansi_colors) else { if (getObjectName(this.globalThis, value)) |name_str| { writer.print("{} ", .{name_str}); } writer.writeAll("{}"); } } else { this.depth -= 1; if (iter.always_newline) { this.indent -|= 1; this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; writer.writeAll("\n"); this.writeIndent(Writer, writer_) catch {}; writer.writeAll("}"); this.estimated_line_length += 1; } else { this.estimated_line_length += 2; writer.writeAll(" }"); } } }, .TypedArray => { const arrayBuffer = value.asArrayBuffer(this.globalThis).?; const slice = arrayBuffer.byteSlice(); writer.writeAll( if (arrayBuffer.typed_array_type == .Uint8Array and arrayBuffer.value.isBuffer(this.globalThis)) "Buffer" else bun.asByteSlice(@tagName(arrayBuffer.typed_array_type)), ); writer.print("({d}) [ ", .{arrayBuffer.len}); if (slice.len > 0) { switch (jsType) { .Int8Array => this.writeTypedArray( *@TypeOf(writer), &writer, i8, @as([]align(std.meta.alignment([]i8)) i8, @alignCast(std.mem.bytesAsSlice(i8, slice))), enable_ansi_colors, ), .Int16Array => this.writeTypedArray( *@TypeOf(writer), &writer, i16, @as([]align(std.meta.alignment([]i16)) i16, @alignCast(std.mem.bytesAsSlice(i16, slice))), enable_ansi_colors, ), .Uint16Array => this.writeTypedArray( *@TypeOf(writer), &writer, u16, @as([]align(std.meta.alignment([]u16)) u16, @alignCast(std.mem.bytesAsSlice(u16, slice))), enable_ansi_colors, ), .Int32Array => this.writeTypedArray( *@TypeOf(writer), &writer, i32, @as([]align(std.meta.alignment([]i32)) i32, @alignCast(std.mem.bytesAsSlice(i32, slice))), enable_ansi_colors, ), .Uint32Array => this.writeTypedArray( *@TypeOf(writer), &writer, u32, @as([]align(std.meta.alignment([]u32)) u32, @alignCast(std.mem.bytesAsSlice(u32, slice))), enable_ansi_colors, ), .Float16Array => this.writeTypedArray( *@TypeOf(writer), &writer, f16, @as([]align(std.meta.alignment([]f16)) f16, @alignCast(std.mem.bytesAsSlice(f16, slice))), enable_ansi_colors, ), .Float32Array => this.writeTypedArray( *@TypeOf(writer), &writer, f32, @as([]align(std.meta.alignment([]f32)) f32, @alignCast(std.mem.bytesAsSlice(f32, slice))), enable_ansi_colors, ), .Float64Array => this.writeTypedArray( *@TypeOf(writer), &writer, f64, @as([]align(std.meta.alignment([]f64)) f64, @alignCast(std.mem.bytesAsSlice(f64, slice))), enable_ansi_colors, ), .BigInt64Array => this.writeTypedArray( *@TypeOf(writer), &writer, i64, @as([]align(std.meta.alignment([]i64)) i64, @alignCast(std.mem.bytesAsSlice(i64, slice))), enable_ansi_colors, ), .BigUint64Array => { this.writeTypedArray( *@TypeOf(writer), &writer, u64, @as([]align(std.meta.alignment([]u64)) u64, @alignCast(std.mem.bytesAsSlice(u64, slice))), enable_ansi_colors, ); }, // Uint8Array, Uint8ClampedArray, DataView, ArrayBuffer else => this.writeTypedArray(*@TypeOf(writer), &writer, u8, slice, enable_ansi_colors), } } writer.writeAll(" ]"); }, .RevokedProxy => { this.addForNewLine("".len); writer.print(comptime Output.prettyFmt("\\", enable_ansi_colors), .{}); }, .Proxy => { const target = value.getProxyInternalField(.target); if (Environment.allow_assert) { // Proxy does not allow non-objects here. bun.assert(target.isCell()); } // TODO: if (options.showProxy), print like `Proxy { target: ..., handlers: ... }` // this is default off so it is not used. try this.format(ConsoleObject.Formatter.Tag.get(target, this.globalThis), Writer, writer_, target, this.globalThis, enable_ansi_colors); }, } } fn writeTypedArray(this: *ConsoleObject.Formatter, comptime WriterWrapped: type, writer: WriterWrapped, comptime Number: type, slice: []const Number, comptime enable_ansi_colors: bool) void { const fmt_ = if (Number == i64 or Number == u64) "{d}n" else "{d}"; const more = if (Number == i64 or Number == u64) "n, ... {d} more" else ", ... {d} more"; writer.print(comptime Output.prettyFmt(fmt_, enable_ansi_colors), .{ if (@typeInfo(Number) == .Float) bun.fmt.double(@floatCast(slice[0])) else slice[0], }); var leftover = slice[1..]; const max = 512; leftover = leftover[0..@min(leftover.len, max)]; for (leftover) |el| { this.printComma(@TypeOf(&writer.ctx), &writer.ctx, enable_ansi_colors) catch return; writer.space(); writer.print(comptime Output.prettyFmt(fmt_, enable_ansi_colors), .{ if (@typeInfo(Number) == .Float) bun.fmt.double(@floatCast(el)) else el, }); } if (slice.len > max + 1) { writer.print(comptime Output.prettyFmt(more, enable_ansi_colors), .{slice.len - max - 1}); } } pub fn format(this: *ConsoleObject.Formatter, result: Tag.Result, comptime Writer: type, writer: Writer, value: JSValue, globalThis: *JSGlobalObject, comptime enable_ansi_colors: bool) bun.JSError!void { const prevGlobalThis = this.globalThis; defer this.globalThis = prevGlobalThis; this.globalThis = globalThis; // This looks incredibly redundant. We make the ConsoleObject.Formatter.Tag a // comptime var so we have to repeat it here. The rationale there is // it _should_ limit the stack usage because each version of the // function will be relatively small switch (result.tag.tag()) { inline else => |tag| try this.printAs(tag, Writer, writer, value, result.cell, enable_ansi_colors), .CustomFormattedObject => { this.custom_formatted_object = result.tag.CustomFormattedObject; try this.printAs(.CustomFormattedObject, Writer, writer, value, result.cell, enable_ansi_colors); }, } } }; pub fn count( // console _: ConsoleObject.Type, // global globalThis: *JSGlobalObject, // chars ptr: [*]const u8, // len len: usize, ) callconv(JSC.conv) void { var this = globalThis.bunVM().console; const slice = ptr[0..len]; const hash = bun.hash(slice); // we don't want to store these strings, it will take too much memory const counter = this.counts.getOrPut(globalThis.allocator(), hash) catch unreachable; const current = @as(u32, if (counter.found_existing) counter.value_ptr.* else @as(u32, 0)) + 1; counter.value_ptr.* = current; var writer_ctx = &this.writer; var writer = &writer_ctx.writer(); if (Output.enable_ansi_colors_stdout) writer.print(comptime Output.prettyFmt("{s}: {d}\n", true), .{ slice, current }) catch unreachable else writer.print(comptime Output.prettyFmt("{s}: {d}\n", false), .{ slice, current }) catch unreachable; writer_ctx.flush() catch unreachable; } pub fn countReset( // console _: ConsoleObject.Type, // global globalThis: *JSGlobalObject, // chars ptr: [*]const u8, // len len: usize, ) callconv(JSC.conv) void { var this = globalThis.bunVM().console; const slice = ptr[0..len]; const hash = bun.hash(slice); // we don't delete it because deleting is implemented via tombstoning const entry = this.counts.getEntry(hash) orelse return; entry.value_ptr.* = 0; } const PendingTimers = std.AutoHashMap(u64, ?std.time.Timer); threadlocal var pending_time_logs: PendingTimers = undefined; threadlocal var pending_time_logs_loaded = false; pub fn time( // console _: ConsoleObject.Type, // global _: *JSGlobalObject, chars: [*]const u8, len: usize, ) callconv(JSC.conv) void { const id = bun.hash(chars[0..len]); if (!pending_time_logs_loaded) { pending_time_logs = PendingTimers.init(default_allocator); pending_time_logs_loaded = true; } const result = pending_time_logs.getOrPut(id) catch unreachable; if (!result.found_existing or (result.found_existing and result.value_ptr.* == null)) { result.value_ptr.* = std.time.Timer.start() catch unreachable; } } pub fn timeEnd( // console _: ConsoleObject.Type, // global _: *JSGlobalObject, chars: [*]const u8, len: usize, ) callconv(JSC.conv) void { if (!pending_time_logs_loaded) { return; } const id = bun.hash(chars[0..len]); const result = (pending_time_logs.fetchPut(id, null) catch null) orelse return; var value: std.time.Timer = result.value orelse return; // get the duration in microseconds // then display it in milliseconds Output.printElapsed(@as(f64, @floatFromInt(value.read() / std.time.ns_per_us)) / std.time.us_per_ms); switch (len) { 0 => Output.printErrorln("\n", .{}), else => Output.printErrorln(" {s}", .{chars[0..len]}), } Output.flush(); } pub fn timeLog( // console _: ConsoleObject.Type, // global global: *JSGlobalObject, // chars chars: [*]const u8, // len len: usize, // args args: [*]JSValue, args_len: usize, ) callconv(JSC.conv) void { if (!pending_time_logs_loaded) { return; } const id = bun.hash(chars[0..len]); var value: std.time.Timer = (pending_time_logs.get(id) orelse return) orelse return; // get the duration in microseconds // then display it in milliseconds Output.printElapsed(@as(f64, @floatFromInt(value.read() / std.time.ns_per_us)) / std.time.us_per_ms); switch (len) { 0 => {}, else => Output.printError(" {s}", .{chars[0..len]}), } Output.flush(); // print the arguments var fmt = ConsoleObject.Formatter{ .remaining_values = &[_]JSValue{}, .globalThis = global, .ordered_properties = false, .quote_strings = false, .stack_check = bun.StackCheck.init(), .can_throw_stack_overflow = true, }; var console = global.bunVM().console; var writer = console.error_writer.writer(); const Writer = @TypeOf(writer); for (args[0..args_len]) |arg| { const tag = ConsoleObject.Formatter.Tag.get(arg, global); _ = writer.write(" ") catch 0; if (Output.enable_ansi_colors_stderr) { fmt.format(tag, Writer, writer, arg, global, true) catch {}; // TODO: } else { fmt.format(tag, Writer, writer, arg, global, false) catch {}; // TODO: } } _ = writer.write("\n") catch 0; writer.context.flush() catch {}; } pub fn profile( // console _: ConsoleObject.Type, // global _: *JSGlobalObject, // chars _: [*]const u8, // len _: usize, ) callconv(JSC.conv) void {} pub fn profileEnd( // console _: ConsoleObject.Type, // global _: *JSGlobalObject, // chars _: [*]const u8, // len _: usize, ) callconv(JSC.conv) void {} pub fn takeHeapSnapshot( // console _: ConsoleObject.Type, // global globalThis: *JSGlobalObject, // chars _: [*]const u8, // len _: usize, ) callconv(JSC.conv) void { // TODO: this does an extra JSONStringify and we don't need it to! var snapshot: [1]JSValue = .{globalThis.generateHeapSnapshot()}; ConsoleObject.messageWithTypeAndLevel(undefined, MessageType.Log, MessageLevel.Debug, globalThis, &snapshot, 1); } pub fn timeStamp( // console _: ConsoleObject.Type, // global _: *JSGlobalObject, // args _: *ScriptArguments, ) callconv(JSC.conv) void {} pub fn record( // console _: ConsoleObject.Type, // global _: *JSGlobalObject, // args _: *ScriptArguments, ) callconv(JSC.conv) void {} pub fn recordEnd( // console _: ConsoleObject.Type, // global _: *JSGlobalObject, // args _: *ScriptArguments, ) callconv(JSC.conv) void {} pub fn screenshot( // console _: ConsoleObject.Type, // global _: *JSGlobalObject, // args _: *ScriptArguments, ) callconv(JSC.conv) void {} comptime { @export(messageWithTypeAndLevel, .{ .name = shim.symbolName("messageWithTypeAndLevel") }); @export(count, .{ .name = shim.symbolName("count") }); @export(countReset, .{ .name = shim.symbolName("countReset") }); @export(time, .{ .name = shim.symbolName("time") }); @export(timeLog, .{ .name = shim.symbolName("timeLog") }); @export(timeEnd, .{ .name = shim.symbolName("timeEnd") }); @export(profile, .{ .name = shim.symbolName("profile") }); @export(profileEnd, .{ .name = shim.symbolName("profileEnd") }); @export(takeHeapSnapshot, .{ .name = shim.symbolName("takeHeapSnapshot") }); @export(timeStamp, .{ .name = shim.symbolName("timeStamp") }); @export(record, .{ .name = shim.symbolName("record") }); @export(recordEnd, .{ .name = shim.symbolName("recordEnd") }); @export(screenshot, .{ .name = shim.symbolName("screenshot") }); }