Skip to content

bake: remap server-side stack frames against the server chunk prefix - #37430

Open
robobun wants to merge 1 commit into
mainfrom
farm/c2cc54b0/server-hmr-sourcemap-prefix
Open

bake: remap server-side stack frames against the server chunk prefix#37430
robobun wants to merge 1 commit into
mainfrom
farm/c2cc54b0/server-hmr-sourcemap-prefix

Conversation

@robobun

@robobun robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Stack traces for server-side code running in the bake dev server (framework mode, where route modules are evaluated on the server) point at the wrong line. With this route module:

// pages/error-page.tsx
export default function ErrorPage() {
  throwError();                        // line 2
  return <div />;
}

function throwError() {                // line 6
  throw new Error("HMR error test");   // line 7
}

a request prints

6 | function throwError() {
    ^
error: HMR error test
      at throwError (pages/error-page.tsx:6:1)
      at ErrorPage (pages/error-page.tsx:1:16)

and with this change it prints

7 |   throw new Error("HMR error test");
            ^
error: HMR error test
      at throwError (pages/error-page.tsx:7:9)
      at ErrorPage (pages/error-page.tsx:2:3)

test/bake/dev/server-sourcemap.test.ts had the wrong positions baked into its expectations (6:16, 6:1, 5:1, 1:28, 3:38, and the churn test's 6 + i), with a comment rationalizing them as "frames remap to the declaration position". This has been the behavior since server source maps were added; the client side is not affected.

Cause

The server HMR chunk and its source map disagree about what precedes the module code.

  • IncrementalGraph::take_js_bundle_to_list_server emits a server HmrChunk as ({ + modules + }). The prefix contains no newline, so the first module starts on generated line 0.
  • source_map_store::Entry::join_vlq ignored its side argument (let _ = side;) and, for every HmrChunk, started the mappings after the line count of the client prefix, self[Symbol.for("bun:hmr")]({\n, i.e. on generated line 1.

So the map for the server chunk is shifted down by one generated line relative to the code. A frame on generated line L is looked up at map line L, which holds the mappings recorded for generated line L - 1, so every frame reports the original position of the previous generated line. For ordinary code that is the line above the statement; the column is whichever mapping on that line happens to precede the frame's generated column (hence 6:16, the ( of function myFunc(, or 3:38, the { of the function declaration). In denser code it can be further off: a react-server-dom frame in the same chunk moved from 3971:15 (a try { line) to 3972:20, the actual Component(props, void 0) call.

The client chunks use the same function but with the client prefix, which is why test/bake/dev/sourcemap.test.ts (exact positions for the initial response and an HMR chunk) passes before and after.

Fix

ChunkKind::prefix(side) returns the code a chunk of that kind starts with on that side (the full HMR runtime for InitialResponse, the one-line client wrapper or the bare ({ for HmrChunk). Both take_js_bundle_to_list* and join_vlq take it from there, and HmrRuntime::from_static derives line_count from the literal, so the emitted code and the map cannot disagree again. join_vlq's side parameter is now used; render_json was already being called with Side::Server for server chunks.

Verification

test/bake/dev/server-sourcemap.test.ts now asserts the full remapped frames (at fn (.../file:line:col)) for the throw site and for each caller, across the initial bundle, an HMR update, a throw in an imported non-page module, and four successive reloads. Without the change in src/ all four tests fail with the positions shown above (6:16 / 1:1, 6:1 / 1:16, 5:1 / 1:28 / 3:38, 6 + i:1); with it they pass.

Also run: test/bake/dev/sourcemap.test.ts (client maps, unchanged behavior) and test/bake/dev/html.test.ts (the /_bun/report_error remapping path, which renders client maps through the same function); cargo clippy -p bun_runtime is clean.

Note on columns: in the first test the page component is async, so React reads error.stack before the error is printed, and that rendering currently places a construct frame at the callee (Error, 7:13) where the printer places it at the new keyword (7:9, what the other tests see). #37396 makes the two agree; once it lands, that one expectation becomes 7:9 (its current version of this file will conflict with this change either way).

While looking at this I also noticed that after any hot update, frames from modules loaded by an earlier server patch stop remapping (every patch is registered under bake://server.patch.js, so the newest patch's map replaces the older ones; visible as at react-stack-bottom-frame (bake://server.patch.js:3242:29) in the HMR test output). That is a separate problem and is not addressed here.


[review] gate passed · iteration 0 · 5 files touched

fails on main (without fix)
ASAN without fix: 4 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bake/dev/server-sourcemap.test.ts
bun test v1.4.0 (178906144)

test/bake/dev/server-sourcemap.test.ts:
Dev server testing directory: /tmp/bun-dev-test-O6wHS9
bun add v1.4.0 (178906144)
Resolving dependencies
Resolved, downloaded and extracted [2]
Saved lockfile

installed react@0.0.0-experimental-603e6108-20241029
installed react-dom@0.0.0-experimental-603e6108-20241029
installed react-server-dom-bun@0.0.0-experimental-603e6108-20241029
installed react-refresh@0.0.0-experimental-603e6108-20241029

6 packages installed [244.00ms]
bun install v1.4.0 (178906144)

Checked 6 installs across 7 packages (no changes) [100.00ms]
�[0;30mdev|�[0m Started development server: http://localhost:39561
�[0;30mdev|�[0m �[32mBundled page in 1920ms�[0m�[2m:�[0m pages/[...slug].tsx �[2m+ 2 more�[0m
�[0;30mdev|�[0m �[0m�[1m1 |�[0m �[0m�[35mexport�[0m �[0m�[35mdefault�[0m �[0m�[35masync�[0m �[0m�[35mfunction�[0m MyPage(params) {
�[0;30mdev|�[0m �[0m�[1m2 |�[0m   myFunc()�[0m�[2m;�[0m
�[0;30mdev|�[0m �[0m�[1m3 |�[0m   �[0m�[35mreturn�[0m �[0m<�[0mh1>{JSO
... (truncated)

release without fix: 4 FAILED
bun test v1.4.0-canary.1 (9008ae7ab)

test/bake/dev/server-sourcemap.test.ts:
Dev server testing directory: /tmp/bun-dev-test-LMzvrB
bun add v1.4.0-canary.1 (9008ae7ab)
Resolving dependencies
Resolved, downloaded and extracted [0]
Saved lockfile

installed react@0.0.0-experimental-603e6108-20241029
installed react-dom@0.0.0-experimental-603e6108-20241029
installed react-server-dom-bun@0.0.0-experimental-603e6108-20241029
installed react-refresh@0.0.0-experimental-603e6108-20241029

6 packages installed [10.00ms]
bun install v1.4.0-canary.1 (9008ae7ab)

Checked 6 installs across 7 packages (no changes) [1.00ms]
�[0;30mdev|�[0m Started development server: http://localhost:38961
�[0;30mdev|�[0m �[32mBundled page in 51ms�[0m�[2m:�[0m pages/[...slug].tsx �[2m+ 2 more�[0m
�[0;30mdev|�[0m �[0m�[1m1 |�[0m �[0m�[35mexport�[0m �[0m�[35mdefault�[0m �[0m�[35masync�[0m �[0m�[35mfunction�[0m MyPage(params) {
�[0;30mdev|�[0m �[0m�[1m2 |�[0m   myFunc()�[0m�[2m;�[0m
�[0;30mdev|�[0m �[0m�[1m3 |�[0m   �[0m�[35mreturn�[0m �[0m<�[0mh1>{JSON�[0m�[3m�[1m.stringify�[0m(params)}�[0m<�[0m/h1>�[0m�[2m;�[0m
�[0;30mdev|�[0m �[0m�[1m4 |�[0m }
�[0;30mdev|�[0m �[0m�[1m5 |�[0m 
�[0;30mdev|�[0m �[0m
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bake/dev/server-sourcemap.test.ts
bun test v1.4.0 (178906144)

test/bake/dev/server-sourcemap.test.ts:
Dev server testing directory: /tmp/bun-dev-test-fA4VAw
bun add v1.4.0 (178906144)
Resolving dependencies
Resolved, downloaded and extracted [0]
Saved lockfile

installed react@0.0.0-experimental-603e6108-20241029
installed react-dom@0.0.0-experimental-603e6108-20241029
installed react-server-dom-bun@0.0.0-experimental-603e6108-20241029
installed react-refresh@0.0.0-experimental-603e6108-20241029

6 packages installed [174.00ms]
bun install v1.4.0 (178906144)

Checked 6 installs across 7 packages (no changes) [100.00ms]
�[0;30mdev|�[0m Started development server: http://localhost:43795
�[0;30mdev|�[0m �[32mBundled page in 1945ms�[0m�[2m:�[0m pages/[...slug].tsx �[2m+ 2 more�[0m
�[0;30mdev|�[0m �[0m�[1m2 |�[0m   myFunc()�[0m�[2m;�[0m
�[0;30mdev|�[0m �[0m�[1m3 |�[0m   �[0m�[35mreturn�[0m �[0m<�[0mh1>{JSON�[0m�[3m�[1m.stringify�[0m(params)}�[0m<�[0m/h1>�[0m�[2m;�[0m
�[0;30mdev|�[0m �[0m�[1m4 |�[0m }
�[0;30mdev|�[0m �[0m�[1m5 |�[0m 

... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     178906144a
  features     baseline

22 deps, 107 codegen, 1176 objects in 805ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1238] install /workspace/bun
bun install v1.4.0-canary.1 (9008ae7ab)

Checked 107 installs across 153 packages (no changes) [14.00ms]
[2/1238] install /workspace/bun/packages/bun-error
bun install v1.4.0-canary.1 (9008ae7ab)

Checked 1 install across 2 packages (no changes) [4.00ms]
[3/1238] gen bindgenv2
[4/1238] gen ErrorCode+*.h
[5/1238] fetch picohttpparser
[picohttpparser] up to date
[6/1238] fetch tinycc
[tinycc] up to date
[7/1237] install /workspace/bun/src/node-fallbacks
bun install v1.4.0-canary.1 (9008ae7ab)

Checked 129 installs across 147 packages (no changes) [14.00ms]
[8/1237] gen .bind.ts → GeneratedBindings.cpp
[9/1237] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[10/1237] gen ProcessBindingConstants.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindingConstants.lut.h from /workspace/bun/src/jsc/bindings/Process
... (truncated)
diff hotspot
src/runtime/bake/dev_server/incremental_graph.rs |  17 +---
 src/runtime/bake/dev_server/mod.rs               |  18 +++-
 src/runtime/bake/dev_server/source_map_store.rs  |  17 +---
 src/runtime/bake/mod.rs                          |  14 ++++
 test/bake/dev/server-sourcemap.test.ts           | 102 +++++++++++------------
 5 files changed, 85 insertions(+), 83 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                              reads  edits  tests
src/runtime/bake/dev_server/incremental_graph.rs      1      2      0
src/runtime/bake/dev_server/mod.rs                    1      2      0
src/runtime/bake/dev_server/source_map_store.rs       2      4      0
src/runtime/bake/mod.rs                               1      1      0
test/bake/dev/server-sourcemap.test.ts                5     11      0

self-review · no surviving concerns

26 concerns were raised and did not survive verification.

join_vlq always skipped one generated line for HmrChunk, the line count of
the client prefix (self[Symbol.for("bun:hmr")]({ plus a newline), but the
server chunk is emitted behind a bare ({ with no newline. Every server-side
frame was therefore looked up one generated line early and remapped to the
position of the previous generated line, usually the line above the
statement.

ChunkKind::prefix(side) now provides the prefix to both the bundle emitter
and the source map stitcher, and the line count is derived from the
literal.
@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced with the released build by running test/bake/dev/server-sourcemap.test.ts as updated here (4 tests fail with the old positions, e.g. at throwError (pages/error-page.tsx:6:1) for a throw on line 7); all 5 pass with this branch's debug build. Client-side maps (test/bake/dev/sourcemap.test.ts, test/bake/dev/html.test.ts) unchanged and passing.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The change centralizes HMR runtime prefix selection by chunk kind and side. Bundle generation and source-map assembly use the shared selection. Dev-server tests now verify exact source-mapped stack frames.

Changes

HMR prefix contract and integration

Layer / File(s) Summary
Centralized HMR prefix contract
src/runtime/bake/mod.rs, src/runtime/bake/dev_server/mod.rs
Adds HmrRuntime::from_static and ChunkKind::prefix for initial, client HMR, and server HMR responses.
Bundle and source-map integration
src/runtime/bake/dev_server/incremental_graph.rs, src/runtime/bake/dev_server/source_map_store.rs
Uses ChunkKind::prefix(side) for bundle prefixes and source-map line offsets.
Exact source-map stack assertions
test/bake/dev/server-sourcemap.test.ts
Adds shared output helpers and exact frame assertions for async errors, HMR, nested imports, and repeated reloads.

Suggested reviewers: jarred-sumner, dylan-conway

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly and concisely describes the primary change: correcting server-side stack-frame source mapping in the bake dev server.
Description check ✅ Passed The description covers the problem, cause, fix, verification steps, test results, and known limitations using the repository template sections.

Comment @coderabbitai help to get the list of available commands.

@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 — centralizing the chunk prefix in ChunkKind::prefix(side) fixes the off-by-one on server maps and makes the emitter and mapper agree by construction.

What was reviewed:

  • Verified ZStr::from_static/as_bytes() exclude the NUL, so from_static(b"({\\0") yields line_count = 0 and the client literal yields 1.
  • Confirmed client paths are unchanged: render_json(.., Side::Client) at DevServer.rs:1774 and render_mappings (client-only /_bun/report_error path) still see the same line counts as before.
  • Checked render_json(.., ChunkKind::HmrChunk, Side::Server) at DevServer.rs:4291 is the server call site that now gets line_count = 0.
  • Test assertions are strictly stronger (full at fn (…file:line:col) regex vs. substring includes); no existing coverage weakened.
Extended reasoning...

Overview

Fixes server-side stack-frame remapping in the bake dev server. source_map_store::Entry::join_vlq was ignoring its side parameter and always offsetting mappings by the client HMR-chunk prefix's newline count (1), while take_js_bundle_to_list_server emits a prefix (({) with 0 newlines — so every server frame remapped one generated line too high. The fix introduces ChunkKind::prefix(side) -> HmrRuntime as the single source for both the emitted prefix and its line count, plus a const HmrRuntime::from_static that derives line_count from the literal. take_js_bundle_to_list{,_server} and join_vlq all pull from it.

Five files touched: the three bake dev-server Rust files plus bake/mod.rs (the from_static helper) and the test file, whose expectations are corrected and tightened.

Security risks

None. This is dev-server source-map line-count arithmetic; no user input, auth, or serialization boundaries are involved.

Level of scrutiny

Medium. The change is small and mechanical, but it rewrites the assertions in an existing test file — REVIEW.md flags that as something to check carefully. I verified each rewritten expectation against the fixture source (line 7 = throw new Error, line 2 = the call, etc.) and confirmed the old expectations were the buggy output, not the correct behavior. The client path is provably unchanged: for (HmrChunk, Client) the new helper returns the same literal with line_count = 1, and for (InitialResponse, Client) it returns get_hmr_runtime(Side::Client) exactly as before. render_mappings still hardcodes Side::Client, which is fine since its only caller (get_parsed_source_maperror_report_request.rs) handles client-reported errors.

Other factors

  • robobun confirms the updated test fails on the released build with the old positions and passes on this branch, satisfying the "test fails for the right reason" bar.
  • test/bake/dev/sourcemap.test.ts and html.test.ts (client maps through the same function) were run and pass unchanged.
  • The (InitialResponse, Server) arm now returns get_hmr_runtime(Side::Server) where the old code used the client runtime's line count regardless; the comment at take_js_bundle_to_list_server notes server InitialResponse is unreachable, so this is a consistency-only change.
  • The PR description flags a known future conflict with #37396 (one column expectation changes from 7:13 to 7:9) and a separate unaddressed issue (server patch maps overwriting each other under bake://server.patch.js) — both correctly scoped out.

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