From 43a5c4a0442cca7de0128d2551a9532be35d57cf Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 11 Oct 2024 21:35:49 -0700 Subject: [PATCH] Implement Bun.inspect.table (#14486) --- docs/api/utils.md | 59 ++++ packages/bun-types/bun.d.ts | 9 + src/bun.js/ConsoleObject.zig | 67 +++- src/bun.js/api/BunObject.zig | 152 +++++---- src/bun.js/bindings/bindings.zig | 4 + src/bun.js/bindings/helpers.h | 2 +- .../bun-inspect-table.test.ts.snap | 289 ++++++++++++++++++ test/js/bun/console/bun-inspect-table.test.ts | 66 ++++ test/js/bun/util/inspect.test.js | 1 - test/js/node/util/bun-inspect.test.ts | 32 ++ 10 files changed, 614 insertions(+), 67 deletions(-) create mode 100644 test/js/bun/console/__snapshots__/bun-inspect-table.test.ts.snap create mode 100644 test/js/bun/console/bun-inspect-table.test.ts diff --git a/docs/api/utils.md b/docs/api/utils.md index d4e46441e0951..765020acd98d7 100644 --- a/docs/api/utils.md +++ b/docs/api/utils.md @@ -580,6 +580,65 @@ const foo = new Foo(); console.log(foo); // => "foo" ``` +## `Bun.inspect.table(tabularData, properties, options)` + +Format tabular data into a string. Like [`console.table`](https://developer.mozilla.org/en-US/docs/Web/API/console/table_static), except it returns a string rather than printing to the console. + +```ts +console.log( + Bun.inspect.table([ + { a: 1, b: 2, c: 3 }, + { a: 4, b: 5, c: 6 }, + { a: 7, b: 8, c: 9 }, + ]), +); +// +// ┌───┬───┬───┬───┐ +// │ │ a │ b │ c │ +// ├───┼───┼───┼───┤ +// │ 0 │ 1 │ 2 │ 3 │ +// │ 1 │ 4 │ 5 │ 6 │ +// │ 2 │ 7 │ 8 │ 9 │ +// └───┴───┴───┴───┘ +``` + +Additionally, you can pass an array of property names to display only a subset of properties. + +```ts +console.log( + Bun.inspect.table( + [ + { a: 1, b: 2, c: 3 }, + { a: 4, b: 5, c: 6 }, + ], + ["a", "c"], + ), +); +// +// ┌───┬───┬───┐ +// │ │ a │ c │ +// ├───┼───┼───┤ +// │ 0 │ 1 │ 3 │ +// │ 1 │ 4 │ 6 │ +// └───┴───┴───┘ +``` + +You can also conditionally enable ANSI colors by passing `{ colors: true }`. + +```ts +console.log( + Bun.inspect.table( + [ + { a: 1, b: 2, c: 3 }, + { a: 4, b: 5, c: 6 }, + ], + { + colors: true, + }, + ), +); +``` + ## `Bun.nanoseconds()` Returns the number of nanoseconds since the current `bun` process started, as a `number`. Useful for high-precision timing and benchmarking. diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index f1b51b96a1af8..63e0fe083df46 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3023,6 +3023,7 @@ declare module "bun" { colors?: boolean; depth?: number; sorted?: boolean; + compact?: boolean; } /** @@ -3038,6 +3039,14 @@ declare module "bun" { * That can be used to declare custom inspect functions. */ const custom: typeof import("util").inspect.custom; + + /** + * Pretty-print an object or array as a table + * + * Like {@link console.table}, except it returns a string + */ + function table(tabularData: object | unknown[], properties?: string[], options?: { colors?: boolean }): string; + function table(tabularData: object | unknown[], options?: { colors?: boolean }): string; } interface MMapOptions { diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index 25b9df0ae0ed0..e037f8ccc0e4c 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -219,7 +219,7 @@ pub fn messageWithTypeAndLevel( } } -const TablePrinter = struct { +pub const TablePrinter = struct { const Column = struct { name: String, width: u32 = 1, @@ -666,6 +666,69 @@ pub const FormatOptions = struct { ordered_properties: bool = false, quote_strings: bool = false, max_depth: u16 = 2, + single_line: bool = false, + + pub fn fromJS(formatOptions: *FormatOptions, globalThis: *JSC.JSGlobalObject, arguments: []const JSC.JSValue) !void { + const arg1 = arguments[0]; + + if (arg1.isObject()) { + if (arg1.getTruthy(globalThis, "depth")) |opt| { + if (opt.isInt32()) { + const arg = opt.toInt32(); + if (arg < 0) { + globalThis.throwInvalidArguments("expected depth to be greater than or equal to 0, got {d}", .{arg}); + return error.JSError; + } + 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 { + globalThis.throwInvalidArguments("expected depth to be an integer, got {d}", .{v}); + return error.JSError; + } + } + } + if (try arg1.getOptional(globalThis, "colors", bool)) |opt| { + formatOptions.enable_colors = opt; + } + if (try arg1.getOptional(globalThis, "sorted", bool)) |opt| { + formatOptions.ordered_properties = opt; + } + + if (try arg1.getOptional(globalThis, "compact", bool)) |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) { + globalThis.throwInvalidArguments("expected depth to be greater than or equal to 0, got {d}", .{arg}); + return error.JSError; + } + 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 { + globalThis.throwInvalidArguments("expected depth to be an integer, got {d}", .{v}); + return error.JSError; + } + } + 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( @@ -694,6 +757,7 @@ pub fn format2( .ordered_properties = options.ordered_properties, .quote_strings = options.quote_strings, .max_depth = options.max_depth, + .single_line = options.single_line, }; const tag = ConsoleObject.Formatter.Tag.get(vals[0], global); @@ -771,6 +835,7 @@ pub fn format2( .globalThis = global, .ordered_properties = options.ordered_properties, .quote_strings = options.quote_strings, + .single_line = options.single_line, }; var tag: ConsoleObject.Formatter.Tag.Result = undefined; diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index e9397f692bbfe..8f3981138a2f7 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -10,8 +10,12 @@ const conv = std.builtin.CallingConvention.Unspecified; pub const BunObject = struct { // --- Callbacks --- pub const allocUnsafe = toJSCallback(Bun.allocUnsafe); + pub const braces = toJSCallback(Bun.braces); pub const build = toJSCallback(Bun.JSBundler.buildFn); + pub const color = bun.css.CssColor.jsFunctionColor; pub const connect = toJSCallback(JSC.wrapStaticMethod(JSC.API.Listener, "connect", false)); + pub const createParsedShellScript = toJSCallback(bun.shell.ParsedShellScript.createParsedShellScript); + pub const createShellInterpreter = toJSCallback(bun.shell.Interpreter.createShellInterpreter); pub const deflateSync = toJSCallback(JSZlib.deflateSync); pub const file = toJSCallback(WebCore.Blob.constructBunFile); pub const gc = toJSCallback(Bun.runGC); @@ -22,7 +26,6 @@ pub const BunObject = struct { pub const inflateSync = toJSCallback(JSZlib.inflateSync); pub const jest = toJSCallback(@import("../test/jest.zig").Jest.call); pub const listen = toJSCallback(JSC.wrapStaticMethod(JSC.API.Listener, "listen", false)); - pub const udpSocket = toJSCallback(JSC.wrapStaticMethod(JSC.API.UDPSocket, "udpSocket", false)); pub const mmap = toJSCallback(Bun.mmapFile); pub const nanoseconds = toJSCallback(Bun.nanoseconds); pub const openInEditor = toJSCallback(Bun.openInEditor); @@ -31,24 +34,22 @@ pub const BunObject = struct { pub const resolveSync = toJSCallback(Bun.resolveSync); pub const serve = toJSCallback(Bun.serve); pub const sha = toJSCallback(JSC.wrapStaticMethod(Crypto.SHA512_256, "hash_", true)); + pub const shellEscape = toJSCallback(Bun.shellEscape); pub const shrink = toJSCallback(Bun.shrink); pub const sleepSync = toJSCallback(Bun.sleepSync); pub const spawn = toJSCallback(JSC.wrapStaticMethod(JSC.Subprocess, "spawn", false)); pub const spawnSync = toJSCallback(JSC.wrapStaticMethod(JSC.Subprocess, "spawnSync", false)); + pub const stringWidth = toJSCallback(Bun.stringWidth); + pub const udpSocket = toJSCallback(JSC.wrapStaticMethod(JSC.API.UDPSocket, "udpSocket", false)); pub const which = toJSCallback(Bun.which); pub const write = toJSCallback(JSC.WebCore.Blob.writeFile); - pub const stringWidth = toJSCallback(Bun.stringWidth); - pub const braces = toJSCallback(Bun.braces); - pub const shellEscape = toJSCallback(Bun.shellEscape); - pub const createParsedShellScript = toJSCallback(bun.shell.ParsedShellScript.createParsedShellScript); - pub const createShellInterpreter = toJSCallback(bun.shell.Interpreter.createShellInterpreter); - pub const color = bun.css.CssColor.jsFunctionColor; // --- Callbacks --- // --- Getters --- pub const CryptoHasher = toJSGetter(Crypto.CryptoHasher.getter); pub const FFI = toJSGetter(Bun.FFIObject.getter); pub const FileSystemRouter = toJSGetter(Bun.getFileSystemRouter); + pub const Glob = toJSGetter(Bun.getGlobConstructor); pub const MD4 = toJSGetter(Crypto.MD4.getter); pub const MD5 = toJSGetter(Crypto.MD5.getter); pub const SHA1 = toJSGetter(Crypto.SHA1.getter); @@ -58,21 +59,20 @@ pub const BunObject = struct { pub const SHA512 = toJSGetter(Crypto.SHA512.getter); pub const SHA512_256 = toJSGetter(Crypto.SHA512_256.getter); pub const TOML = toJSGetter(Bun.getTOMLObject); - pub const Glob = toJSGetter(Bun.getGlobConstructor); pub const Transpiler = toJSGetter(Bun.getTranspilerConstructor); pub const argv = toJSGetter(Bun.getArgv); pub const cwd = toJSGetter(Bun.getCWD); + pub const embeddedFiles = toJSGetter(Bun.getEmbeddedFiles); pub const enableANSIColors = toJSGetter(Bun.enableANSIColors); pub const hash = toJSGetter(Bun.getHashObject); pub const inspect = toJSGetter(Bun.getInspect); pub const main = toJSGetter(Bun.getMain); pub const origin = toJSGetter(Bun.getOrigin); + pub const semver = toJSGetter(Bun.getSemver); pub const stderr = toJSGetter(Bun.getStderr); pub const stdin = toJSGetter(Bun.getStdin); pub const stdout = toJSGetter(Bun.getStdout); pub const unsafe = toJSGetter(Bun.getUnsafe); - pub const semver = toJSGetter(Bun.getSemver); - pub const embeddedFiles = toJSGetter(Bun.getEmbeddedFiles); // --- Getters --- fn getterName(comptime baseName: anytype) [:0]const u8 { @@ -483,6 +483,81 @@ pub fn which( return JSC.JSValue.jsNull(); } +pub fn inspectTable( + globalThis: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, +) callconv(JSC.conv) JSC.JSValue { + var args_buf = callframe.argumentsUndef(5); + var all_arguments = args_buf.mut(); + if (all_arguments[0].isUndefined() or all_arguments[0].isNull()) + return bun.String.empty.toJS(globalThis); + + for (all_arguments) |arg| { + arg.protect(); + } + defer { + for (all_arguments) |arg| { + arg.unprotect(); + } + } + + var arguments = all_arguments[0..]; + + if (!arguments[1].isArray()) { + arguments[2] = arguments[1]; + arguments[1] = .undefined; + } + + var formatOptions = ConsoleObject.FormatOptions{ + .enable_colors = false, + .add_newline = false, + .flush = false, + .max_depth = 5, + .quote_strings = true, + .ordered_properties = false, + .single_line = true, + }; + if (arguments[2].isObject()) { + formatOptions.fromJS(globalThis, arguments[2..]) catch return .zero; + } + const value = arguments[0]; + + // very stable memory address + var array = MutableString.init(getAllocator(globalThis), 0) catch bun.outOfMemory(); + defer array.deinit(); + var buffered_writer_ = MutableString.BufferedWriter{ .context = &array }; + var buffered_writer = &buffered_writer_; + + const writer = buffered_writer.writer(); + const Writer = @TypeOf(writer); + const properties = if (arguments[1].jsType().isArray()) arguments[1] else JSValue.undefined; + var table_printer = ConsoleObject.TablePrinter.init( + globalThis, + .Log, + value, + properties, + ); + table_printer.value_formatter.depth = formatOptions.max_depth; + table_printer.value_formatter.ordered_properties = formatOptions.ordered_properties; + table_printer.value_formatter.single_line = formatOptions.single_line; + + switch (formatOptions.enable_colors) { + inline else => |colors| table_printer.printTable(Writer, writer, colors) catch { + if (!globalThis.hasException()) + globalThis.throwOutOfMemory(); + return .zero; + }, + } + + buffered_writer.flush() catch return { + globalThis.throwOutOfMemory(); + return .zero; + }; + + var out = bun.String.createUTF8(array.toOwnedSliceLeaky()); + return out.transferToJS(globalThis); +} + pub fn inspect( globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, @@ -508,62 +583,10 @@ pub fn inspect( .quote_strings = true, .ordered_properties = false, }; - const value = arguments[0]; - if (arguments.len > 1) { - const arg1 = arguments[1]; - - if (arg1.isObject()) { - if (arg1.getTruthy(globalThis, "depth")) |opt| { - if (opt.isInt32()) { - const arg = opt.toInt32(); - if (arg < 0) { - globalThis.throwInvalidArguments("expected depth to be greater than or equal to 0, got {d}", .{arg}); - return .zero; - } - 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 { - globalThis.throwInvalidArguments("expected depth to be an integer, got {d}", .{v}); - return .zero; - } - } - } - if (arg1.getOptional(globalThis, "colors", bool) catch return .zero) |opt| { - formatOptions.enable_colors = opt; - } - if (arg1.getOptional(globalThis, "sorted", bool) catch return .zero) |opt| { - formatOptions.ordered_properties = opt; - } - } else { - // formatOptions.show_hidden = arg1.toBoolean(); - if (arguments.len > 2) { - var depthArg = arguments[1]; - if (depthArg.isInt32()) { - const arg = depthArg.toInt32(); - if (arg < 0) { - globalThis.throwInvalidArguments("expected depth to be greater than or equal to 0, got {d}", .{arg}); - return .zero; - } - 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 { - globalThis.throwInvalidArguments("expected depth to be an integer, got {d}", .{v}); - return .zero; - } - } - if (arguments.len > 3) { - formatOptions.enable_colors = arguments[2].toBoolean(); - } - } - } + formatOptions.fromJS(globalThis, arguments[1..]) catch return .zero; } + const value = arguments[0]; // very stable memory address var array = MutableString.init(getAllocator(globalThis), 0) catch unreachable; @@ -600,6 +623,7 @@ pub fn getInspect(globalObject: *JSC.JSGlobalObject, _: *JSC.JSObject) JSC.JSVal const fun = JSC.createCallback(globalObject, ZigString.static("inspect"), 2, inspect); var str = ZigString.init("nodejs.util.inspect.custom"); fun.put(globalObject, ZigString.static("custom"), JSC.JSValue.symbolFor(globalObject, &str)); + fun.put(globalObject, ZigString.static("table"), JSC.createCallback(globalObject, ZigString.static("table"), 3, inspectTable)); return fun; } diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 2fc5a560b0f3a..f6e0068dde1b7 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -6506,6 +6506,10 @@ pub const CallFrame = opaque { pub inline fn all(self: *const @This()) []const JSValue { return self.ptr[0..]; } + + pub inline fn mut(self: *@This()) []JSValue { + return self.ptr[0..]; + } }; } diff --git a/src/bun.js/bindings/helpers.h b/src/bun.js/bindings/helpers.h index ee00db842f23c..5aeca386957d2 100644 --- a/src/bun.js/bindings/helpers.h +++ b/src/bun.js/bindings/helpers.h @@ -81,7 +81,7 @@ static const WTF::String toString(ZigString str) return WTF::String(); } if (UNLIKELY(isTaggedUTF8Ptr(str.ptr))) { - return WTF::String::fromUTF8(std::span { untag(str.ptr), str.len }); + return WTF::String::fromUTF8ReplacingInvalidSequences(std::span { untag(str.ptr), str.len }); } if (UNLIKELY(isTaggedExternalPtr(str.ptr))) { diff --git a/test/js/bun/console/__snapshots__/bun-inspect-table.test.ts.snap b/test/js/bun/console/__snapshots__/bun-inspect-table.test.ts.snap new file mode 100644 index 0000000000000..0dbd06b2d18f8 --- /dev/null +++ b/test/js/bun/console/__snapshots__/bun-inspect-table.test.ts.snap @@ -0,0 +1,289 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`inspect.table { a: 1, b: 2 } 1`] = ` +"┌───┬────────┐ +│ │ Values │ +├───┼────────┤ +│ a │ 1 │ +│ b │ 2 │ +└───┴────────┘ +" +`; + +exports[`inspect.table { a: 1, b: 2, c: 3 } 1`] = ` +"┌───┬────────┐ +│ │ Values │ +├───┼────────┤ +│ a │ 1 │ +│ b │ 2 │ +│ c │ 3 │ +└───┴────────┘ +" +`; + +exports[`inspect.table { a: 1, b: 2, c: 3, d: 4 } 1`] = ` +"┌───┬────────┐ +│ │ Values │ +├───┼────────┤ +│ a │ 1 │ +│ b │ 2 │ +│ c │ 3 │ +│ d │ 4 │ +└───┴────────┘ +" +`; + +exports[`inspect.table Map(2) { "a": 1, "b": 2 } 1`] = ` +"┌───┬─────┬────────┐ +│ │ Key │ Values │ +├───┼─────┼────────┤ +│ 0 │ a │ 1 │ +│ 1 │ b │ 2 │ +└───┴─────┴────────┘ +" +`; + +exports[`inspect.table [ [ "a", 1 ], [ "b", 2 ] ] 1`] = ` +"┌───┬───┬───┐ +│ │ 0 │ 1 │ +├───┼───┼───┤ +│ 0 │ a │ 1 │ +│ 1 │ b │ 2 │ +└───┴───┴───┘ +" +`; + +exports[`inspect.table Set(3) { 1, 2, 3 } 1`] = ` +"┌───┬────────┐ +│ │ Values │ +├───┼────────┤ +│ 0 │ 1 │ +│ 1 │ 2 │ +│ 2 │ 3 │ +└───┴────────┘ +" +`; + +exports[`inspect.table { "0": 1, "1": 2, "2": 3 } 1`] = ` +"┌───┬────────┐ +│ │ Values │ +├───┼────────┤ +│ 0 │ 1 │ +│ 1 │ 2 │ +│ 2 │ 3 │ +└───┴────────┘ +" +`; + +exports[`inspect.table [ 1, 2, 3 ] 1`] = ` +"┌───┬────────┐ +│ │ Values │ +├───┼────────┤ +│ 0 │ 1 │ +│ 1 │ 2 │ +│ 2 │ 3 │ +└───┴────────┘ +" +`; + +exports[`inspect.table [ "a", 1, "b", 2, "c", 3 ] 1`] = ` +"┌───┬────────┐ +│ │ Values │ +├───┼────────┤ +│ 0 │ a │ +│ 1 │ 1 │ +│ 2 │ b │ +│ 3 │ 2 │ +│ 4 │ c │ +│ 5 │ 3 │ +└───┴────────┘ +" +`; + +exports[`inspect.table [ /a/, 1, /b/, 2, /c/, 3 ] 1`] = ` +"┌───┬────────┐ +│ │ Values │ +├───┼────────┤ +│ 0 │ │ +│ 1 │ 1 │ +│ 2 │ │ +│ 3 │ 2 │ +│ 4 │ │ +│ 5 │ 3 │ +└───┴────────┘ +" +`; + +exports[`inspect.table (ansi) { a: 1, b: 2 } 1`] = ` +"┌───┬────────┐ +│   │ Values │ +├───┼────────┤ +│ a │ 1 │ +│ b │ 2 │ +└───┴────────┘ +" +`; + +exports[`inspect.table (ansi) { a: 1, b: 2, c: 3 } 1`] = ` +"┌───┬────────┐ +│   │ Values │ +├───┼────────┤ +│ a │ 1 │ +│ b │ 2 │ +│ c │ 3 │ +└───┴────────┘ +" +`; + +exports[`inspect.table (ansi) { a: 1, b: 2, c: 3, d: 4 } 1`] = ` +"┌───┬────────┐ +│   │ Values │ +├───┼────────┤ +│ a │ 1 │ +│ b │ 2 │ +│ c │ 3 │ +│ d │ 4 │ +└───┴────────┘ +" +`; + +exports[`inspect.table (ansi) Map(2) { "a": 1, "b": 2 } 1`] = ` +"┌───┬─────┬────────┐ +│   │ Key │ Values │ +├───┼─────┼────────┤ +│ 0 │ a │ 1 │ +│ 1 │ b │ 2 │ +└───┴─────┴────────┘ +" +`; + +exports[`inspect.table (ansi) [ [ "a", 1 ], [ "b", 2 ] ] 1`] = ` +"┌───┬───┬───┐ +│   │ 0 │ 1 │ +├───┼───┼───┤ +│ 0 │ a │ 1 │ +│ 1 │ b │ 2 │ +└───┴───┴───┘ +" +`; + +exports[`inspect.table (ansi) Set(3) { 1, 2, 3 } 1`] = ` +"┌───┬────────┐ +│   │ Values │ +├───┼────────┤ +│ 0 │ 1 │ +│ 1 │ 2 │ +│ 2 │ 3 │ +└───┴────────┘ +" +`; + +exports[`inspect.table (ansi) { "0": 1, "1": 2, "2": 3 } 1`] = ` +"┌───┬────────┐ +│   │ Values │ +├───┼────────┤ +│ 0 │ 1 │ +│ 1 │ 2 │ +│ 2 │ 3 │ +└───┴────────┘ +" +`; + +exports[`inspect.table (ansi) [ 1, 2, 3 ] 1`] = ` +"┌───┬────────┐ +│   │ Values │ +├───┼────────┤ +│ 0 │ 1 │ +│ 1 │ 2 │ +│ 2 │ 3 │ +└───┴────────┘ +" +`; + +exports[`inspect.table (ansi) [ "a", 1, "b", 2, "c", 3 ] 1`] = ` +"┌───┬────────┐ +│   │ Values │ +├───┼────────┤ +│ 0 │ a │ +│ 1 │ 1 │ +│ 2 │ b │ +│ 3 │ 2 │ +│ 4 │ c │ +│ 5 │ 3 │ +└───┴────────┘ +" +`; + +exports[`inspect.table (ansi) [ /a/, 1, /b/, 2, /c/, 3 ] 1`] = ` +"┌───┬────────┐ +│   │ Values │ +├───┼────────┤ +│ 0 │ │ +│ 1 │ 1 │ +│ 2 │ │ +│ 3 │ 2 │ +│ 4 │ │ +│ 5 │ 3 │ +└───┴────────┘ +" +`; + +exports[`inspect.table (with properties) { a: 1, b: 2 } 1`] = ` +"┌───┬───┐ +│ │ b │ +├───┼───┤ +│ a │ │ +│ b │ │ +└───┴───┘ +" +`; + +exports[`inspect.table (with properties) { a: 1, b: 2 } 2`] = ` +"┌───┬───┐ +│ │ a │ +├───┼───┤ +│ a │ │ +│ b │ │ +└───┴───┘ +" +`; + +exports[`inspect.table (with properties and colors) { a: 1, b: 2 } 1`] = ` +"┌───┬───┐ +│   │ b │ +├───┼───┤ +│ a │ │ +│ b │ │ +└───┴───┘ +" +`; + +exports[`inspect.table (with properties and colors) { a: 1, b: 2 } 2`] = ` +"┌───┬───┐ +│   │ a │ +├───┼───┤ +│ a │ │ +│ b │ │ +└───┴───┘ +" +`; + +exports[`inspect.table (with colors in 2nd position) { a: 1, b: 2 } 1`] = ` +"┌───┬────────┐ +│   │ Values │ +├───┼────────┤ +│ a │ 1 │ +│ b │ 2 │ +└───┴────────┘ +" +`; + +exports[`inspect.table (with colors in 2nd position) { a: 1, b: 2 } 2`] = ` +"┌───┬────────┐ +│   │ Values │ +├───┼────────┤ +│ a │ 1 │ +│ b │ 2 │ +└───┴────────┘ +" +`; diff --git a/test/js/bun/console/bun-inspect-table.test.ts b/test/js/bun/console/bun-inspect-table.test.ts new file mode 100644 index 0000000000000..3736701a4f1ce --- /dev/null +++ b/test/js/bun/console/bun-inspect-table.test.ts @@ -0,0 +1,66 @@ +import { inspect } from "bun"; +import { test, expect, describe } from "bun:test"; + +const inputs = [ + { a: 1, b: 2 }, + { a: 1, b: 2, c: 3 }, + { a: 1, b: 2, c: 3, d: 4 }, + new Map([ + ["a", 1], + ["b", 2], + ]), + [ + ["a", 1], + ["b", 2], + ], + new Set([1, 2, 3]), + { 0: 1, 1: 2, 2: 3 }, + [1, 2, 3], + ["a", 1, "b", 2, "c", 3], + [/a/, 1, /b/, 2, /c/, 3], +]; + +describe("inspect.table", () => { + inputs.forEach(input => { + test(Bun.inspect(input, { colors: false, sorted: true, compact: true }), () => { + expect(inspect.table(input, { colors: false, sorted: true })).toMatchSnapshot(); + }); + }); +}); + +describe("inspect.table (ansi)", () => { + inputs.forEach(input => { + test(Bun.inspect(input, { colors: false, sorted: true, compact: true }), () => { + expect(inspect.table(input, { colors: true, sorted: true })).toMatchSnapshot(); + }); + }); +}); + +const withProperties = [ + [{ a: 1, b: 2 }, ["b"]], + [{ a: 1, b: 2 }, ["a"]], +]; + +describe("inspect.table (with properties)", () => { + withProperties.forEach(([input, properties]) => { + test(Bun.inspect(input, { colors: false, sorted: true, compact: true }), () => { + expect(inspect.table(input, properties, { colors: false, sorted: true })).toMatchSnapshot(); + }); + }); +}); + +describe("inspect.table (with properties and colors)", () => { + withProperties.forEach(([input, properties]) => { + test(Bun.inspect(input, { colors: false, sorted: true, compact: true }), () => { + expect(inspect.table(input, properties, { colors: true, sorted: true })).toMatchSnapshot(); + }); + }); +}); + +describe("inspect.table (with colors in 2nd position)", () => { + withProperties.forEach(([input, properties]) => { + test(Bun.inspect(input, { colors: false, sorted: true, compact: true }), () => { + expect(inspect.table(input, { colors: true, sorted: true })).toMatchSnapshot(); + }); + }); +}); diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 15ad2702c935e..301d26742668c 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -147,7 +147,6 @@ it("utf16 property name", () => { 笑: "😀", }, ], - null, 2, ); expect(Bun.inspect(db.prepare("select '😀' as 笑").all())).toBe(output); diff --git a/test/js/node/util/bun-inspect.test.ts b/test/js/node/util/bun-inspect.test.ts index 5d6e93b5f2b64..57151a7b5b5b1 100644 --- a/test/js/node/util/bun-inspect.test.ts +++ b/test/js/node/util/bun-inspect.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "bun:test"; +import stripAnsi from "strip-ansi"; describe("Bun.inspect", () => { it("reports error instead of [native code]", () => { @@ -11,6 +12,37 @@ describe("Bun.inspect", () => { ).toBe("[custom formatter threw an exception]"); }); + it("supports colors: false", () => { + const output = Bun.inspect({ a: 1 }, { colors: false }); + expect(stripAnsi(output)).toBe(output); + }); + + it("supports colors: true", () => { + const output = Bun.inspect({ a: 1 }, { colors: true }); + expect(stripAnsi(output)).not.toBe(output); + expect(stripAnsi(output)).toBe(Bun.inspect({ a: 1 }, { colors: false })); + }); + + it("supports colors: false, via 2nd arg", () => { + const output = Bun.inspect({ a: 1 }, null, null); + expect(stripAnsi(output)).toBe(output); + }); + + it("supports colors: true, via 2nd arg", () => { + const output = Bun.inspect({ a: 1 }, true, 2); + expect(stripAnsi(output)).not.toBe(output); + }); + + it("supports compact", () => { + expect(Bun.inspect({ a: 1, b: 2 }, { compact: true })).toBe("{ a: 1, b: 2 }"); + expect(Bun.inspect({ a: 1, b: 2 }, { compact: false })).toBe("{\n a: 1,\n b: 2,\n}"); + + expect(Bun.inspect({ a: { 0: 1, 1: 2 }, b: 3 }, { compact: true })).toBe('{ a: { "0": 1, "1": 2 }, b: 3 }'); + expect(Bun.inspect({ a: { 0: 1, 1: 2 }, b: 3 }, { compact: false })).toBe( + '{\n a: {\n "0": 1,\n "1": 2,\n },\n b: 3,\n}', + ); + }); + it("depth < 0 throws", () => { expect(() => Bun.inspect({}, { depth: -1 })).toThrow(); expect(() => Bun.inspect({}, { depth: -13210 })).toThrow();