Skip to content

console.table, Bun.inspect compact: carry single-line mode to inspect.custom hooks and native printers - #47593

Open
robobun wants to merge 5 commits into
mainfrom
robobun/43fedc2b/inspect-single-line-hooks
Open

robobun wants to merge 5 commits into
mainfrom
robobun/43fedc2b/inspect-single-line-hooks

Conversation

@robobun

@robobun robobun commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • A console.table or Bun.inspect.table cell that holds a URL, TransformStream, PerformanceMark, Request, Response or File prints on many lines. console.table([{ c: new URL("http://example.com/a?b=1") }]) prints 18 lines. Bun.inspect(value, { compact: true }) has the same defect.
  • Two hand-offs drop the formatter's single-line mode (tables and compact: true set it). An [inspect.custom] hook gets only { stylize, depth, colors } (src/jsc/bindings/UtilInspect.cpp:32), so util.inspect wraps at 80 columns. Native printers write "\n" by hand (src/jsc/lib.rs:238).

Fix

  • Single-line mode adds breakLength: Infinity and compact: true to the hook options. Eight native printers write 44 line breaks through ConsoleFormatter::write_break().
  • Correct because the default text does not move: Bun.inspect(value) is identical for 229 kinds.
  • Verified: test/js/bun/console/console-table.test.ts, one test per kind and function. Also inspect.test.js, custom-inspect.test.js.
  • Self-reviewed: 18 concerns raised, 16 addressed. Rejected: a print_event rewrite (Print snapshots with the console.log formatter, delete pretty_format.rs #46729 owns it), #[deprecated] on write_indent (one caller remains).

Background

  • depth is not changed: it decides how much of a value prints, not where lines break.
  • Considered collapsing line feeds in the cell: both printers stay wrong, compact stays broken.

Downsides

  • Outside tables, Buffer extra properties and a Request, Response or Blob in an error message print on one line. A hook that forwards to Bun.inspect now drops JSX children (<b />).
  • The AOT host table (what the ahead-of-time compiler knows about a native object) omits the two new keys: an engine test pins its row. Compiled programs read both correctly (tested).
  • Single-line hook call: +32 B. Default path: +8 instructions per hook call, 14,534 to 14,754 for a Response. .text: -8,704 bytes.
Notes

Origin

There is no issue for this change. @Jarred-Sumner asked for a PR that keeps these six cells on one line in console.table and in Bun.inspect.table, with a test for each kind. Nobody else reported it.

Regression

Lines that a one-row table prints. 5 is a whole box.

cell 1.3.14 1.4.0 1.4.1 to 1.4.4, main this PR Node 26.3.0
URL 5 18 18 5 5
TransformStream 5 9 9 5 5
PerformanceMark 5 5 11 5 5
Request 9 9 9 5 5
Response 14 14 14 5 5
File 8 8 8 5 5

Which lines are the fix

  • createInspectOptionsObject in src/jsc/bindings/UtilInspect.cpp: the two option keys. print_custom_formatted_object passes one flags byte (colors, single-line) where it passed colors: bool, so the call keeps six register arguments.
  • ConsoleFormatter::write_break and single_line in src/jsc/lib.rs. The edits in Request.rs, Response.rs, Body.rs, Blob.rs, S3File.rs, S3Client.rs, Archive.rs and JSBundler.rs replace each "\n" plus write_indent with one write_break.
  • The cheaper shape, a fold of line feed plus indent in TablePrinter::format_cell, gives the same six cells. It does not fix compact: true, it prints a Request with a body without a separator (headers: Headers {} Blob (2 bytes)), and it rewrites a line feed that is content (a File name).

What a hook is told

  • Default mode: { stylize, depth, colors }, as before.
  • Single-line mode: also breakLength: Infinity and compact: true. Node's Buffer hook passes this pair for one line (lib/buffer.js). Node's console.table passes breakLength: Infinity only (constructor.js). That is not enough here: with a numeric compact, util.inspect still breaks an object nested three levels and an array of more than six items. Node's cells hold neither (depth 0, three array items). Bun's cells do.
  • compact: true has two effects on a hook that forwards its options:
    • util.inspect puts a base inside the braces: Box { [Function: foo] a: 1 }, where the default layout prints Box [Function: foo] { a: 1 }. A test row pins this.
    • A hook that forwards to Bun.inspect now prints in single-line mode. There a JSX element prints without its children (<b>bold</b> prints <b />). A JSX element that is a cell by itself prints the same on main. Behind such a hook, main printed the children. print_jsx is not changed here.

How this relates to depth

  • console.table starts a cell at depth 0 of 5. Bun.inspect.table starts it at its depth option, 5 of 5 by default. A hook gets max_depth - depth as its first argument and options.depth = max_depth. This PR reads and changes none of that.
  • The six kinds print the same text in both functions today, because their hooks use options.depth and the native printers do not read a depth. If depth gets a new meaning for tables, the expected text of a cell can change with it (the nested streams of a TransformStream, for example), but it stays on one line.

Text that changes outside tables and compact

  • A Buffer with an extra own property that holds one of these values: util.inspect(buffer), console.log(buffer) and every other user of the Buffer hook print the property on one line. On main a URL property printed on 14 lines. For URL, TransformStream and PerformanceMark the text is now equal to Node's.
  • Error messages that hold an inspected value (ERR_INVALID_ARG_VALUE, the MessageEvent constructor):
    • A value with an [inspect.custom] hook keeps its text, which is Node's text. 18 of 18 probes are identical to main. The callers pass SingleLineHooks::OwnLayout for this.
    • A value with a native printer prints on one line. On main it printed a mix of both layouts: Received Request (2 bytes) {\n method: "POST",\n url: "http://example.com/",\n headers: Headers { "x-a": "1" }\n Blob (2 bytes)\n }. 18 of 18 probes change.

Still multi-line in a cell

The table does not enforce one line. { compact: false } turns single-line mode off.

Default text that stays odd on purpose

Each has a one-line comment in the code and a test row marked known-odd. They are one defect: separators written by hand for each member.

  • Request with a body: no comma between headers and the body.
  • A sliced Blob or File: a second line feed after offset, and the comma on a line of its own.
  • A sliced S3 file with no type: offset on the first line.

Related open PRs

AOT host table

  • b.hostObject(inspectOptionsCell, ...) in src/codegen/builtins/host/runtime-dependencies.builtins.ts tells the ahead-of-time compiler which own properties native code puts on the hook options: stylize, depth, colors. The generator's doc says that an object without otherNames has the listed fields alone. In single-line mode that is no longer exact.
  • The row cannot change in this repository alone. AOTBuiltinTableTests.cpp in oven-sh/WebKit (builtin-table/host-uses-of-startup-loads) asserts options->fields.size() == 4 for it. With breakLength and compact added as optional fields, test/aot/engine/native-4.test.ts fails with ASSERTION FAILED: options != table().hostObjects().end() && options->prototype == Builtins::ObjectPrototype && options->fields.size() == 4.
  • Without the two fields a compiled program still reads both keys. Six probe programs, each built alone with bun build --compile --aot on a release build of ad0c6f2 without the two fields, print what plain Bun prints. They cover own keys, in, destructuring, for in, JSON.stringify, a hook that only a table calls, a hook that forwards its options to inspect, and the built-in hooks of URL, TransformStream and PerformanceMark. The same holds with --aot-validate and with the run-time checks (run-parallel.ts --checks-at-run-time).
  • test/aot/parallel/closed-world/custom-inspect-method-reads-one-line-options.js keeps that true. If the compiler starts to rely on the row, this program fails.
  • To make the row exact, add the two fields and change the count in the engine test in the same WebKit update.

Open question

The two option keys are added with putDirectNamed on the cached 3-slot object. That is the extra 32 B allocation. A second cached 5-slot Structure removes it and costs one more lazy field on ZigGlobalObject.

Measurements

Release builds of 34cbc70 (main) and of ad0c6f2 (this branch before it merged main), linux x64. The source diff is the same since then, less the print_array gate that main now has and the two AOT table fields.

  • Bun.inspect(value), with and without colors: 0 of 229 kinds differ. A matrix of 59 printer-heavy kinds in 5 positions: 0 of 1,239 outputs without compact differ. bun:test matcher text for 14 kinds: identical.
  • compact: true: 52 of 229 kinds change. Kinds that hold a line feed: 63, then 11.
  • .text: 108,082,281 to 108,073,577 bytes. Binary: 137,570,424 to 137,562,232 bytes (llvm-size).
  • Hook call in default mode: 230 to 238 instructions, 28 to 29 conditional branches (gdb single-step of print_custom_formatted_object and JSC__JSValue__callCustomInspectFunction). The options object is 48 B in 1 cell on both builds.
  • Hook call in single-line mode: the options object is 48 B in 1 cell on main, 80 B in 2 cells here (bun:jsc heapStats).
  • Native printer in default mode, inclusive instructions: Response 14,534 to 14,754, Request with a body 9,525 to 9,694, File 2,475 to 2,567. The cause is one write_break call for each line, where the break was inline. Keeping it out of line is also what shrinks .text. No allocation is added.
  • size_of::<Formatter>() stays 144 bytes (gdb). The new byte fills padding.
  • bloaty is not installed. Sizes are from llvm-size and llvm-nm.

Tests

  • test/js/bun/console/console-table.test.ts: the six kinds, each through Bun.inspect.table, as a Map value, and through the real console.table in a child process. All 18 fail on a debug build of main (2b0ace8).
  • test/js/bun/util/inspect.test.js: 21 rows with the default and the compact text of every native printer, stream bodies, BunRequest with params, BuildArtifact, Buffer properties, error messages. 27 of its 28 tests fail on that build of main. The first commit holds only the default-text pins.
  • test/js/node/util/custom-inspect.test.js: the options a hook gets in each mode. 1 test fails on that build of main.
  • test/aot/parallel/closed-world/custom-inspect-method-reads-one-line-options.js: a compiled program reads the two keys. Built by hand on the debug build (bun build --compile --aot), it prints what plain Bun prints.
  • Green on the debug build of this branch after the merge of main (2b0ace8): the three files above, test/aot/engine/native-4.test.ts, test/internal/source-lints/.
  • Also green on the debug build before that merge: bun-inspect-table, console-log, message-event, url, fetch response, body, blob and body-clone, FormData, archive, s3, buffer, perf_hooks, streams, bun-inspect, util, the node inspect tests, bun-build-api.

Request, Response, Blob, S3, Archive and BuildArtifact print through
write_format functions that write each line feed by hand. The next
commit routes those line feeds through one helper. These tests hold the
default text, which must not change: Request with each kind of body,
BunRequest with params, Response with and without a body, Blob and File
with each member, S3Client and S3 files, Archive, BuildArtifact,
FormData with a File, a class whose inspect.custom hook forwards its
options, and a value with a hook in an error message. Four of the texts
are odd and are marked so. The tests pass before the change.
…tive printers

A console.table cell, a Bun.inspect.table cell and
Bun.inspect(value, { compact: true }) print with a formatter in
single-line mode. Two kinds of printer outside the formatter did not
know that mode and wrote line feeds:

- An inspect.custom hook. The formatter called it with the options
  { stylize, depth, colors }. A hook that forwards its options to
  util.inspect (URL, the web streams, PerformanceEntry, CryptoKey, user
  classes) got the default breakLength of 80. In single-line mode the
  options now also hold breakLength: Infinity and compact: true, the
  pair that Node's Buffer hook passes for one line.
- A native write_format printer (Request, Response, Blob and File, S3,
  Archive, BuildArtifact). Each wrote "\n" and the indent by hand. They
  now call ConsoleFormatter::write_break, which writes one space in
  single-line mode.

print_array wrote a line feed before "... N more items" in single-line
mode. It writes a space there now.

The default text of these values does not change.

Bun__inspect_singleline takes a SingleLineHooks argument. The extra
properties of a Buffer pass OneLine and print on one line, as in Node.
Error messages pass OwnLayout: a value with an inspect.custom hook
keeps the text that Node's message has. A value with a native printer
prints on one line there, where it had a mix of both layouts.
@robobun

robobun commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

  • Reproduced on main (34cbc70, 2b0ace8) and on 1.4.0 to 1.4.4: console.table([{ c: new URL("http://example.com/a?b=1") }]) prints 18 lines. A one-row table is 5 lines. TransformStream and performance.mark cells print 9 and 11 lines. Request, Response and File cells print 9, 14 and 8 lines on every release, 1.3.14 included.
  • On this branch each of the six cells prints 5 lines in console.table and in Bun.inspect.table, as in Node 26.3.0.
  • On a debug build of main 2b0ace8 the new tests fail: 18 in console-table.test.ts, 27 in inspect.test.js, 1 in custom-inspect.test.js. On this branch (4ffdea1) the three files pass.
  • CI build 136292 is complete. 29 test files are red. All are AOT tests that this diff does not touch, and all 29 are also red on main's build 136333 of cee9dd9. 28 of them are still red on main's build 136408 of 3f3a9ff. 35 more test files failed once and passed on a retry. No test that this PR adds or changes failed on any lane. The diff is ready for review. A merge of main gives a clean run once those tests pass on main.
  • PR: console.table, Bun.inspect compact: carry single-line mode to inspect.custom hooks and native printers #47593

@coderabbitai

coderabbitai Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 2b0ace8 and 4ffdea1.

Files selected for processing: 20
  • src/jsc/ConsoleObject.rs
  • src/jsc/bindings/ErrorCode.cpp
  • src/jsc/bindings/JSBuffer.cpp
  • src/jsc/bindings/UtilInspect.cpp
  • src/jsc/bindings/UtilInspect.h
  • src/jsc/bindings/webcore/JSMessageEvent.cpp
  • src/jsc/lib.rs
  • src/runtime/api/Archive.rs
  • src/runtime/api/BunObject.rs
  • src/runtime/api/JSBundler.rs
  • src/runtime/webcore/Blob.rs
  • src/runtime/webcore/Body.rs
  • src/runtime/webcore/Request.rs
  • src/runtime/webcore/Response.rs
  • src/runtime/webcore/S3Client.rs
  • src/runtime/webcore/S3File.rs
  • test/aot/parallel/closed-world/custom-inspect-method-reads-one-line-options.js
  • test/js/bun/console/console-table.test.ts
  • test/js/bun/util/inspect.test.js
  • test/js/node/util/custom-inspect.test.js

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

Walkthrough

Inspection hooks now receive single-line options when formatting requests one-line output. Console and runtime value formatters use formatter-controlled breaks for single-line and multiline output. Added tests cover hook options, inspected values, table cells, and error messages.

Changes

Single-line inspection formatting

Layer / File(s) Summary
Custom inspect hook options
src/jsc/ConsoleObject.rs, src/jsc/bindings/UtilInspect.*, src/runtime/api/BunObject.rs, src/jsc/bindings/ErrorCode.cpp, src/jsc/bindings/JSBuffer.cpp, src/jsc/bindings/webcore/JSMessageEvent.cpp
Formatter options now distinguish hooks asked to print on one line from hooks that retain their own layout. The custom-inspect call passes color and single-line flags, which are converted into inspect options. Call sites specify the hook layout setting.
Formatter-controlled breaks
src/jsc/lib.rs, src/runtime/api/Archive.rs, src/runtime/api/JSBundler.rs, src/runtime/webcore/{Blob,Body,Request,Response,S3Client,S3File}.rs
ConsoleFormatter adds a break operation that writes a space in single-line mode or a newline and indentation otherwise. The listed value formatters use it in place of fixed line breaks and indentation.
Inspection and console formatting tests
test/aot/parallel/closed-world/custom-inspect-method-reads-one-line-options.js, test/js/bun/console/console-table.test.ts, test/js/bun/util/inspect.test.js, test/js/node/util/custom-inspect.test.js
Tests check custom-hook options, compact and pretty inspection output, one-line table cells, and formatting in error messages.

Possibly related issues

  • oven-sh/bun issue 37302: The issue requests Node-compatible console and inspect layout. This change updates compact inspection and console table formatting.

Suggested reviewers: jarred-sumner, dylan-conway

Merge Risk: Minimal · up to 4ffde

No merge-blocking issue is established for the single-line formatting changes. Merge readiness remains subject to normal build and test checks.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly and concisely summarizes the main change: propagating single-line formatting to custom inspect hooks and native printers for console.table and Bun.inspect.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description clearly explains the problem, implementation, scope, downsides, and verification. It does not use the exact template headings, but it provides the required information and is complete.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

…ect-single-line-hooks

# Conflicts:
#	src/jsc/ConsoleObject.rs
@robobun

robobun commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 11:34 PM PT - Oct 31st, 2026

❌ @robobun, your commit 4ffdea1 has 40 failures in Build #136292 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 47593

That installs a local version of the PR into your bun-47593 executable, so you can run:

bun-47593 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline findings, I traced the "\n" + write_indent -> write_break rewrite through all eight native printers against the pre-change source (including the Blob double line feed, the S3File first-line offset, and the Request body without a comma) and the default-mode byte sequence is unchanged; Body::write_format losing its leading write_indent is safe because Response::write_format is its only caller and now emits the break itself. The Rust SingleLineHooks/CustomInspectFlags match the C++ Bun::SingleLineHooks/CustomInspectFlag values and the u8 FFI signatures, and the bun:test ConsoleFormatter impl in src/runtime/test_runner/pretty_format.rs inherits the default single_line() == false, so its output is unaffected.

Extended reasoning...

The change spans the Rust console formatter, eight native write_format printers, the C++ inspect-options object handed to [inspect.custom] hooks, and three Bun__inspect_singleline callers, with no security-sensitive surface. It alters user-visible text outside tables (Buffer own-property hooks, error messages that embed a Request/Response/Blob), so a human should weigh those behavior changes alongside the inline findings.

Comment thread src/jsc/ConsoleObject.rs Resolved
A native test of the engine pins the row of the inspect options:
AOTBuiltinTableTests.cpp, builtin-table/host-uses-of-startup-loads,
asserts four field entries. The two optional fields made that six and
test/aot/engine/native-4.test.ts failed.

A compiled program reads both keys without the two fields.
test/aot/parallel/closed-world/custom-inspect-method-reads-one-line-options.js
holds that.
- main gates the `... N more items` break of print_array and tests it
  (#44833). Remove the two tests that did the same.
- main no longer adds a charset to an explicit Blob type. Use
  `image/png` in the rows that pin a type.
- The `compact: false` table test asserts the whole row line.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment thread src/jsc/ConsoleObject.rs Resolved
Comment thread src/runtime/webcore/Blob.rs Resolved

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants