fix: collapse React to a single optimized chunk to prevent duplicate React on cold start#132
Merged
Merged
Conversation
…React on cold start On a cold-started dev server (no `.vite` cache), React was loaded twice on the very first request, producing an `Invalid hook call` error before Vite auto-reloaded the page (#128). Built-in file-system routing without `ssr: true` hit a related failure where the eval'd dev-JSX could not resolve `react/jsx-runtime` (`does not provide an export named 't'`, #124). Both are the same underlying problem: a dependency that `@vitejs/plugin-rsc` reaches through a request-time `client-package-proxy` virtual module (e.g. `@funstack/router`) is invisible to the optimizer's initial scan, so it is only discovered on the first render. That late discovery triggers a re-optimization pass which, mid-flight, corrupts CJS-interop module references — leaving a second copy of React loaded (#128) or a stale dev-JSX runtime (#124). Two coordinated changes: - Pre-bundle the project's directly-declared client packages (deps that list `react` as a peer dependency — the same heuristic plugin-rsc uses) in the browser environment, so they are discovered in the optimizer's initial pass and no re-optimization happens on the first render. See findClientPackages. - Include React/ReactDOM as bare `react`/`react-dom` entries instead of the nested `@funstack/static > react` form. The bare specifiers merge with the copy the optimizer scanner already discovers, so React is bundled into a single optimized chunk. The nested form produced a second, redundant React chunk from the same source file; whenever a re-optimization changed which chunk was canonical, two live React instances ended up loaded. One chunk makes that structurally impossible. Verified: multiple cold starts are clean (single React chunk, no re-optimization, no auto-reload) for fsRoutes/`@funstack/router` with zero user config; full unit suite, dev e2e (24), and build e2e (27) pass across single-entry, multi-entry, ssr-defer, and fs-routing modes. Fixes #128 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Summary
Fixes #128 — on a cold-started dev server (no
.vitecache), React was loaded twice on the very first request, producing anInvalid hook callbefore Vite auto-reloaded. Also fixes the related fsRoutes-without-ssr:truedev failure (#124), where the eval'd dev-JSX could not resolvereact/jsx-runtime(does not provide an export named 't').This is an alternative to #129 that makes the failure mode structurally impossible rather than merely avoided. See the root-cause investigation on #128 for the full analysis.
Root cause (one bug, two symptoms)
A dependency that
@vitejs/plugin-rscreaches through a request-timeclient-package-proxyvirtual module (e.g.@funstack/router) is invisible to the dependency optimizer's initial scan, so it is only discovered on the first render. That late discovery triggers a re-optimization pass which, mid-flight, corrupts CJS-interop module references:react(scanned) and@funstack/static > react. The optimizer dedups them within a pass (one canonical, one re-export), but the re-optimization flips which chunk is canonical. Modules loaded before the flip bind to one real React instance;@funstack/router(optimized after) binds to the other → two live React instances →Invalid hook call.ssr: truefor the dev server (lift this limitation) #124: the eval'd dev-JSX references a pre-re-optimization version ofreact/jsx-runtime, which no longer matches after the pass →does not provide an export named 't'.Both vanish on reload because everything then agrees on the post-re-optimization state — hence the cold-start-only, first-render-only nature.
Fix
Two coordinated changes:
reactas apeerDependency— the same heuristic plugin-rsc uses) in the browser environment, so they are discovered in the optimizer's initial pass and no re-optimization happens on the first render. (findClientPackages)react/react-dominstead of the nested@funstack/static > reactform. The bare specifiers merge with the copy the scanner already discovers, so React is bundled into a single optimized chunk — making the duplicate-React flip impossible even if a re-optimization ever slips through.Why both: bare React alone can't ship — it regresses #124 (that's why the nested form was added in #126). Pre-discovery fixes #124's real cause; the single chunk makes #128 impossible by construction. This is strictly stronger than #129, which keeps the redundant second React chunk and only avoids the flip.
Notes:
@funstack/static(excluded from optimizeDeps) andreact-dom(handled separately) are skipped. Thereact-in-peerDependencieskey excludes build plugins like@vitejs/plugin-react(peer-depends onvite), so no false positives.Verification
Invalid hook callreliably before; after, multiple cold starts are clean (single React chunk, no re-optimization, no auto-reload) with zero user config — verified with a Playwright +.vite/deps/_metadata.jsonharness that asserts both File-system routing requiresssr: truefor the dev server (lift this limitation) #124 and React loads twice in first load from cold started dev server #128.clientPackages.test.ts(fixture-based detection) +index.test.ts(single-chunk + client-env-scoped pre-discovery). Full unit suite (60),typecheck,lint,format:checkpass.Fixes #128
🤖 Generated with Claude Code