feat(vite-plugin): RSC dev support via viteEnvironment child environments#47
Open
agcty wants to merge 11 commits into
Open
feat(vite-plugin): RSC dev support via viteEnvironment child environments#47agcty wants to merge 11 commits into
agcty wants to merge 11 commits into
Conversation
The dev plugin hardcoded a single worker environment named "ssr". RSC
apps (@vitejs/plugin-rsc: React Router RSC, Waku) run the worker in the
"rsc" environment (react-server condition) and load "ssr" from it at
runtime, so a single-env assumption can't host them — dev crashed with
"registerMissingImport is not supported in dev rsc" and, once past that,
failed SSR with a duplicate-React null dispatcher.
Add a viteEnvironment { name, childEnvironments } option (mirroring the
official @cloudflare/vite-plugin) and generalize the single "ssr" env to
an entry env plus its children:
- each worker env gets the workerd resolve conditions + dependency
pre-bundling (noDiscovery: false), built per-env so each carries its
own optimizeDeps.entries (a shared entries seeded from the rsc main
left the ssr child with no scan root, causing a mid-session
re-optimization that re-hashed and duplicated React)
- dev connects a module runner for the entry env and each child, and
awaits each env's depsOptimizer.init() before its runner imports
- the worker entry environment name is the configured entry (was "ssr")
Default (no viteEnvironment) is unchanged: entry "ssr", no children.
Minimal @vitejs/plugin-rsc starter (react-rsc) and a React Router on RSC
app (react-router-rsc), both wired through the child-environment model
(viteEnvironment: { name: rsc, childEnvironments: [ssr] }) to exercise
RSC dev.
- restore the exact non-RSC optimizeDeps.entries behavior: the per-env input fallback now applies only to multi-environment (RSC) topologies; single-worker apps keep entries solely from an explicit `main` (was a behavioral change vs the original single-"ssr" code) - give the actionable maintainer-style hint when a runner is missing for an environment (point at viteEnvironment.childEnvironments) Verified: RR-RSC fixture still renders; non-RSC (static-website) dev unchanged.
… pattern)
Adds a /worker-render endpoint that loads a custom "worker-ssr" module
from the ssr environment via loadModule("ssr", "worker-ssr") instead of
importing react-dom/server in the rsc worker entry — the blessed pattern
from agcty/vite-rsc-worker-env-repro#1. Proves the distilled plugin
handles: multiple ssr inputs (framework index + worker-ssr), the worker
loading a non-index ssr module cross-environment, and react-dom/server
resolving in the ssr child (it would fail under the rsc react-server
condition). Verified: GET /worker-render → 200 with rendered HTML; / still renders.
Reject viteEnvironment configs that would silently corrupt the generated per-env config via computed keys: name "client" (the reserved browser env), children including "client", children colliding with the entry, and duplicate children. Mirrors @cloudflare/vite-plugin. (review finding)
Boot `vite dev` and assert the RSC routes render: react-rsc (server +
client component + server action) and react-router-rsc (home, /about,
and /worker-render exercising loadModule("ssr","worker-ssr")). Makes RSC
dev support an automated, runnable check (`bun test`). Build-mode is a
separate track and intentionally not covered.
A reviewer flagged that the rsc env's resolve.conditions ordering depends on plugin order: with cloudflare() before rsc(), react-server lands at index 5 instead of 0. Verified via resolveConfig probe that the ordering does change — but export-condition resolution is set-membership (React's own exports key order decides), so react-server being present is what matters, not its array index. Adds vite.config.cf-first.ts (cloudflare before rsc) + a test booting it: the app renders 200 with a working RSC flight stream, proving order-independence. Not a correctness bug; locked so it can't silently regress.
This was referenced Jun 16, 2026
Open
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.
Status / stack
Ready for review and intended to merge first.
Cloudflare.Viteand proves the deploy path end to end. It stays draft until feat(vite-plugin): RSC build manifest for deploy #50 or an equivalent published package version exists.What
Adds React Server Components dev support to the Cloudflare vite plugin by generalizing the hardcoded single
ssrworker environment into an entry environment + child environments, via the sameviteEnvironment: { name, childEnvironments }contract the official@cloudflare/vite-pluginexposes.@vitejs/plugin-rscapps (React Router RSC, Waku) run the worker in therscenvironment (resolved with thereact-servercondition) and loadssrfrom it at runtime viaimport.meta.viteRsc.loadModule("ssr", ...). The single-env assumption could not host that, sovite devfailed two ways:Vite Internal Error: registerMissingImport is not supported in dev rscat startup - therscenv never got the worker treatment (noDiscovery: false), so it carried Vite's throwing deps-optimizer stub.ssrchild (no scan root -> lazy discovery -> re-hash).Approach
viteEnvironment?: { name?, childEnvironments? }(default{ name: "ssr" }, no children -> unchanged) + aworkerEnvironments(options)helper.makeWorkerEnvironment(name, { isEntry })so each carries its ownoptimizeDeps.entries- a sharedentriesleft the child with no scan root, which is what caused the React duplication.ModuleRunnerDO), andawaits each env'sdepsOptimizer.init()before its runner imports, so deps settle in one pass and React stays a singleton. Cross-envloadModuleresolves through the existing__VITE_ENVIRONMENT_RUNNER_IMPORT__path.workerEnvironmentsrejects invalid configs (name"client", child/entry collisions, duplicate children), matching the official plugin.The default path (no
viteEnvironment) produces equivalent config to before, so non-RSC apps are unaffected.What works
vite devrenders end to end on two fixtures:fixtures/react-rsc- minimal@vitejs/plugin-rscstarter.fixtures/react-router-rsc- React Router on RSC (routes, server actions, client components), single-worker child-env topology, plus a/worker-renderroute exercising theloadModule("ssr", "worker-ssr")pattern (worker-sidereact-dom/serverrendering routed through thessrenv rather than imported in therscentry).Server-component HMR is hot (
rsc:update).Tests
bun testper fixture (dev-mode smoke tests that bootvite devand assert the routes/worker-render respond):react-rsc- server + client component + server action render; plus a plugin-order lock (vite.config.cf-first.ts) proving RSC resolves correctly even when the Cloudflare plugin is registered beforersc()(export-condition resolution is set-membership, soreact-server's presence, not its index in the conditions array, is what matters).react-router-rsc- home,/about, and/worker-render.No regression: the existing
static-websitesuite (vite build+ miniflare + playwright) passes;tsc -bis clean across packages.Scope / follow-ups
ssrenv becomes a reload; appears inherent to a worker-hosted ssr env rather than specific to this change. Pointers welcome.viteEnvironmentis threaded at the top level (one worker), withisEntrystanding in for the entry/parent role. If you would prefer it structured per-worker later (forauxiliaryWorkers/ a prerender worker) with an explicit entry-vs-parent split, that should be a follow-up rather than bundled into the dev fix.Written on behalf of agcty.