internal/analysis/rsc-rspack-client-reference-inclusion-decision-record-2026-06-27.md
Record the maintainer-facing history and current decision points for React on Rails Pro + React Server Components + Rspack client-reference manifest generation.
This is an internal decision record, not user docs. It reconstructs the sequence from the first Rspack client-manifest plugin through the lazyCompilation fix, then records which longer-term Rspack inclusion mechanisms remain viable. It exists so future ROR/RORP/RSC work does not rediscover the same traps around:
RSCRspackPlugin inclusion mechanisms;lazyCompilation in Rails dev-server setups;compilation.addInclude versus source-level dynamic import injection;bin/dev because react_on_rails disables top-level Rspack lazyCompilation for generated RSC + Rspack dev-server configs in react_on_rails#4227.RSCRspackPlugin / production-ready Rspack RSC.compilation.addInclude-only client inclusion approach documented below should not be treated as the final client-bundle architecture. It proved one useful fact: addInclude can make the manifest contain real modules without lazy-compilation proxies. It also exposed a blocker: client refs became eager entry code (chunks: []) instead of selective async client-reference chunks.React Flight client references need a manifest entry that maps a file/export to:
Typical shape:
{
"file:///app/javascript/src/HelloServer/components/LikeButton.jsx": {
"id": "./app/javascript/src/HelloServer/components/LikeButton.jsx",
"chunks": ["123", "js/client0-abc123.chunk.js"],
"name": "*"
}
}
chunks is not just decoration. It preserves RSC's selective client-island loading:
chunks means Flight can load only the client component chunks needed by the current RSC payload;chunks: [] means the module is already in the initial/entry chunk, so no extra chunk needs loading.chunks: [] is valid, but it weakens the architecture if many client references are included up front in the generated RSC entry. It keeps correctness, but loses the intended async splitting benefit.
react_on_rails_rsc#29: first Rspack plugin using addIncludereact_on_rails_rsc#29 introduced RSCRspackPlugin as a Rspack-native manifest emitter. The PR described three phases:
clientReferences for 'use client' files;compilation.addInclude / EntryPlugin.createDependency;react-client-manifest.json and react-server-client-manifest.json.The important design constraint already existed: Rspack's Rust-backed dependency model did not expose Webpack's custom dependency/subclass APIs cleanly to JavaScript, so the implementation used public Rspack APIs.
Problem discovered after #29: using addInclude with generated names like client0, client1, etc. creates entry-like chunks, not Webpack-style async child chunks of the Flight runtime. That distinction matters because React Flight expects client-reference chunks to register modules into the runtime that loads the RSC page.
Bad shape:
client ref -> addInclude(name: client0)
-> standalone entry/IIFE/runtime
Desired shape:
client ref -> async block under Flight runtime
-> async chunk registers modules with shared runtime
Separate entry/IIFE chunks can trap modules in a private runtime, duplicate shared dependencies, and break hydration/runtime lookup assumptions.
react_on_rails_rsc#36: server parity/export fixes, then injection-loader directionreact_on_rails_rsc#36 fixed production/server-side issues found after #29:
addInclude needed to reuse an existing server entry name to avoid Rspack node-target chunk/external crashes;setUsedInUnknownWay() so production export mangling did not rename exports that Flight resolves by original name.The final code path moved the client-side inclusion mechanism toward an injection loader. Current react_on_rails_rsc source still has this architecture:
client refs -> loader prepends import("/abs/client-ref") into Flight runtime -> Rspack creates async chunks
Current source references:
Why this mechanism was chosen: for the client bundle, dynamic import() is Rspack's public path for producing real async chunks from JavaScript plugin code. It goes through normal code splitting and produces chunks that register into the shared runtime.
react_on_rails_rsc#38: stabilize injection-loader manifest generationreact_on_rails_rsc#38 fixed failures surfaced after the injection-loader move:
clientReferences rather than every 'use client' file in an app;node_modules from directive detection;Simple terms: #38 made the injection-loader path produce scoped, usable manifests in static/prod-style builds. It was not proof that normal Rspack dev-server lazy-compilation/HMR mode worked.
react_on_rails#3553, react_on_rails_rsc#46, react_on_rails#3556: derive refs from the real RSC graphreact_on_rails#3553 proposed deriving RSC client references from the actual RSC graph instead of broad file scanning.
Related work:
This solved a different problem:
Which files are RSC client refs?
It did not solve:
How should Rspack include those refs so dev-server lazy compilation does not proxy them and so async chunk metadata remains correct?
Graph-derived refs reduce false positives. They do not, by themselves, provide a safe Rspack client-reference inclusion primitive.
react_on_rails#4200 and react_on_rails#4227: normal bin/dev failurereact_on_rails#4200 described the fresh-app normal bin/dev failure. react_on_rails#4213 and react_on_rails#4223 improved diagnostics around that failure first; react_on_rails#4227 later changed the generated dev config so normal bin/dev works.
Failure shape:
/hello_server returned 200;react-client-manifest.json was empty;POST /_rspack/lazy/trigger against the Rails origin and got 404.Root cause:
injection-loader inserted dynamic import("/abs/LikeButton.jsx")
-> Rspack dev-server lazyCompilation proxied dynamic imports
-> manifest builder saw lazy proxies or unbuilt modules, not real client refs
-> server render needed manifest before browser lazy trigger could build modules
Rspack docs say web-target lazy compilation defaults to { entries: false, imports: true }, and lazy compilation proxies unexecuted entries/dynamic imports until runtime requests trigger compilation. Default trigger prefix is /_rspack/lazy/trigger.
Sources:
react_on_rails#4227 shipped the short-term fix:
clientWebpackConfig.lazyCompilation = false;
Only for generated RSC + Rspack dev-server config.
This was intentionally a minimal runtime fix, not a full Rspack RSC architecture rewrite.
react_on_rails#4234 and react_on_rails#4243: docs/doctor follow-upreact_on_rails#4234 improved docs and doctor guidance around the lazyCompilation footgun.
react_on_rails#4243 tracks that the doctor check currently recognizes the generated literal assignment pattern, not every equivalent final config:
clientWebpackConfig.lazyCompilation = false;
Empirical variants that produced effective lazyCompilation: false but still warned included:
Object.assign(clientWebpackConfig, { lazyCompilation: false });clientWebpackConfig.js or rspack.config.js;{ entries: false, imports: false }.addInclude implementation testsThree concrete compilation.addInclude shapes were tested after the lazyCompilation failure. Each shape answered a specific question about whether addInclude can replace source-level dynamic imports for RSC client-reference inclusion. These were June 2026 local implementation tests, not merged package changes; rerun them against current react_on_rails_rsc before using any result as design proof.
addInclude entry/groupResult:
Cannot fulfil chunk condition of external node-commonjs "fs";This is not acceptable as a client-side RSC chunk strategy.
addInclude into an existing generated entryThis approach attached discovered refs to an existing generated RSC entry instead of creating new standalone include entries:
LikeButton.jsx -> generated/HelloServer
Result:
bin/dev Rspack dev-server mode;lazy-compilation-proxy;/_rspack/lazy requests;LikeButton.jsx;chunks: [].Meaning:
LikeButton.jsx was bundled into generated/HelloServer.js up front.
That fixes rendering correctness but loses async client-reference chunk splitting. It should not be considered final unless the project explicitly accepts eager client refs for generated RSC entries and measures the bundle-size/runtime impact.
addIncludeThis approach added discovered refs to every configured entry. It proved that addInclude can force real modules into the build graph, but it polluted unrelated entries, including non-RSC application entries. This is not a viable design.
Ownership was introduced only because addInclude requires a target entry name. Existing injection-loader architecture does not need ownership metadata.
For an addInclude design, ownership means:
Which generated RSC entry caused this client ref to be needed?
Example:
generated/HelloServer -> LikeButton.jsx
generated/ProductServer -> ProductButton.jsx
Without ownership, addInclude choices are all bad:
Native Rspack RSC also cares about ownership. web-infra-dev/rspack#13880 groups client chunks by server-entry ownership and treats shared/multi-owner modules specially.
The Webpack RSC-style client inclusion primitive is roughly:
const block = new webpack.AsyncDependenciesBlock({ name: 'client0' });
block.addDependency(dep);
module.addBlock(block);
This attaches an async dependency block to the Flight runtime module. The bundler emits async chunks that register into the same runtime module table.
Rspack is aware of this API gap:
Module.addBlock(...) and new AsyncDependenciesBlock(...) parity.AsyncDependenciesBlock;block.addDependency;module.blocks;module.addBlock.#8469 is closed, but not because public JavaScript module.addBlock parity exists. It closed after Rspack/Modern.js concluded RSC support could be built with Rspack-provided APIs and native/internal Rspack RSC mechanisms.
As of 2026-06-27, Rspack main exposes read-side block wrappers, not the Webpack-style construction/mutation API:
AsyncDependenciesBlock.dependencies getter;AsyncDependenciesBlock.blocks getter;Module.blocks getter.It does not expose a safe public JS path for:
AsyncDependenciesBlock;Rspack PR #9661 added JS API identity support such as block instanceof AsyncDependenciesBlock, not full block construction/mutation.
Rspack is actively solving RSC, but mostly inside its native/plugin internals rather than by exposing Webpack-compatible JS plugin APIs.
Relevant work:
ClientEntryDependency / ClientEntryModule approach so native RSC is compatible with lazy compilation. The PR explains the same bug class: loader-created import() becomes DynamicImport, lazy compilation proxies it, and Flight expects real modules synchronously after chunk load.AsyncDependenciesBlock creation in crates/rspack_plugin_rsc/src/rsc_entry_module.rs. rspack_plugin_rsc is a Rust crate compiled into the Rspack binary, not an npm package or public JavaScript plugin API.This is useful prior art. It is not a drop-in replacement for RORP today because RORP's RSC integration, generated pack layout, Rails routing, SSR/node-renderer flow, and manifest consumption differ from Rspack native RSC's same-name entry/layer model.
This failure mode is not purely RSC-specific.
Rails split dev topology:
Rails serves HTML: http://localhost:<base>
Rspack dev-server serves JS: http://localhost:<base+1>
If Rspack lazy compilation injects a runtime trigger using the page origin, dynamic import execution can do:
POST /_rspack/lazy/trigger -> Rails -> 404
Evidence outside the RORP RSC fresh-app repro:
lazyCompilation: false.Internal context: Hichee's Rspack migration also disabled top-level lazyCompilation in its Rails/Rspack dev-server wiring; see private PR shakacode/hichee#9508 for the RSC + Rspack migration context.
Implication:
lazyCompilation.serverUrl, or a Rails proxy to Rspack's lazy middleware. This belongs more naturally in Shakapacker than React on Rails.| Mechanism | Client async chunks? | Avoids lazy proxy? | Server manifest parity? | Main problems |
|---|---|---|---|---|
| Injection-loader dynamic imports, lazy off | Yes | Yes | Needs server-side handling/fallbacks depending graph | Loader/runtime matching, module-global loader state, splitChunks guardrails |
| Injection-loader dynamic imports, lazy on | Theoretically yes, practically broken | No | No for normal dev | Lazy proxies dynamic imports; manifest can be empty or proxy-backed |
addInclude with new generated include entries | Looks split, but wrong kind | Yes | Risky | Separate runtime/IIFE, node externals crash, duplicated deps, hydration risk |
addInclude into existing generated owner entry | No: refs become eager (chunks: []) | Yes | Better, but server side needs retest | Loses async client-ref chunking; needs ownership/multi-owner policy |
| Native Rspack RSC plugin | Yes, via internal primitives | Rspack solved in native path | Native model | Bigger architecture migration; not drop-in for RORP |
Public JS module.addBlock / AsyncDependenciesBlock | Desired | Would avoid fake imports | Desired with server handling | Not available today; track Rspack #7174 |
Disabling top-level lazyCompilation for generated RSC + Rspack dev-server config is justified and already shipped.
Reason: the current client inclusion mechanism uses dynamic imports to create async chunks. Rspack lazy compilation proxies those imports. RSC server render needs the manifest before browser lazy triggers can compile modules.
addInclude-only as long-term fix yetTwo tested shapes are bad in different ways:
chunks: []).A client-side addInclude design should not proceed unless it proves async chunk preservation or explicitly accepts eager client refs with measured bundle impact.
Given missing public Rspack JS AsyncDependenciesBlock mutation APIs, source-level dynamic imports are currently the public Rspack-compatible way for a JS plugin to create async chunks.
The injection-loader remains fragile because it:
But with lazy disabled, it preserves async chunk behavior better than the tested addInclude approaches.
addIncludeCandidate:
Client bundle:
keep injection-loader/dynamic imports to preserve async chunks
disable or exclude lazyCompilation for RSC-critical imports
Server bundle:
use addInclude into existing server entry when needed for manifest parity
reuse existing generated entry names, not new include entries
mark exports used in unknown way
This matches the #36 research direction: client async chunks need bundler code-splitting; server bundle can use existing-entry addInclude because server output is usually merged into one server bundle and does not need browser async chunk semantics. Existing-entry reuse is a hard constraint: new include entries can trigger Rspack node-target external chunk errors such as Cannot fulfil chunk condition of external node-commonjs "fs".
This candidate still needs implementation and validation against current react_on_rails_rsc main before PR work.
Native Rspack RSC has mechanisms RORP needs:
But adopting it in RORP is a larger architecture migration, not a narrow fix for current generated apps.
Rspack public API follow-up
module.addBlock / AsyncDependenciesBlock mutation is planned.Shakapacker lazyCompilation follow-up
lazyCompilation for Rails split dev-server setups.lazyCompilation: false when using rspack serve through Shakapacker;lazyCompilation.serverUrl to the Rspack dev-server origin;RORP hybrid implementation follow-up
react_on_rails_rsc main.bin/dev Rspack dev-server mode;Graph-derived refs with ownership metadata
addInclude or native-like grouping.Tracking issue update
React on Rails / RORP:
RSC package:
Rspack / Shakapacker: