This repository was archived by the owner on Aug 8, 2026. It is now read-only.
feat(vite-plugin): RSC build manifest for deploy - #50
Closed
agcty wants to merge 21 commits into
Closed
Conversation
agcty
marked this pull request as ready for review
June 15, 2026 21:21
agcty
marked this pull request as draft
June 15, 2026 21:22
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.
Address review: empty worker output dirs up front so the on-disk module walk reflects only the current build (Vite skips emptyOutDir for envs the framework scan-passes mark rendered); include all Cloudflare module kinds (.bin/.txt/.html/.sql) via MODULE_RULES, not just .js/.mjs/.wasm; and remove any stale manifest when a build emits none.
A pure SPA has no worker entry environment in the builder, so the handler returned before reaching the manifest removal, leaving a prior manifest in place. Resolve the manifest root from the entry or client output and remove any stale manifest before the no-worker check.
This was referenced Jun 16, 2026
Contributor
|
Replaced by #55. I adapted a lot of your work so you're listed as a co-author. |
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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
Draft until #47 lands or maintainers confirm this should be reviewed before that merge.
viteEnvironmentchild-environment model.main.feat/rsc-dev...feat/rsc-build.Cloudflare.Viteand has now been live-tested with a one-worker Vite + Durable Object fixture.What
Adds the production-build half of RSC support to the distilled Cloudflare vite plugin. A
vite buildnow emits a__distilled-build.jsonmanifest describing the deployable Worker - its entry, full module set, compatibility data, module types, and static-assets directory - so a deployer such as Alchemy'sCloudflare.Vitecan ship an RSC app without inferring the topology from directory convention or a single environment's in-memory bundle.Dev support is #47. This PR is the build/deploy contract. The deploy-side consumer is alchemy-run/alchemy#615.
Why an explicit manifest, and why on disk
The official
@cloudflare/vite-plugindoes not infer the Worker from directory convention either: it writes an explicit deploy contract to disk (.wrangler/deploy/config.jsonplus per-worker Wrangler config) that Wrangler reads. This PR follows the same pattern, but emits a Wrangler-free, deployer-native manifest - keeping the information a deployer needs while avoiding a Wrangler-specific format.On disk, specifically:
vite buildwritesdist/server/**,dist/ssr/**,dist/client/**, etc.vite buildproduces it, it is inspectable, and it does not couple the consumer to plugin internals. Because Alchemy runs the build in-process it could capture the manifest object in memory, but it still has to read the module bundles from disk, so an on-disk manifest is the natural CLI-compatible contract.The single-worker module set
The official plugin can model
rsc -> ssras multiple Workers with service bindings. The distilled plugin's RSC topology currently folds this into one uploaded Worker module set: the worker entry loads child-environment output through module imports, so the deployed Worker is a single Worker whose module set spans the entry (rsc) plus child (ssr) outputs. The manifest preserves the on-disk relative layout so those cross-environment imports resolve after upload.That matches the current distilled runtime model and the Alchemy consumer in #615. Multi-worker/service-binding topology is a future manifest extension, not part of this PR.
The manifest
Current v2 shape:
{ "version": 2, "workers": { "app": { "main": "server/entry.worker.js", "modules": [ { "path": "server/entry.worker.js", "type": "esm" }, { "path": "ssr/index.js", "type": "esm" }, { "path": "ssr/assets-manifest.json", "type": "json" } ], "compatibilityDate": "2026-03-17", "compatibilityFlags": ["nodejs_compat"] } }, "assets": { "directory": "client", "runWorkerFirst": ["/api/*"] } }All paths are POSIX-style and relative to the manifest directory (the build root).
mainis the distilled worker-entry chunk, identified by its module marker, so it is not confused with a framework's own entry. Module kind is explicit (esm,wasm,data,text,json) using the same module rules the build emits.A deployer reads the manifest, uploads
workers.app.modulesas one module set withworkers.app.mainas the entry, and servesassets.directorywith the optional asset-routing settings.buildApp: defer to the frameworkThe plugin previously imposed its own
buildApploop, which double-built every environment on top of plugin-rsc's multi-pass orchestration. It now defers to the framework'sbuildAppand writes the manifest in abuildApphook withorder: "post", after every environment is on disk - the same broad timing hook the official plugin uses.Edge cases handled by design
client, so it is not served as a client asset.Verified
Across both RSC fixtures and a non-RSC worker fixture:
Bun.Transpiler) confirms every relative import in the worker module set resolves within the set and no client assets leak in.mainis always the real worker entry.static-website) emits no manifest and removes any stale prior manifest.format/lint/typecheckare clean.Downstream validation in alchemy-run/alchemy#615:
Cloudflare.Vitereads this v2 manifest and uploads the manifest-selected module set with explicit content types./api/resetreturned ok, and/api/countincremented through the DO.alchemy devalso served the same fixture locally through the distilled runtime and Vite dev server.Limitations & follow-ups
build.outDirwith the RSC topology is unsupported. plugin-rsc keeps thessrchild atdist/ssreven when the entry moves, so the worker output spans two roots. The plugin detects this, emits nothing, and removes any stale manifest rather than producing a broken one.workersbeyondworkers.appafter a deployer needs that shape.Scope
buildAppdeferral.Written on behalf of agcty.