Repository navigation
refactor(vite): lazy per-router chunk carries only derived match data + rule #1 bundle guard - #751
Merged
Merged
Conversation
The lazy virtual:rsc-router/routes-manifest/<routerId> module re-exported the name->path map (__flat(NamedRoutes), or a second inlined JSON.parse copy for routers without a gen file) even though the eager manifest module already sets the identical map via setRouterManifest(). ensureRouterManifest then overwrote the eagerly-set map with the lazy copy, so a divergence between the two paths would have been silently masked by last-writer-wins. The lazy module now carries only the derived match data (trie + precomputedEntries); a bare side-effect import of the gen file is kept as the module-graph edge that invalidates the module on route changes in dev. ensureRouterManifest ignores any manifest field on the loaded module and gates purely on the trie. Also adds a bundle guard to build-test-app.setup.ts pinning Bundle Hygiene rule #1: serialized route data lands in exactly one RSC chunk, never in client/ssr, and is referenced only via dynamic import().
commit: |
This was referenced Jul 10, 2026
fix(vite): register per-router manifest loaders in build only — CF dev stale authoritative trie
#752
Merged
ivogt
added a commit
that referenced
this pull request
Jul 10, 2026
…nks, prerender/shell/static discipline (#754) Closes the guard gaps the route-data audit found. Everything asserted here is currently true in real builds but had no automated enforcement: - cloudflare-basic manifest-text-module.test.ts: the workerd Text-module channel (the largest generated artifact, on the preset where cold-start matters most) now pins exactly one staged .txt per router, no dual channel (route data must not ALSO ship as an inline JSON.parse literal), and no leak of the module or its payload bytes into the client bundle. - examples/cloudflare-multi-router e2e/bundle-guard.test.ts (new): parses the registerRouterManifestLoader wiring out of the built RSC entry (the exact routerId -> chunk mapping the runtime uses) and asserts one lazy chunk per router, pairwise-distinct content, dynamic-import-only references, and a clean client bundle. Multi-router is where the #751 regression class compounds and nothing inspected its dist before. - build-test-app.setup.ts: prerender/shell/static artifact discipline — __pr-*/__ps-*/__st-* payload chunks are RSC-only and never statically imported; __static-manifest.js (the one eagerly-imported generated artifact, required before static-store.ts evaluates) stays a thunk-only lookup table with a size tripwire for O(#Static handlers) eager growth. All detectors red-proven against planted regressions (payload copied into ssr, payload marker in the static manifest, planted inline dual-channel literal, duplicated chunk content). Guards are production-only by nature (build artifacts; dev has no dist), following the manifest-text-module precedent.
This was referenced Jul 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem statement
The lazy per-router virtual module (
virtual:rsc-router/routes-manifest/<routerId>) re-exported the route name->path map even though the eager manifest module already sets the identical map viasetRouterManifest(). On first request,ensureRouterManifest()overwrote the eagerly-set map with the lazy copy — two write paths intoperRouterManifestMapwhere last-writer-wins would silently mask any future divergence between them. For routers without a gen file, the map was genuinely inlined twice (once in the eager module, once in the lazy chunk) — duplicated route data in two chunks, the exact shape Bundle Hygiene rule #1 exists to prevent, just below the size where anyone noticed. Separately, rule #1 (route data in exactly one lazy chunk) had no build-time guard at all — only the dev-React check existed inbuild-test-app.setup.ts.No consumer-visible change: public API, generated
.gen.tsfiles, and runtime behavior are identical;reverse()/href()resolve from the same eagerly-set map as before.Before vs. after (generated lazy per-router module)
Before:
After:
The bare import is load-bearing: it is the edge that invalidates the per-router module when routes change in dev. Without it, a Cloudflare dev program reload re-imports the cached module and
ensureRouterManifest()re-installs a stale authoritative trie (wrong 404s until full restart). Pinned by a codegen unit test.Semantics
setRouterManifest(), then overwritten by lazymod.manifeston first requestsetRouterManifest()only; loadermanifestfield ignored__flat+ trie/entriesensureRouterManifestearly returnmanifestMap.has && trieMap.hastrieMap.has(trie is the only lazy-supplied data)__flat()pass + duplicate map allocation + GC of replaced copyCode-change summary
src/vite/discovery/virtual-module-codegen.ts—generatePerRouterModuledrops themanifestexport in both branches; keeps a bare side-effect gen-file import with a comment explaining why it must stay.src/route-map-builder.ts—ensureRouterManifestno longer readsmod.manifest; early return gates on the trie only. Eager coverage is 1:1 with loader registration (both emitted from the same loop,discover-routers.ts:239/:267).e2e/build-test-app.setup.ts— new bundle guard: serialized route data (detected as aJSON.parsestring literal >= 8 KB; measured 40 KB trie vs 1.7 KB next-largest) must land in exactly one RSC chunk, zero client/SSR chunks, and be referenced only via dynamicimport()(forbidden static-import shape matched directly so bundler formatting changes false-negative rather than failing CI). SharedcollectJsFileswalker feeds both this and the existing dev-React guard.src/build/__tests__/per-router-manifest.test.ts,src/vite/discovery/__tests__/virtual-module-codegen.test.ts— tests updated/added to pin the new contract, including: a straymanifestfield on a lazy module must not clobber the eagerly-set map.docs/manifests.md— documents the data-only lazy chunk and the bare-import invalidation edge.Test plan
pnpm run typecheckpnpm run test:unit:all— includes new/updated pins:virtual-module-codegen.test.ts"carries only derived match data" (module shape + bare import),per-router-manifest.test.ts"stray manifest field must not clobber the eagerly-set map"pnpm run lint,pnpm run formatimport.metado not false-positivebuild-test-appsetup (guard) green; test-app rebuilt — lazy chunk starts atvar trie = JSON.parse(, single dynamic reference from entryroute-types-hmr --project=hmr-routes: 17 passed, 1 flaky (flaky on baseaf355625too)hmr.test.ts,hmr-burst-save): passsetRouterManifest+ 4 lazy loaders, each per-router chunk data-onlyNotes
tests/cloudflare-basic/e2e/hmr-route-mutations.test.ts"converges the open page to the catch-all when the viewed route is removed" fails persistently and identically on baseaf355625(verified by stash + rebuild + rerun); its serial mode blocks 7 later tests in that file.sourceFilebinding. Skipped: "perRouterManifestDataMap values are write-only" (false positive —propagateDiscoveryStateconsumes the values,router-discovery.ts:975); relocating the guard into the plugin'sgenerateBundle(out of scope; repo convention keeps bundle guards in the setup test — noted as a possible future deepening sincechunk.imports/dynamicImportswould remove the size heuristic entirely).RANGO_MANIFEST_TEXT) has noJSON.parseliteral and is not covered by this guard; it remains covered by codegen unit tests.