Repository navigation
fix(vite): register per-router manifest loaders in build only — CF dev stale authoritative trie - #752
Merged
Merged
Conversation
In Cloudflare dev, the eager routes-manifest module registered lazy per-router manifest loaders just like a production build. After a program reload, ensureRouterManifest ran before the handler's buildRouterTrieFromUrlpatterns fallback, imported the cached per-router virtual module (its only invalidation edge — the gen-file import — lags an urls.tsx edit by one reload cycle), and installed its STALE trie as authoritative. A browser full-reload landing in that window rendered a removed route from the stale trie and never converged; misses were hard 404s instead of falling back to regex over live routes. This violated the documented invariant that dev tries are never authoritative (route-map-builder.ts) and preempted the fallback handler.ts promises. Loaders are now registered in BUILD only; dev deterministically rebuilds the per-router trie from live router.urlpatterns (fresh, non-authoritative). clearAllRouterData additionally clears the loader registry so a loader can never outlive the data it closes over. Fixes the persistently-failing cloudflare-basic e2e 'converges the open page to the catch-all when the viewed route is removed' (red on main, green with this change; unblocks the 7 serially blocked tests in hmr-route-mutations.test.ts).
commit: |
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
In Cloudflare dev, removing a route while the browser had it open could leave the page permanently rendering the removed route: the workerd program reload triggered a browser full-reload, but the reloaded worker served the removed route from a stale route trie marked authoritative, so the page never converged to the catch-all — and because the trie was authoritative, misses on other routes were hard 404s instead of falling back to live matching. This is the persistently-failing e2e
hmr-route-mutations › converges the open page to the catch-all when the viewed route is removed(red on every run of current main), whose serial failure also blocked the 7 tests after it in that file.Root cause: the eager routes-manifest module registered lazy per-router manifest loaders in dev exactly as in a production build.
ensureRouterManifest()runs before the handler'sbuildRouterTrieFromUrlpatternsfallback (router.ts:1101vsrsc/handler.ts:513), so after a program reload it imported the cached per-router virtual module — whose only invalidation edge (the gen-file import) lags anurls.tsxedit by one reload cycle — and installed its stale trie, marking it authoritative (route-map-builder.ts, loader path). That violated the documented invariant that dev tries are never authoritative and preempted the always-correct live-urlpatternsfallback the handler comment promises. The fetch-polling sibling test masked the window; a browser's single full reload landing inside it did not.Consumer-visible effect: CF dev route removal/rename now converges reliably; no production behavior changes.
Before vs. after (generated eager routes-manifest module, dev)
Before (dev and build identical):
After (dev):
Build output is unchanged (loader still registered; trie still authoritative — correct for a trie serialized from complete discovery).
Semantics
router.urlpatterns, non-authoritativepropagateDiscoveryState; loader raced in the re-eval window and could install an authoritative trieclearAllRouterData()Code-change summary
src/vite/discovery/virtual-module-codegen.ts—registerRouterManifestLoaderemission (and its import) gated onstate.isBuildMode, with a comment encoding the stale-authoritative-trie scar tissue. The codegen site is the only viable gate:isBuildModeexists only onDiscoveryState, and gatingmarkRouterTrieAuthoritativedownstream would still install a stale trie whose hits serve removed routes.src/route-map-builder.ts—clearAllRouterData()also clearsrouterManifestLoaders(a surviving loader closes over pre-clear data).src/vite/discovery/__tests__/virtual-module-codegen.test.ts— new describe pinning loaders-are-build-only (dev emits no loader but keeps the eager name→path wiring); defensive-pin annotation on the dev Text-module test.src/build/__tests__/per-router-manifest.test.ts— pins that cleared loaders are never re-invoked.docs/manifests.md— documents the build-only loader contract and the dev rebuild path.Test plan
converges the open page to the catch-all when the viewed route is removedfails on current main (verified this session), passes with the fix; fullhmr-route-mutations.test.tsnow 11/11 (previously 3 passed, 1 failed, 7 serially blocked)pnpm run typecheck,pnpm run test:unit:all,pnpm run lint,pnpm run formatgenerateRoutesManifestModule — per-router loaders are build-only(dev/build codegen shape);clearAllRouterData clears registered loadershmr.test.ts+hmr-burst-savepass; dev routing subset (navigation/routing/wildcard/catch) 50 passedroute-types-hmr --project=hmr-routes18/18build-test-appbundle guard green (build still registers the loader; data chunk still lazy-only)Notes
--repeat-each=3stress one iteration can still fail with the same visible symptom via a different mechanism — the browser's full reload being served by the old worker before the program-reload swap (vite/workerd reload sequencing, orthogonal to the trie). On main the test failed every run; post-fix it only reproduces under back-to-back mutate/restore stress.urlpatternsonce per program reload (this was always the designed fallback; node dev still receives tries viapropagateDiscoveryState, no double build).isBuildModeconjunct inuseTextModulekept deliberately as defense against dev loaders returning.__flat, Text-module/multi-router/prerender guards) are queued as follow-ups.