Skip to content

fix(server-hmr): metadata routes overwrite page runtime HMR handler#92273

Merged
wbinnssmith merged 11 commits intocanaryfrom
wbinnssmith/server-hmr-react-compiler
Apr 7, 2026
Merged

fix(server-hmr): metadata routes overwrite page runtime HMR handler#92273
wbinnssmith merged 11 commits intocanaryfrom
wbinnssmith/server-hmr-react-compiler

Conversation

@wbinnssmith
Copy link
Copy Markdown
Member

@wbinnssmith wbinnssmith commented Apr 2, 2026

What?

Fix server HMR becoming unresponsive after a metadata route is loaded in the same Node.js process as an app page.

Why?

Turbopack loads separate runtime chunks for app pages and metadata routes (robots.ts, sitemap.ts, manifest.ts, icon.tsx, etc.) in the same Node.js process. Each runtime chunk embeds dev-nodejs.ts and produces a distinct __turbopack_server_hmr_apply__ closure bound to its own moduleFactories and devModuleCache.

Previously each runtime simply overwrote globalThis.__turbopack_server_hmr_apply__, so the last chunk to load silently won. Navigating to /robots.txt before an HMR update caused the metadata route runtime to overwrite the page runtime's handler. Subsequent HMR updates were dispatched only to the metadata route runtime, which has no knowledge of the page module — the page appeared frozen and stopped reflecting file changes.

How?

Replace the bare assignment with a multicast registry:

  1. Each runtime appends its own __turbopack_server_hmr_apply__ handler to globalThis.__turbopack_server_hmr_handlers__[].
  2. The first runtime to register installs a shared dispatcher as globalThis.__turbopack_server_hmr_apply__ that iterates all registered handlers at call time (not install time), so newly loaded runtimes are always included.
  3. On full cache reset, hot-reloader-turbopack.ts resets __turbopack_server_hmr_handlers__ to [] so stale handlers from evicted chunks don't accumulate into the next generation.

Because dev-nodejs.ts is embedded into the Turbopack binary via include_dir!, this fix requires rebuilding the native binary.

Tests

test/development/app-dir/server-hmr/server-hmr.test.ts — extended with metadata route hmr tests that load a metadata route before patching a page file, verifying HMR updates still reach the page runtime after a second runtime chunk is loaded.

@nextjs-bot nextjs-bot added created-by: Turbopack team PRs by the Turbopack team. Documentation Related to Next.js' official documentation. tests Turbopack Related to Turbopack with Next.js. type: next labels Apr 2, 2026
@wbinnssmith wbinnssmith changed the title fix: server HMR broken with react-compiler and metadata routes fix(server-hmr): metadata routes break server HMR when loaded before page runtime Apr 2, 2026
@nextjs-bot
Copy link
Copy Markdown
Collaborator

nextjs-bot commented Apr 2, 2026

Tests Passed

@wbinnssmith wbinnssmith force-pushed the wbinnssmith/server-hmr-react-compiler branch from d730ad5 to ca4c9bd Compare April 2, 2026 17:15
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Apr 2, 2026

Merging this PR will not alter performance

✅ 17 untouched benchmarks
⏩ 3 skipped benchmarks1


Comparing wbinnssmith/server-hmr-react-compiler (b2f3ec6) with canary (f065bf5)

Open in CodSpeed

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@nextjs-bot
Copy link
Copy Markdown
Collaborator

nextjs-bot commented Apr 2, 2026

Stats from current PR

✅ No significant changes detected

📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 456ms 455ms ▁█▅▅▅
Cold (Ready in log) 441ms 441ms ▁▂▅▇▄
Cold (First Request) 1.093s 1.083s ▆▁▁▂▂
Warm (Listen) 456ms 456ms █▁██▁
Warm (Ready in log) 442ms 439ms ▂▂▇█▁
Warm (First Request) 344ms 339ms ▃▄▇█▄
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 456ms 455ms ▁▁▅▁▅
Cold (Ready in log) 434ms 434ms ▁▃▆▂▂
Cold (First Request) 1.912s 1.926s ▇▇▇▆▁
Warm (Listen) 455ms 456ms ▁▁▁▁▁
Warm (Ready in log) 435ms 434ms ▁▂▅▁▂
Warm (First Request) 1.943s 1.949s ▆▇█▆▁

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 3.928s 3.952s ▇▆██▁
Cached Build 3.840s 3.898s ▄███▄
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 14.374s 14.337s ▁▂▅▂▂
Cached Build 14.463s 14.456s ▁▂▇▃▄
node_modules Size 488 MB 488 MB █████
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles
Canary PR Change
02fkg8wfh0iju.js gzip 9.19 kB N/A -
050zwt5xh_0tx.js gzip 10.4 kB N/A -
087fzjd-gvlzv.js gzip 450 B N/A -
0cz1d0mv5g_q7.js gzip 39.4 kB 39.4 kB
0gg9vd4d42ysn.js gzip 155 B N/A -
0ppxcl_z43mad.js gzip 8.52 kB N/A -
0stk-tfxgx6s3.js gzip 161 B N/A -
17vpypn74v37h.js gzip 156 B N/A -
19gog_gke730n.js gzip 168 B N/A -
19oha6-znmkcv.js gzip 8.55 kB N/A -
1elt1qium-r2m.css gzip 115 B 115 B
1ogy9445ihjv_.js gzip 157 B N/A -
2_36-yzit1z47.js gzip 157 B N/A -
2_5rjb7lqxntf.js gzip 221 B 221 B
2-_zedt0-5b4-.js gzip 157 B N/A -
219prxwxgaalc.js gzip 7.61 kB N/A -
26elcgxnn9zjd.js gzip 8.52 kB N/A -
28vf2dsziua0m.js gzip 70.8 kB N/A -
2900hudr6gvm0.js gzip 2.28 kB N/A -
2b-qdapbk0_23.js gzip 158 B N/A -
2dmjqczehgzg_.js gzip 156 B N/A -
2lv2js3kmdeho.js gzip 8.48 kB N/A -
2rehygrd36hqv.js gzip 8.58 kB N/A -
2srwswih0m9_h.js gzip 13.3 kB N/A -
2v1rzj-4r9mjo.js gzip 157 B N/A -
3-jz00s4w-r6h.js gzip 13 kB N/A -
3-p9p9mheqhzx.js gzip 8.55 kB N/A -
31030bryqpolg.js gzip 8.53 kB N/A -
31dx5nmrzzuy7.js gzip 225 B N/A -
36al5_90rqsno.js gzip 153 B N/A -
3925v09gtu-5k.js gzip 49 kB N/A -
39x4zj5mjb4d_.js gzip 9.77 kB N/A -
3k-48b78ys_vy.js gzip 10.1 kB N/A -
3lh57e9tbpp5k.js gzip 156 B N/A -
3m7-5rfj0avoz.js gzip 12.9 kB N/A -
3o29-jkxrkk63.js gzip 160 B N/A -
3uqce_6sa526g.js gzip 8.47 kB N/A -
3yurjqk-sjs3y.js gzip 1.46 kB N/A -
40ybjx9c192n0.js gzip 13.8 kB N/A -
421vzwdt9j1b_.js gzip 5.62 kB N/A -
43zbv3-q9h7d1.js gzip 65.7 kB N/A -
turbopack-01..uti9.js gzip 4.18 kB N/A -
turbopack-04..nlgr.js gzip 4.18 kB N/A -
turbopack-0e..w8_v.js gzip 4.18 kB N/A -
turbopack-0y..i5li.js gzip 4.18 kB N/A -
turbopack-0y..-ad-.js gzip 4.18 kB N/A -
turbopack-1n..c46w.js gzip 4.18 kB N/A -
turbopack-2g..z66t.js gzip 4.19 kB N/A -
turbopack-2h..mlzp.js gzip 4.18 kB N/A -
turbopack-2j..bmwo.js gzip 4.18 kB N/A -
turbopack-2p..b7tk.js gzip 4.18 kB N/A -
turbopack-2y..tc-v.js gzip 4.18 kB N/A -
turbopack-3_..bu9z.js gzip 4.16 kB N/A -
turbopack-32..g08x.js gzip 4.18 kB N/A -
turbopack-3g..tyy5.js gzip 4.18 kB N/A -
03dgzoo-qf3sm.js gzip N/A 9.19 kB -
05tx5f25dlivn.js gzip N/A 8.53 kB -
0c7ez6p2qc57f.js gzip N/A 5.62 kB -
0duvj3qk5pvgn.js gzip N/A 13.8 kB -
0epwnk2m75vvt.js gzip N/A 65.7 kB -
0euowdr-zfgpn.js gzip N/A 157 B -
0m-34rm9w_wpm.js gzip N/A 7.6 kB -
0qnwuk92m8i7o.js gzip N/A 10.4 kB -
0r4wrn6n0ue2m.js gzip N/A 8.55 kB -
0rp0fodtbt_6m.js gzip N/A 8.52 kB -
0sfck-km4dl1k.js gzip N/A 8.47 kB -
0x0xuhmxzwkp8.js gzip N/A 8.47 kB -
1-wdvgxnzicj7.js gzip N/A 1.46 kB -
10nk0013u_0ot.js gzip N/A 158 B -
11u6nxujb2eg4.js gzip N/A 450 B -
1358q39tl51ro.js gzip N/A 70.8 kB -
17k1_b_sj3nf3.js gzip N/A 156 B -
1eetj6eamfvuf.js gzip N/A 160 B -
1jknla7i0176h.js gzip N/A 158 B -
1jv-o1_s-zmua.js gzip N/A 49 kB -
1pu9skbtkl89b.js gzip N/A 155 B -
1u93khd-ql03d.js gzip N/A 158 B -
20f9n8uqh6te6.js gzip N/A 160 B -
21pkweq15g4zs.js gzip N/A 157 B -
2e2z-03lx4fjc.js gzip N/A 13 kB -
2h5d27gd6q4za.js gzip N/A 153 B -
2je-6c8ipbl3k.js gzip N/A 156 B -
2k6v2hbc1--uv.js gzip N/A 169 B -
2k9ax08cjl2id.js gzip N/A 12.9 kB -
2lms6k76q5-6m.js gzip N/A 13.3 kB -
2qx4twi9i3xus.js gzip N/A 2.28 kB -
2srnqic6tvxxd.js gzip N/A 8.52 kB -
30l7m4nayp73a.js gzip N/A 8.55 kB -
3h_ecpiaatwgc.js gzip N/A 10.1 kB -
3iovodzm4-o4b.js gzip N/A 157 B -
3ity0aahajapd.js gzip N/A 225 B -
3wrhpuc-j1aw9.js gzip N/A 9.77 kB -
43mlw9dy_8f02.js gzip N/A 8.58 kB -
turbopack-0_..biw3.js gzip N/A 4.18 kB -
turbopack-01..zbxm.js gzip N/A 4.18 kB -
turbopack-0a..i5-9.js gzip N/A 4.19 kB -
turbopack-1c..tcho.js gzip N/A 4.18 kB -
turbopack-1j..rijn.js gzip N/A 4.18 kB -
turbopack-1o..rysz.js gzip N/A 4.18 kB -
turbopack-1r..5zc-.js gzip N/A 4.18 kB -
turbopack-27.._zmw.js gzip N/A 4.18 kB -
turbopack-38..70-b.js gzip N/A 4.18 kB -
turbopack-3s..tkk4.js gzip N/A 4.16 kB -
turbopack-3u..gyzk.js gzip N/A 4.18 kB -
turbopack-3w..xccy.js gzip N/A 4.18 kB -
turbopack-3z..kcm_.js gzip N/A 4.18 kB -
turbopack-41..33w8.js gzip N/A 4.18 kB -
Total 464 kB 464 kB ✅ -29 B

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 721 B 715 B
Total 721 B 715 B ✅ -6 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 432 B 434 B
Total 432 B 434 B ⚠️ +2 B

📦 Webpack

Client

Main Bundles
Canary PR Change
5528-HASH.js gzip 5.54 kB N/A -
6280-HASH.js gzip 60.7 kB N/A -
6335.HASH.js gzip 169 B N/A -
912-HASH.js gzip 4.59 kB N/A -
e8aec2e4-HASH.js gzip 62.8 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB
main-app-HASH.js gzip 255 B 254 B
main-HASH.js gzip 39.4 kB 39.3 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
262-HASH.js gzip N/A 4.59 kB -
2889.HASH.js gzip N/A 169 B -
5602-HASH.js gzip N/A 5.55 kB -
6948ada0-HASH.js gzip N/A 62.8 kB -
9544-HASH.js gzip N/A 61.4 kB -
Total 235 kB 235 kB ⚠️ +580 B
Polyfills
Canary PR Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Total 39.4 kB 39.4 kB
Pages
Canary PR Change
_app-HASH.js gzip 194 B 194 B
_error-HASH.js gzip 183 B 180 B 🟢 3 B (-2%)
css-HASH.js gzip 331 B 330 B
dynamic-HASH.js gzip 1.81 kB 1.81 kB
edge-ssr-HASH.js gzip 256 B 256 B
head-HASH.js gzip 351 B 352 B
hooks-HASH.js gzip 384 B 383 B
image-HASH.js gzip 580 B 581 B
index-HASH.js gzip 260 B 260 B
link-HASH.js gzip 2.51 kB 2.51 kB
routerDirect..HASH.js gzip 320 B 319 B
script-HASH.js gzip 386 B 386 B
withRouter-HASH.js gzip 315 B 315 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.98 kB 7.98 kB ✅ -1 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 125 kB 126 kB
page.js gzip 273 kB 273 kB
Total 398 kB 398 kB ⚠️ +175 B
Middleware
Canary PR Change
middleware-b..fest.js gzip 618 B 613 B
middleware-r..fest.js gzip 156 B 155 B
middleware.js gzip 44.3 kB 44.5 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 45.9 kB 46.1 kB ⚠️ +151 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 715 B 718 B
Total 715 B 718 B ⚠️ +3 B
Build Cache
Canary PR Change
0.pack gzip 4.38 MB 4.38 MB
index.pack gzip 113 kB 113 kB
index.pack.old gzip 114 kB 114 kB
Total 4.61 MB 4.6 MB ✅ -2.78 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 342 kB 342 kB
app-page-exp..prod.js gzip 189 kB 189 kB
app-page-tur...dev.js gzip 341 kB 341 kB
app-page-tur..prod.js gzip 189 kB 189 kB
app-page-tur...dev.js gzip 338 kB 338 kB
app-page-tur..prod.js gzip 187 kB 187 kB
app-page.run...dev.js gzip 338 kB 338 kB
app-page.run..prod.js gzip 187 kB 187 kB
app-route-ex...dev.js gzip 76.6 kB 76.6 kB
app-route-ex..prod.js gzip 52.2 kB 52.2 kB
app-route-tu...dev.js gzip 76.6 kB 76.6 kB
app-route-tu..prod.js gzip 52.2 kB 52.2 kB
app-route-tu...dev.js gzip 76.2 kB 76.2 kB
app-route-tu..prod.js gzip 52 kB 52 kB
app-route.ru...dev.js gzip 76.2 kB 76.2 kB
app-route.ru..prod.js gzip 52 kB 52 kB
dist_client_...dev.js gzip 324 B 324 B
dist_client_...dev.js gzip 326 B 326 B
dist_client_...dev.js gzip 318 B 318 B
dist_client_...dev.js gzip 317 B 317 B
pages-api-tu...dev.js gzip 43.8 kB 43.8 kB
pages-api-tu..prod.js gzip 33.4 kB 33.4 kB
pages-api.ru...dev.js gzip 43.8 kB 43.8 kB
pages-api.ru..prod.js gzip 33.4 kB 33.4 kB
pages-turbo....dev.js gzip 53.2 kB 53.2 kB
pages-turbo...prod.js gzip 39 kB 39 kB
pages.runtim...dev.js gzip 53.2 kB 53.2 kB
pages.runtim..prod.js gzip 39 kB 39 kB
server.runti..prod.js gzip 62.8 kB 62.8 kB
Total 3.03 MB 3.03 MB ⚠️ +2 B
📎 Tarball URL
https://vercel-packages.vercel.app/next/commits/b2f3ec6cc07972372b5a62159a8d71952fad2e7e/next

Turbopack loads separate runtime chunks for app pages and metadata routes
(robots.ts, sitemap.ts, etc.) in the same Node.js process. Each chunk
registers a __turbopack_server_hmr_apply__ handler that is bound to its
own moduleFactories/devModuleCache. Previously each runtime just assigned
to globalThis.__turbopack_server_hmr_apply__, so the last chunk to load
would silently win. Navigating to /robots.txt before an HMR update meant
only the metadata route runtime received the update; the page appeared
frozen.

Fix by using a multicast registry: each runtime appends its own handler
to globalThis.__turbopack_server_hmr_handlers__[], and a shared dispatcher
installed on first registration calls all of them. On full cache reset,
hot-reloader-turbopack resets the array so stale handlers from evicted
chunks don't accumulate.

Co-Authored-By: Claude <[email protected]>
@wbinnssmith wbinnssmith changed the title fix(server-hmr): metadata routes break server HMR when loaded before page runtime fix(server-hmr): metadata routes overwrite page runtime HMR handler Apr 2, 2026
@wbinnssmith wbinnssmith force-pushed the wbinnssmith/server-hmr-react-compiler branch from ca4c9bd to 3051225 Compare April 2, 2026 18:28
@wbinnssmith wbinnssmith marked this pull request as ready for review April 2, 2026 18:41
Copy link
Copy Markdown
Contributor

in client side code we prevent multiple copies of the runtime from loading with a runtime test

turbopack/crates/turbopack-ecmascript-runtime/src/browser_runtime.rs see the if (Array.isArray(globalThis[{}]) ... part

should we do the same thing server side? do we really need multiple copies of the server runtime to be loaded?

@wbinnssmith
Copy link
Copy Markdown
Member Author

in client side code we prevent multiple copies of the runtime from loading with a runtime test

turbopack/crates/turbopack-ecmascript-runtime/src/browser_runtime.rs see the if (Array.isArray(globalThis[{}]) ... part

should we do the same thing server side? do we really need multiple copies of the server runtime to be loaded?

I briefly took a look at merging the module caches and runtimes but it seemed pretty difficult given they don't share the same chunking context (ssr vs rsc modules). @sokra how possible is this?

Comment thread turbopack/crates/turbopack-ecmascript-runtime/js/src/nodejs/dev/dev-nodejs.ts Outdated
let applied = false
for (const fn of fns) {
try {
if (fn(update)) applied = true
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.

should we break, after the first runtime accepts?

also if we have different chunking contexts can we filter to the expected runtime based on asset prefixes. just wondering if this will cause us to install modules from the wrong runtime.

wbinnssmith and others added 3 commits April 6, 2026 17:13
`!fns` is unreachable (globalThis.__turbopack_server_hmr_handlers__ is
always set to an array before any handler call). Replace it with a
comment explaining why we do not break on the first accepting runtime
and why per-runtime moduleFactories closures make asset-prefix filtering
unnecessary.

Co-Authored-By: Claude <[email protected]>
@wbinnssmith wbinnssmith force-pushed the wbinnssmith/server-hmr-react-compiler branch from 80ffbfb to 7c5a384 Compare April 6, 2026 22:26
Comment thread turbopack/crates/turbopack-ecmascript-runtime/js/src/nodejs/dev/dev-nodejs.ts Outdated
Comment thread turbopack/crates/turbopack-ecmascript-runtime/js/src/nodejs/dev/dev-nodejs.ts Outdated
wbinnssmith and others added 4 commits April 6, 2026 23:00
The registry was reset to [] after a require cache clear, but dev-nodejs.ts
now stores handlers in a Map (keyed by __filename for routing by chunkPrefix).
Reading the [] back via ?? new Map() failed because ?? doesn't treat [] as
nullish, so .set() was called on an Array, crashing on page reload.

Co-Authored-By: Claude <[email protected]>
…ewrite

The sourcesContent in the runtime source map was still referencing the old
Array-based multicast implementation. Updated to reflect the current
Map-based routing dispatcher introduced in 7c5a384.

Co-Authored-By: Claude <[email protected]>
…t single-line update

The previous snapshot commit wrote a single-line JSON blob, but the Rust snapshot
test framework expects the multi-line per-section format. Restore the correct format
while keeping the updated dev-nodejs.ts sourcesContent/mappings for the Map-based
routing dispatcher.

Co-Authored-By: Claude <[email protected]>
…y for toCall

- Replace startsWith prefix matching with path.dirname exact comparison so
  chunk updates are routed to the runtime whose output directory contains that
  specific chunk, not any runtime whose prefix is a prefix of the chunk path.
- Change toCall from Set<HmrHandlerEntry> to HmrHandlerEntry[] as suggested.
  A seen-key Set handles deduplication when multiple chunk paths fall in the
  same runtime directory.
- Update both snapshot files to match the new compiled output.

Co-Authored-By: Claude <[email protected]>
wbinnssmith and others added 2 commits April 7, 2026 00:36
…rbopack output

Turbopack's sections_to_rope generates {"offset": {"line": N, "column": 0}
with spaces, but a previous Python-based snapshot update wrote compact JSON
{"offset":{"line":N,"column":0} without spaces. Rewrite the file using the
exact format the Rust serializer produces.

Co-Authored-By: Claude <[email protected]>
Use the test framework's own snapshot recording instead of manual Python
reconstruction to ensure exact format parity.

Co-Authored-By: Claude <[email protected]>
@wbinnssmith wbinnssmith merged commit 1fe590f into canary Apr 7, 2026
174 checks passed
@wbinnssmith wbinnssmith deleted the wbinnssmith/server-hmr-react-compiler branch April 7, 2026 01:32
eps1lon pushed a commit that referenced this pull request Apr 7, 2026
…92273)

### What?

Fix server HMR becoming unresponsive after a metadata route is loaded in
the same Node.js process as an app page.

### Why?

Turbopack loads separate runtime chunks for app pages and metadata
routes (`robots.ts`, `sitemap.ts`, `manifest.ts`, `icon.tsx`, etc.) in
the same Node.js process. Each runtime chunk embeds `dev-nodejs.ts` and
produces a distinct `__turbopack_server_hmr_apply__` closure bound to
its own `moduleFactories` and `devModuleCache`.

Previously each runtime simply overwrote
`globalThis.__turbopack_server_hmr_apply__`, so the last chunk to load
silently won. Navigating to `/robots.txt` before an HMR update caused
the metadata route runtime to overwrite the page runtime's handler.
Subsequent HMR updates were dispatched only to the metadata route
runtime, which has no knowledge of the page module — the page appeared
frozen and stopped reflecting file changes.

### How?

Replace the bare assignment with a multicast registry:

1. Each runtime appends its own `__turbopack_server_hmr_apply__` handler
to `globalThis.__turbopack_server_hmr_handlers__[]`.
2. The first runtime to register installs a shared dispatcher as
`globalThis.__turbopack_server_hmr_apply__` that iterates all registered
handlers at call time (not install time), so newly loaded runtimes are
always included.
3. On full cache reset, `hot-reloader-turbopack.ts` resets
`__turbopack_server_hmr_handlers__` to `[]` so stale handlers from
evicted chunks don't accumulate into the next generation.

Because `dev-nodejs.ts` is embedded into the Turbopack binary via
`include_dir!`, this fix requires rebuilding the native binary.

### Tests

`test/development/app-dir/server-hmr/server-hmr.test.ts` — extended with
`metadata route hmr` tests that load a metadata route before patching a
page file, verifying HMR updates still reach the page runtime after a
second runtime chunk is loaded.

---------

Co-authored-by: Will Binns-Smith <[email protected]>
Co-authored-by: Claude <[email protected]>
eps1lon pushed a commit that referenced this pull request Apr 7, 2026
…92273)

### What?

Fix server HMR becoming unresponsive after a metadata route is loaded in
the same Node.js process as an app page.

### Why?

Turbopack loads separate runtime chunks for app pages and metadata
routes (`robots.ts`, `sitemap.ts`, `manifest.ts`, `icon.tsx`, etc.) in
the same Node.js process. Each runtime chunk embeds `dev-nodejs.ts` and
produces a distinct `__turbopack_server_hmr_apply__` closure bound to
its own `moduleFactories` and `devModuleCache`.

Previously each runtime simply overwrote
`globalThis.__turbopack_server_hmr_apply__`, so the last chunk to load
silently won. Navigating to `/robots.txt` before an HMR update caused
the metadata route runtime to overwrite the page runtime's handler.
Subsequent HMR updates were dispatched only to the metadata route
runtime, which has no knowledge of the page module — the page appeared
frozen and stopped reflecting file changes.

### How?

Replace the bare assignment with a multicast registry:

1. Each runtime appends its own `__turbopack_server_hmr_apply__` handler
to `globalThis.__turbopack_server_hmr_handlers__[]`.
2. The first runtime to register installs a shared dispatcher as
`globalThis.__turbopack_server_hmr_apply__` that iterates all registered
handlers at call time (not install time), so newly loaded runtimes are
always included.
3. On full cache reset, `hot-reloader-turbopack.ts` resets
`__turbopack_server_hmr_handlers__` to `[]` so stale handlers from
evicted chunks don't accumulate into the next generation.

Because `dev-nodejs.ts` is embedded into the Turbopack binary via
`include_dir!`, this fix requires rebuilding the native binary.

### Tests

`test/development/app-dir/server-hmr/server-hmr.test.ts` — extended with
`metadata route hmr` tests that load a metadata route before patching a
page file, verifying HMR updates still reach the page runtime after a
second runtime chunk is loaded.

---------

Co-authored-by: Will Binns-Smith <[email protected]>
Co-authored-by: Claude <[email protected]>
wbinnssmith added a commit that referenced this pull request Apr 7, 2026
…92273)

### What?

Fix server HMR becoming unresponsive after a metadata route is loaded in
the same Node.js process as an app page.

### Why?

Turbopack loads separate runtime chunks for app pages and metadata
routes (`robots.ts`, `sitemap.ts`, `manifest.ts`, `icon.tsx`, etc.) in
the same Node.js process. Each runtime chunk embeds `dev-nodejs.ts` and
produces a distinct `__turbopack_server_hmr_apply__` closure bound to
its own `moduleFactories` and `devModuleCache`.

Previously each runtime simply overwrote
`globalThis.__turbopack_server_hmr_apply__`, so the last chunk to load
silently won. Navigating to `/robots.txt` before an HMR update caused
the metadata route runtime to overwrite the page runtime's handler.
Subsequent HMR updates were dispatched only to the metadata route
runtime, which has no knowledge of the page module — the page appeared
frozen and stopped reflecting file changes.

### How?

Replace the bare assignment with a multicast registry:

1. Each runtime appends its own `__turbopack_server_hmr_apply__` handler
to `globalThis.__turbopack_server_hmr_handlers__[]`.
2. The first runtime to register installs a shared dispatcher as
`globalThis.__turbopack_server_hmr_apply__` that iterates all registered
handlers at call time (not install time), so newly loaded runtimes are
always included.
3. On full cache reset, `hot-reloader-turbopack.ts` resets
`__turbopack_server_hmr_handlers__` to `[]` so stale handlers from
evicted chunks don't accumulate into the next generation.

Because `dev-nodejs.ts` is embedded into the Turbopack binary via
`include_dir!`, this fix requires rebuilding the native binary.

### Tests

`test/development/app-dir/server-hmr/server-hmr.test.ts` — extended with
`metadata route hmr` tests that load a metadata route before patching a
page file, verifying HMR updates still reach the page runtime after a
second runtime chunk is loaded.

---------

Co-authored-by: Will Binns-Smith <[email protected]>
Co-authored-by: Claude <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

created-by: Turbopack team PRs by the Turbopack team. Documentation Related to Next.js' official documentation. tests Turbopack Related to Turbopack with Next.js. type: next

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants