You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add first-class Rspack support to react-on-rails-rsc by shipping a new RSCRspackPlugin that emits the same manifest schema the package already consumes. Today only RSCWebpackPlugin exists, and it cannot run under rspack because it reaches into webpack/lib/dependencies/* internals that rspack does not expose.
Design and investigation are fully captured in shakacode/react_on_rails#3147. That PR lands three docs under .claude/docs/:
rspack-rsc-support-state.md — state of official rspack RSC support (v2 RC, built-in but experimental and tightly coupled to Rsbuild / Layers)
rsc-rspack-implementation-plan.md — original high-level investigation
rsc-rspack-plugin-implementation.md — the decision doc for the plugin design
Why this is scoped to just the plugin
Empirically verified on branch test/rspack-compatibility (31 tests across 7 suites, including a full Flight encode→decode round trip where both sides are bundled by rspack):
File
Rspack-compatible?
src/WebpackLoader.ts
Yes — only uses this.resourcePath
src/server.node.ts
Yes — Node built-ins + react-dom only
src/client.node.ts
Yes — uses __webpack_require__ / __webpack_chunk_load__ (rspack emits these identically)
src/client.browser.ts
Yes — runtime globals only; __webpack_require__.u is monkey-patchable
src/types.ts
Yes
src/WebpackPlugin.ts
No — wraps a vendored plugin that imports webpack/lib/dependencies/ModuleDependency, webpack/lib/dependencies/NullDependency, webpack/lib/Template, and calls contextModuleFactory.resolveDependencies() which rspack does not expose
The first runtime failure under rspack is deterministic:
TypeError: contextModuleFactory.resolveDependencies is not a function
at react-server-dom-webpack-plugin.js:345:38
What we are explicitly NOT doing
Not adopting rspack's built-in RSC (rspackExperiments.reactServerComponents + experiments.rsc.createPlugins()). It is coupled to rspack's Layers machinery + the builtin:swc-loader flag + the Rsbuild path, and is still experimental.
Not depending on react-server-dom-rspack (ByteDance/SyMind wrapper). It ships a 394-line chunk-cache patch over the vendored react-server-dom-webpack and pins us to their release cadence.
Not changing the manifest schema. The new plugin emits the exact JSON shape that server.node.ts, client.node.ts, and client.browser.ts already consume — so zero runtime changes anywhere else.
Plugin design (summary)
Dual-path behind the existing RSCWebpackPlugin facade:
compiler.rspack / compiler.webpack detection at apply() time
Webpack path unchanged (back-compat preserved)
Rspack path uses only standard public bundler APIs
Discovery technique is a strict improvement over the current FS walk in both plugins:
Loader tags modules during parse. A tiny loader (enforce: 'pre', matches /\.[cm]?[jt]sx?$/) checks each source for a top-of-file "use client" directive (accepting both quote styles, optional semicolon, leading whitespace / BOM / shebang). If present, the loader records this.resourcePath on a compilation-scoped Set via a well-known key (not a module-level singleton — avoids clashes in parallel tests).
Plugin collects the set in processAssets at stage PROCESS_ASSETS_STAGE_REPORT. By then every reachable module has been parsed and (if client) tagged.
Per-module chunk lookup via compilation.chunkGraph.getModuleChunks(module) + getModuleId(module). No need to walk all chunk groups; no need for a custom ModuleDependency subclass.
Emit the manifest via compilation.emitAsset + sources.RawSource.
Advantages over the current FS walk:
No directory / include regex; works with any resolver config (aliases, node_modules, conditional exports, virtual modules)
Dead code is excluded automatically (unreachable "use client" files are never parsed → never tagged)
Single pass, no double I/O
Acceptance criteria
src/react-server-dom-rspack/{loader.ts,plugin.ts,shared.ts} — new files, self-contained
src/WebpackPlugin.ts dispatches to rspack path when compiler.rspack or bundler.rspackVersion is present (back-compat: pre-existing constructors keep behaving identically on webpack)
Manifest JSON is byte-compatible with the webpack plugin's output (filePathToModuleMetadata with file:// URL keys, moduleLoading.prefix from publicPath, crossOrigin derived from outputOptions.crossOriginLoading)
Test suite covering: manifest emission, top-level shape, directive edge cases, multiple clients, dead-code exclusion, per-entry shape, option validation
Package exports add react-on-rails-rsc/RspackPlugin and react-on-rails-rsc/RspackLoader
Handles ConcatenatedModule (scope-hoisted) correctly — walk module.modules for inner resources
Webpack refactor — parameterize vendored plugin to take an injected bundler handle (no behavior change)
Rspack plugin — production implementation
Chunk-cache patch (conditional) — only if the prototype reproduces the bug that react-server-dom-rspack patched around
Generator updates in react_on_rails
Testing infra + docs
This repo owns phases 1–4; phases 5–6 happen in react_on_rails.
Known unknowns to resolve in the prototype
Which built-in dep type to pass to AsyncDependenciesBlock.addDependency() if/when we decide to force per-client-file chunk splits (likely deferrable — the plugin's initial version can rely on rspack's existing splitChunks behavior and still produce a correct manifest).
Whether the chunkCache patch from react-server-dom-rspack is required for React on Rails. Tested by exercising concurrent client-component loads in the prototype; apply the patch only if the bug reproduces.
Whether compilation.chunkGraph.getModuleChunks(module) returns the right chunks for modules ending up in named async chunks.
Whether the tagging loader coexists cleanly with users' other SWC/Babel loaders (must run first — enforce: 'pre').
Module ID format under rspack (plain ./src/Dialog.tsx vs. layer-prefixed (server-side-rendering)/./src/Dialog.tsx) and whether it round-trips through __webpack_require__(id) at runtime.
compilation.entrypoints.forEach vs. compilation.chunkGroups.forEach semantics on rspack.
outputOptions.crossOriginLoading defaults on rspack.
Summary
Add first-class Rspack support to
react-on-rails-rscby shipping a newRSCRspackPluginthat emits the same manifest schema the package already consumes. Today onlyRSCWebpackPluginexists, and it cannot run under rspack because it reaches intowebpack/lib/dependencies/*internals that rspack does not expose.Design and investigation are fully captured in shakacode/react_on_rails#3147. That PR lands three docs under
.claude/docs/:rspack-rsc-support-state.md— state of official rspack RSC support (v2 RC, built-in but experimental and tightly coupled to Rsbuild / Layers)rsc-rspack-implementation-plan.md— original high-level investigationrsc-rspack-plugin-implementation.md— the decision doc for the plugin designWhy this is scoped to just the plugin
Empirically verified on branch
test/rspack-compatibility(31 tests across 7 suites, including a full Flight encode→decode round trip where both sides are bundled by rspack):src/WebpackLoader.tsthis.resourcePathsrc/server.node.tsreact-domonlysrc/client.node.ts__webpack_require__/__webpack_chunk_load__(rspack emits these identically)src/client.browser.ts__webpack_require__.uis monkey-patchablesrc/types.tssrc/WebpackPlugin.tswebpack/lib/dependencies/ModuleDependency,webpack/lib/dependencies/NullDependency,webpack/lib/Template, and callscontextModuleFactory.resolveDependencies()which rspack does not exposeThe first runtime failure under rspack is deterministic:
What we are explicitly NOT doing
rspackExperiments.reactServerComponents+experiments.rsc.createPlugins()). It is coupled to rspack's Layers machinery + thebuiltin:swc-loaderflag + the Rsbuild path, and is still experimental.react-server-dom-rspack(ByteDance/SyMind wrapper). It ships a 394-line chunk-cache patch over the vendoredreact-server-dom-webpackand pins us to their release cadence.server.node.ts,client.node.ts, andclient.browser.tsalready consume — so zero runtime changes anywhere else.Plugin design (summary)
Dual-path behind the existing
RSCWebpackPluginfacade:compiler.rspack/compiler.webpackdetection atapply()timeDiscovery technique is a strict improvement over the current FS walk in both plugins:
enforce: 'pre', matches/\.[cm]?[jt]sx?$/) checks each source for a top-of-file"use client"directive (accepting both quote styles, optional semicolon, leading whitespace / BOM / shebang). If present, the loader recordsthis.resourcePathon a compilation-scopedSetvia a well-known key (not a module-level singleton — avoids clashes in parallel tests).processAssetsat stagePROCESS_ASSETS_STAGE_REPORT. By then every reachable module has been parsed and (if client) tagged.compilation.chunkGraph.getModuleChunks(module)+getModuleId(module). No need to walk all chunk groups; no need for a customModuleDependencysubclass.compilation.emitAsset+sources.RawSource.Advantages over the current FS walk:
directory/includeregex; works with any resolver config (aliases,node_modules, conditional exports, virtual modules)"use client"files are never parsed → never tagged)Acceptance criteria
src/react-server-dom-rspack/{loader.ts,plugin.ts,shared.ts}— new files, self-containedsrc/WebpackPlugin.tsdispatches to rspack path whencompiler.rspackorbundler.rspackVersionis present (back-compat: pre-existing constructors keep behaving identically on webpack)filePathToModuleMetadatawithfile://URL keys,moduleLoading.prefixfrompublicPath,crossOriginderived fromoutputOptions.crossOriginLoading)react-on-rails-rsc/RspackPluginandreact-on-rails-rsc/RspackLoaderConcatenatedModule(scope-hoisted) correctly — walkmodule.modulesfor inner resourcesPhased implementation (from the design doc)
react-server-dom-rspackpatched aroundreact_on_railsThis repo owns phases 1–4; phases 5–6 happen in
react_on_rails.Known unknowns to resolve in the prototype
AsyncDependenciesBlock.addDependency()if/when we decide to force per-client-file chunk splits (likely deferrable — the plugin's initial version can rely on rspack's existing splitChunks behavior and still produce a correct manifest).chunkCachepatch fromreact-server-dom-rspackis required for React on Rails. Tested by exercising concurrent client-component loads in the prototype; apply the patch only if the bug reproduces.compilation.chunkGraph.getModuleChunks(module)returns the right chunks for modules ending up in named async chunks.enforce: 'pre')../src/Dialog.tsxvs. layer-prefixed(server-side-rendering)/./src/Dialog.tsx) and whether it round-trips through__webpack_require__(id)at runtime.compilation.entrypoints.forEachvs.compilation.chunkGroups.forEachsemantics on rspack.outputOptions.crossOriginLoadingdefaults on rspack.References
*WebpackConfig.js)test/rspack-compatibilityfeat/rspack-plugin