Skip to content

bake: align the error overlay underline with a windowed line excerpt - #37872

Open
robobun wants to merge 1 commit into
farm/f2fefb37/log-caret-windowed-excerptfrom
farm/de419011/bake-overlay-windowed-line-offset
Open

robobun wants to merge 1 commit into
farm/f2fefb37/log-caret-windowed-excerptfrom
farm/de419011/bake-overlay-windowed-line-offset

Conversation

@robobun

@robobun robobun commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • In the dev server error overlay, an error deep inside a long line (anything minified) shows a short excerpt of the line, but the red underline lands well to the right of the error, or past the end of the excerpt. Notes have the same problem.
  • Cause: the excerpt is a window cut out of the line, but the column sent with it still counts from the start of the whole line, and the payload says nothing about how much was cut off in front.
  • The terminal caret had the same bug. logger: align the caret with a left-trimmed line excerpt #37848 fixes that side; this PR is stacked on it and only touches the overlay.

Fix

  • Each serialized location gains one field: the width of the prefix the window dropped (0 when the excerpt is the whole line). The client subtracts it from the column when padding the underline, clamped at 0 so a negative count cannot throw and take the overlay down.
  • This lines up because the column and the offset are both counted in UTF-16 code units, the unit the client indexes the excerpt in, so non-ASCII prefixes work too. The column on the wire keeps its whole-line meaning, as in logger: align the caret with a left-trimmed line excerpt #37848.
  • Adding a field is safe because the payload has one writer and one reader in the same binary; the inspector's bundleFailed event passes the same bytes through untouched.
  • Verification: a dev server test renders the repro and checks the underline lands under the ] in the excerpt, and an inspector test decodes the payload for a long ASCII line and a long é line and checks the offset is where the excerpt starts in the line. Both fail without the fix. The other overlay position tests were run locally and are unchanged.

Background

  • The dev server behind bun ./index.html ("bake" in the tree) does not only print bundle errors to the terminal: it serializes them into a binary payload, embedded in the error page and pushed to the HMR client, which draws the in-browser overlay.
  • That payload has one Rust writer in the dev server and one TypeScript reader in the client, and no version field. The same bytes are also handed to inspector clients as BunFrontendDevServer.bundleFailed.
  • A message location is a line, a 1-based column, a length and a line text. For long lines the line text is a window of about 120 bytes around the error (about 40 before, 80 after), while the column still counts from the start of the line.
  • logger: align the caret with a left-trimmed line excerpt #37848 added line_text_column_offset to that location, the width of the dropped prefix in the same units as the column, and used it for the terminal caret. This PR carries the value to the overlay.
  • The overlay draws the underline as a second line of text under the excerpt: column - 1 filler characters, then length underline characters.
Original description

Stacked on #37848 (this PR's base is that branch, so the diff here is only this PR's own commit). #37848 adds Location.line_text_column_offset and fixes the terminal caret; it leaves the dev server overlay alone, which is what this PR does.

Repro

mkdir x && cd x
printf 'let ok = 1;\n%s]%s\n' "$(printf 'a%.0s' $(seq 100))" "$(printf 'b%.0s' $(seq 100))" > index.ts
echo '<script type="module" src="./index.ts"></script>' > index.html
bun ./index.html

Open the page. The overlay shows the second line as a 120 character window starting 40 characters before the ] (aaaa...a]bbbb...b), but the red underline is drawn 100 characters in, under a b 60 characters to the right of the ]. With a longer line (anything minified) it is drawn past the end of the shown text altogether. Same for notes. Decoding the payload the page embeds shows why: column: 101 next to a lineText that only has 40 characters in front of the ].

Cause

Location::init_or_null (src/ast/lib.rs) windows a long line to about 120 bytes around the error, but column stays relative to the whole line. write_log_data in src/runtime/bake/dev_server/serialized_failure.rs serializes both as they are, and renderCodeLine in src/runtime/bake/client/overlay.ts pads the underline with column - 1 characters under the windowed lineText. That is the overlay's copy of the caret bug #37848 fixes in Data::write_format.

Fix

  • write_log_data writes loc.line_text_column_offset (the width of the part of the line the window dropped, 0 whenever line_text is the whole line) as a u32 after the length. The format has one writer and one reader, both shipped in the same binary (the HMR client and the embedded error page; the inspector's BunFrontendDevServer.bundleFailed payload is the same bytes and is passed through opaquely), so adding a field is safe.
  • readBundlerMessageLocationOrNull reads it into BundlerMessageLocation.lineTextColumnOffset.
  • renderCodeLine pads with column - 1 - lineTextColumnOffset (saturated at 0, like write_format does; a negative count would throw out of repeat and take the whole overlay down).

column itself is deliberately left alone rather than being rewritten to a window-relative value on the wire: it keeps meaning the same thing as BuildMessage.position.column and the logger's at file:line:col, which is also what #37848 chose, and a consumer of the inspector payload that wants to show a real column can still do so. Both values are in UTF-16 code units (ErrorPositionState counts columns that way and the offset is derived from the same scan), which is exactly the unit lineText is indexed in on the client, so the padding lines up for non-ASCII prefixes too. The window is not otherwise changed, and like the terminal output the overlay does not mark the truncation.

Verification

  • test/cli/inspect/BunFrontendDevServer.test.ts: the existing bundleFailed snapshot gains lineTextColumnOffset: 0 for its two short lines, and a new test rewrites utils.ts with two 220 character lines (ASCII and one padded with é), each with a redeclared binding, and decodes the payload with the client's own decoder. It pins the error and note locations of both lines (column: 115 with lineTextColumnOffset: 74 and 91, the notes with 0 and an untrimmed window) and checks that line.indexOf(lineText) === lineTextColumnOffset and that lineText[column - 1 - lineTextColumnOffset] is the declared name. Fails without the change (field missing), passes with it.
  • test/bake/dev/bundle.test.ts: renders the repro above in the dev server client harness and checks that the underline starts 40 characters in (index.ts:2:41 in the harness's underline-derived format, previously 2:101) and that the character under it in the shown excerpt is the ].

Also run locally with the change: the rest of BunFrontendDevServer.test.ts, and bundle, css, hot and html in test/bake/dev (the other tests that assert overlay positions; every location they cover has an offset of 0, so they are unchanged).

While looking at this I noticed that a message whose Location.line is 0 (a throwing dev server plugin produces one) is written with its column/length/text while the client stops reading after the line, which desyncs the rest of the payload. That predates this change and is being handled separately.

Location::init_or_null sends the dev server a ~120 byte window of a long
source line as line_text, but column still counts from the start of the
whole line. The overlay drew the underline column - 1 characters into the
window, so for any error more than 40 bytes into a long line it ended up
far to the right of the token, or past the end of the shown text.

Serialize Location.line_text_column_offset (the width of the part of the
line the window dropped) after the location length, read it back on the
client, and start the underline at column - 1 - lineTextColumnOffset.
column itself is unchanged so it keeps referring to the whole line, like
the logger's "at file:line:col" suffix does.
@robobun

robobun commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

Reproduced on bun 1.4.0: for let ok = 1; followed by a 201 character line with a ] at column 101, the dev server's failure payload carries column: 101 next to a 120 character lineText window that has only 40 characters in front of the ], so the overlay underline is drawn 60 characters to the right of it (test/bake/dev/bundle.test.ts reports index.ts:2:101 for the underline position, expected 2:41; the inspector test's decoded locations have no offset field).

Fix is in this PR (one commit on top of #37848, which it is stacked on). Both new tests fail without the change and pass with it on a debug build; the remaining BunFrontendDevServer.test.ts tests and the bundle, css, hot, html dev server suites still pass.

CI for this PR's commit: https://buildkite.com/bun/bun/builds/93349 (the bot comments below refer to the base branch's build because of the stacked base).

@robobun

robobun commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 9:05 AM PT - Aug 12th, 2026

@robobun, your commit 9ac595e is building: #93262

@robobun

robobun commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 2:05 PM PT - Aug 12th, 2026

❌ @robobun, your commit 0b0beb8 has some failures in Build #93349 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 37872

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

bun-37872 --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.

LGTM — small, self-contained fix mirroring #37848's terminal-caret change for the dev server overlay.

What was reviewed:

  • Wire format: confirmed error-serialization.ts is the only decoder of write_log_data's output; the inspector agent passes buildErrorsPayloadBase64 through opaquely, so inserting a u32 between length and lineText is safe.
  • renderCodeLine is the only consumer of BundlerMessageLocation.column for underline padding; the runtime-error code-preview path uses a separate structure and serialization, so it is correctly out of scope.
  • Math.max(0, …) guards .repeat() against a negative count (would otherwise throw and blank the overlay), matching Data::write_format's saturating subtraction.
  • Tests pin ASCII and two-byte UTF-8 prefixes, note locations, and the invariant lineText[column - 1 - offset] === name; the bake harness's errors matcher derives its column from .space length, so index.ts:2:41 directly exercises the fix.
Extended reasoning...

Overview

This PR fixes the dev server error overlay's underline position when the bundler windows a long source line. It adds one u32 field (line_text_column_offset, introduced on bun_ast::Location by the base PR #37848) to the serialized failure payload in serialized_failure.rs, reads it in error-serialization.ts as lineTextColumnOffset, and subtracts it in overlay.ts's renderCodeLine when computing the spacer width. Two tests are added (a wire-format decode test with ASCII + é-padded lines in BunFrontendDevServer.test.ts, and an end-to-end DOM test in test/bake/dev/bundle.test.ts), plus the existing bundleFailed snapshot gains lineTextColumnOffset: 0.

Security risks

None. This is a purely cosmetic rendering fix in the dev-server error overlay. The new field is a u32 written by trusted native code and read by the bundled HMR client; no user-controlled input reaches new parsing.

Level of scrutiny

Low-to-medium. The change is a three-line runtime edit (one write, one read, one arithmetic adjustment) that mirrors an already-landed pattern from #37848. The wire format is internal (single writer in serialized_failure.rs, single reader in error-serialization.ts, both shipped in the same binary; the inspector agent forwards the payload as an opaque base64 string). I verified there are no other decoders in src/ or packages/, and that renderCodeLine is the only place BundlerMessageLocation.column is used to position an underline — the runtime-error preview in updateRuntimeErrorOverlay uses a different CodePreview struct sourced from /_bun/report_error, so it is correctly untouched.

Other factors

  • The Math.max(0, …) clamp is defensive and correct: a negative argument to String.prototype.repeat throws, which would take down the whole overlay.
  • Test coverage is thorough for a UI fix: exact-value assertions on the decoded locations (both error and note, ASCII and multi-byte prefix), the structural invariant line.indexOf(lineText) === lineTextColumnOffset, and a DOM-level check that excerpt[underlineStart] === ']'. The bake harness's error matcher (client-fixture.mjs:551) derives its :col from .space length, so the errors: ['index.ts:2:41: …'] assertion is exactly the changed padding.
  • The pre-existing line == 0 writer/reader desync noted in the PR description is unchanged by this PR (it already writes column/length/lineText that the reader skips); adding one more u32 to that already-desynced tail doesn't change its observable behavior, and the author is handling it separately.
  • No CODEOWNERS on the touched paths; no outstanding reviewer comments.

@robobun

robobun commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

Nothing to change from the review above. One note for anyone reading the CI comments on this PR: because the PR is based on #37848's branch, the status comments above point at that branch's build (#93262, flaky-only failures so far). The build for this PR's own commit (0b0beb8) is https://buildkite.com/bun/bun/builds/93349.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant