Repository navigation
Conversation
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.
|
Status
|
|
No actionable comments were generated in the recent review.
|
| 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: 
4
- 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.
…ect-single-line-hooks # Conflicts: # src/jsc/ConsoleObject.rs
There was a problem hiding this comment.
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.
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.
Problem
console.tableorBun.inspect.tablecell that holds aURL,TransformStream,PerformanceMark,Request,ResponseorFileprints 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.compact: trueset it). An[inspect.custom]hook gets only{ stylize, depth, colors }(src/jsc/bindings/UtilInspect.cpp:32), soutil.inspectwraps at 80 columns. Native printers write"\n"by hand (src/jsc/lib.rs:238).Fix
breakLength: Infinityandcompact: trueto the hook options. Eight native printers write 44 line breaks throughConsoleFormatter::write_break().Bun.inspect(value)is identical for 229 kinds.test/js/bun/console/console-table.test.ts, one test per kind and function. Alsoinspect.test.js,custom-inspect.test.js.print_eventrewrite (Print snapshots with theconsole.logformatter, deletepretty_format.rs#46729 owns it),#[deprecated]onwrite_indent(one caller remains).Background
depthis not changed: it decides how much of a value prints, not where lines break.compactstays broken.Downsides
Request,ResponseorBlobin an error message print on one line. A hook that forwards toBun.inspectnow drops JSX children (<b />).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.tableand inBun.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.
URLTransformStreamPerformanceMarkRequestResponseFile[inspect.custom]hooks:URLin node compat batch: callback-throw dispatch, Assert class + native deep-equality parity, Intl gate + URL/buffer fallout, compile cache, watch kill-signal, profilers (+98 tests) #34660 and the web streams in Wire up HeapAnalyzer for Web Streams cell classes #33830 (both first in 1.4.0),PerformanceEntryin perf_hooks: native toJSON (with detail) and inspect hook for performance entries #39921 (first in 1.4.1). Before that the formatter printed them itself.Request,ResponseandFileare multi-line in a cell sinceconsole.tableshipped (feat: implement console.table #7866).Which lines are the fix
createInspectOptionsObjectinsrc/jsc/bindings/UtilInspect.cpp: the two option keys.print_custom_formatted_objectpasses one flags byte (colors, single-line) where it passedcolors: bool, so the call keeps six register arguments.ConsoleFormatter::write_breakandsingle_lineinsrc/jsc/lib.rs. The edits inRequest.rs,Response.rs,Body.rs,Blob.rs,S3File.rs,S3Client.rs,Archive.rsandJSBundler.rsreplace each"\n"pluswrite_indentwith onewrite_break.TablePrinter::format_cell, gives the same six cells. It does not fixcompact: true, it prints aRequestwith a body without a separator (headers: Headers {} Blob (2 bytes)), and it rewrites a line feed that is content (aFilename).What a hook is told
{ stylize, depth, colors }, as before.breakLength: Infinityandcompact: true. Node's Buffer hook passes this pair for one line (lib/buffer.js). Node'sconsole.tablepassesbreakLength: Infinityonly (constructor.js). That is not enough here: with a numericcompact,util.inspectstill 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: truehas two effects on a hook that forwards its options:util.inspectputs a base inside the braces:Box { [Function: foo] a: 1 }, where the default layout printsBox [Function: foo] { a: 1 }. A test row pins this.Bun.inspectnow 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_jsxis not changed here.How this relates to
depthconsole.tablestarts a cell at depth 0 of 5.Bun.inspect.tablestarts it at itsdepthoption, 5 of 5 by default. A hook getsmax_depth - depthas its first argument andoptions.depth = max_depth. This PR reads and changes none of that.options.depthand the native printers do not read a depth. Ifdepthgets a new meaning for tables, the expected text of a cell can change with it (the nested streams of aTransformStream, for example), but it stays on one line.Text that changes outside tables and
compactutil.inspect(buffer),console.log(buffer)and every other user of the Buffer hook print the property on one line. On main aURLproperty printed on 14 lines. ForURL,TransformStreamandPerformanceMarkthe text is now equal to Node's.ERR_INVALID_ARG_VALUE, theMessageEventconstructor):[inspect.custom]hook keeps its text, which is Node's text. 18 of 18 probes are identical to main. The callers passSingleLineHooks::OwnLayoutfor this.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.Error(its stack), a hook that returns a multi-line string, a hook that ignores its options.MessageEventandErrorEvent. Inprint_eventthe header writes a line feed, no space follows the member commas, and the text ends with,}. Print snapshots with theconsole.logformatter, deletepretty_format.rs#46729 rewrites those lines.BuildMessageandResolveMessage.console.table()renders ANSI escape sequences instead of escaping them #32223, open PR console.table: escape control characters in string cells #32224. Not changed here.console.table(new Set([url]))prints an empty row where Node prints the value underValues. That is the column model, not the layout.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.
Requestwith a body: no comma betweenheadersand the body.BloborFile: a second line feed afteroffset, and the comma on a line of its own.offseton the first line.Related open PRs
console.logformatter, deletepretty_format.rs#46729 moves bun:test onto the console formatter. It removes the secondConsoleFormatterimplementor, sowrite_breakandsingle_lineconflict there insrc/jsc/lib.rs.SingleLineHooks::OneLinewith autil.inspectcall with the same option pair. If it lands, the enum argument can go.AOT host table
b.hostObject(inspectOptionsCell, ...)insrc/codegen/builtins/host/runtime-dependencies.builtins.tstells 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 withoutotherNameshas the listed fields alone. In single-line mode that is no longer exact.AOTBuiltinTableTests.cppin oven-sh/WebKit (builtin-table/host-uses-of-startup-loads) assertsoptions->fields.size() == 4for it. WithbreakLengthandcompactadded as optional fields,test/aot/engine/native-4.test.tsfails withASSERTION FAILED: options != table().hostObjects().end() && options->prototype == Builtins::ObjectPrototype && options->fields.size() == 4.bun build --compile --aoton 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 toinspect, and the built-in hooks ofURL,TransformStreamandPerformanceMark. The same holds with--aot-validateand with the run-time checks (run-parallel.ts --checks-at-run-time).test/aot/parallel/closed-world/custom-inspect-method-reads-one-line-options.jskeeps that true. If the compiler starts to rely on the row, this program fails.Open question
The two option keys are added with
putDirectNamedon the cached 3-slot object. That is the extra 32 B allocation. A second cached 5-slotStructureremoves it and costs one more lazy field onZigGlobalObject.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_arraygate 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 withoutcompactdiffer. 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).print_custom_formatted_objectandJSC__JSValue__callCustomInspectFunction). The options object is 48 B in 1 cell on both builds.bun:jscheapStats).Response14,534 to 14,754,Requestwith a body 9,525 to 9,694,File2,475 to 2,567. The cause is onewrite_breakcall 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.bloatyis not installed. Sizes are fromllvm-sizeandllvm-nm.Tests
test/js/bun/console/console-table.test.ts: the six kinds, each throughBun.inspect.table, as a Map value, and through the realconsole.tablein 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 thecompacttext of every native printer, stream bodies,BunRequestwith 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.test/aot/engine/native-4.test.ts,test/internal/source-lints/.bun-inspect-table,console-log,message-event,url, fetchresponse,body,blobandbody-clone,FormData,archive,s3,buffer,perf_hooks,streams,bun-inspect,util, the node inspect tests,bun-build-api.