docs: RSC + Rspack plugin implementation plan and findings (#3141)#3147
docs: RSC + Rspack plugin implementation plan and findings (#3141)#3147AbanoubGhadban wants to merge 1 commit intomainfrom
Conversation
Investigation of issue #3141 (revise Rspack support and plan RSC + Rspack compatibility). Three docs captured under .claude/docs/: 1. rspack-rsc-support-state.md — state of official Rspack RSC support as of 2026-04-14. Rspack v2 shipped built-in RSC (PR #12012); we decide NOT to adopt it for React on Rails (too coupled to their experiments flag + Layers + rsbuild-plugin-rsc path). 2. rsc-rspack-implementation-plan.md — original high-level investigation plan: current architecture, compatibility analysis, 7 critical discussion points, 6 refactoring opportunities, phased plan with risk matrix. 3. rsc-rspack-plugin-implementation.md — focused plugin plan (this is the decision doc for implementation): - Only RSCWebpackPlugin needs rewriting; the other 4 components (WebpackLoader, server.node, client.node, client.browser) are rspack-compatible as-is (proven by 31-test suite on shakacode/react_on_rails_rsc#test/rspack-compatibility). - Dual-path plugin (webpack path unchanged, rspack path new) behind the existing RSCWebpackPlugin facade. - Rspack path uses standard bundler APIs only — no rspackExperiments.reactServerComponents flag, no rspack.experiments.rsc.createPlugins(), no Layers machinery. - Better discovery technique than current FS walk: loader tags "use client" modules during parse, plugin walks the module graph via compilation.hooks.finishModules. - Known risk: react-server-dom-rspack ships a 394-line chunk-cache patch over react-server-dom-webpack. Need to validate in prototype whether RoR hits the same bug. - Six-phase implementation: prototype → webpack refactor → rspack plugin → optional chunk-cache patch → generator → tests → docs. Companion assets: - shakacode/react_on_rails_rsc#test/rspack-compatibility — 31 runtime compatibility tests for the 4 non-plugin components - /mnt/ssd/demos/rspack-rsc-pure — working pure-rspack RSC demo (no React on Rails, validates the stack is viable today) These docs replace the need for further investigation. Implementation can start from Phase 0 (prototype) in the plugin doc. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e9559d0d51
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
|
||
| ### 1.3 Webpack internals the plugin reaches into | ||
|
|
||
| From `packages/react-on-rails-rsc/src/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js` (vendored copy inside the `react-on-rails-rsc` repo at `/mnt/ssd/react-on-rails-rsc/src/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js:12-19`, 409 lines total): |
There was a problem hiding this comment.
Replace machine-local paths with repository links
This plan documents evidence using absolute paths like /mnt/ssd/..., which are only valid on the author’s machine; collaborators cannot open these references, so key verification points in the investigation become non-reproducible. Please switch these citations to repo-relative paths or GitHub permalinks so the documented findings can be checked by anyone reviewing or implementing the plan.
Useful? React with 👍 / 👎.
Greptile SummaryDocs-only PR adding three Confidence Score: 5/5Safe to merge — docs-only, no code changes, all findings are P2 consistency/clarity issues. No code changes. All remaining findings are P2: a stale claim in the older investigation doc (contradicted and superseded by companion docs in the same PR), machine-local
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["RSCWebpackPlugin constructor\n(facade — unchanged export)"] --> B{"bundler.rspackVersion\nis a string?"}
B -->|Yes — rspack| C["RSCRspackPlugin path\n(new file)"]
B -->|No — webpack| D["Existing webpack path\n(unchanged)"]
C --> E["Tiny loader: detect\n'use client' directive\nRecord in shared Set"]
E --> F["compilation.hooks.finishModules\nCollect all tagged modules"]
F --> G["AsyncDependenciesBlock\nper tagged module\n(no custom dep subclass)"]
G --> H["processAssets STAGE_REPORT\nWalk chunkGraph → emit JSON"]
D --> I["FS walk + acorn-loose parse\n(current behavior)"]
I --> J["ClientReferenceDependency\nextends ModuleDependency"]
J --> K["AsyncDependenciesBlock\nper 'use client' file"]
K --> L["processAssets STAGE_REPORT\nWalk chunkGraph → emit JSON"]
H --> M["react-client-manifest.json\n+ react-ssr-manifest.json\n(identical schema for both paths)"]
L --> M
Reviews (1): Last reviewed commit: "docs: Add RSC + Rspack plugin implementa..." | Re-trigger Greptile |
|
|
||
| - **3.2.a — It works.** Runtime test passes; manifests generate correctly. Minimal work required; update docs only. | ||
| - **3.2.b — Build-time plugin fails, runtime works.** We need to write a replacement manifest plugin using rspack-native APIs (`Compilation.PROCESS_ASSETS_STAGE_REPORT` + `sources.RawSource` are supported). The replacement must produce identical JSON shape so `buildServerRenderer` / `buildClientRenderer` keep working unchanged. | ||
| - **3.2.c — Runtime breaks too.** We need to either (i) use a bundled `react-server-dom-rspack` if/when it exists, or (ii) wait for rspack's built-in RSC support. There's no evidence a `react-server-dom-rspack` package exists as of 2026-04. |
There was a problem hiding this comment.
Stale claim contradicts companion doc
This line states react-server-dom-rspack doesn't exist "as of 2026-04", but rspack-rsc-support-state.md (added in this same PR) documents that react-server-dom-rspack@0.0.2 was published on 2026-03-14 and is available on npm. A developer reading only this document would get a factually incorrect picture of the fallback options. Since rsc-rspack-plugin-implementation.md deliberately decided not to use it (§3.3), the stale claim in this contingency branch should be corrected to reflect reality.
| - **3.2.c — Runtime breaks too.** We need to either (i) use a bundled `react-server-dom-rspack` if/when it exists, or (ii) wait for rspack's built-in RSC support. There's no evidence a `react-server-dom-rspack` package exists as of 2026-04. | |
| **3.2.c — Runtime breaks too.** We need to either (i) use `react-server-dom-rspack` (now exists at `0.0.2`, maintained by SyMind/ByteDance — but we're NOT adopting it; see `rsc-rspack-plugin-implementation.md` §3 for rationale) or (ii) wait for rspack's built-in RSC support. |
|
|
||
| - `/mnt/ssd/react-on-rails-rsc/src/WebpackPlugin.ts` — `RSCWebpackPlugin` wrapper | ||
| - `/mnt/ssd/react-on-rails-rsc/src/WebpackLoader.ts` — the `use client` transform loader | ||
| - `/mnt/ssd/react-on-rails-rsc/src/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js` — **the critical 409-line vendored React plugin** using all the webpack internals | ||
| - `/mnt/ssd/react-on-rails-rsc/src/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.production.js:58-112` — proof that runtime uses `__webpack_require__` et al | ||
| - `/mnt/ssd/react-on-rails-rsc/src/{client.node,client.browser,server.node}.ts` — the shakacode-owned API surface on top of React's internals | ||
| - `/mnt/ssd/shakacode-related/shakapacker/package/plugins/rspack.ts`, `rules/rspack.ts` — reference for how shakapacker wires rspack today | ||
| - `/mnt/ssd/shakacode-related/shakapacker/package.json:54-55, 102-103` — rspack peer-dep `^1.0.0` (needs bump for v2) |
There was a problem hiding this comment.
Local machine paths not accessible to other developers
The "External source files" section and the companion plugin-implementation doc (§11 "Working prototype") embed /mnt/ssd/... absolute paths that only exist on the original author's machine. Any developer (or future Claude Code agent) following up on this investigation to start Phase 0 will hit dead-ends on these references. The same pattern appears in rsc-rspack-plugin-implementation.md lines 58 and 608–610.
Consider replacing each /mnt/ssd/... path with the corresponding public URL (GitHub branch/repo) where one exists, or annotating them explicitly as "author-local, not reproducible" so readers know to substitute their own clones.
| # RSC + Rspack Implementation Plan | ||
|
|
||
| Planning/investigation doc for issue [shakacode/react_on_rails#3141](https://github.com/shakacode/react_on_rails/issues/3141). | ||
|
|
||
| **Audience:** React on Rails + Shakapacker maintainer with strong webpack knowledge and no deep rspack internals experience. | ||
|
|
||
| **Goal:** Drive a decision on _how_ to make React Server Components (RSC) run end-to-end under rspack before a single line of production code is written. | ||
|
|
||
| **Status tags used in this doc:** | ||
| - **VERIFIED** — confirmed by source reading in this repo | ||
| - **NEEDS VERIFICATION** — plausible but not runtime-tested | ||
| - **UNKNOWN** — open question | ||
| - **ASSUMED** — working assumption; flag before committing | ||
|
|
There was a problem hiding this comment.
Missing "partially superseded" status banner
rsc-rspack-plugin-implementation.md explicitly calls this document "superseded by this doc for plugin specifics" in its companion-docs header. However, this document carries no equivalent notice — it still presents open questions (§3.2, §3.4, §6 Q1–Q13) that have since been answered by the two companion docs. A reader discovering this file first will spend time on resolved questions without realising they're stale.
Consider adding a status notice at the top, e.g.:
> **Status note (2026-04-14):** The plugin-specific findings in this document have been superseded by [`rsc-rspack-plugin-implementation.md`](./rsc-rspack-plugin-implementation.md). The architecture overview (§1–§2) and risk matrix (§7) remain valid as background context. See that doc for the final decisions on all critical discussion points in §3.| ### 1.2 A working pure-rspack RSC demo (no React on Rails) | ||
|
|
||
| Location: `/mnt/ssd/demos/rspack-rsc-pure`. Runs at http://localhost:3000. Produces full HTML server-side with embedded Flight payload. Uses: | ||
| - `@rspack/core@2.0.0-rc.2` (released 2026-04-14) | ||
| - `react-server-dom-rspack@0.0.2` (maintained by SyMind / ByteDance) | ||
| - `react@19.2.0` + Express 5 + `worker_threads` | ||
|
|
||
| This proves RSC is buildable with rspack end-to-end today, outside React on Rails. |
There was a problem hiding this comment.
Local machine demo path not reproducible
/mnt/ssd/demos/rspack-rsc-pure/ and http://localhost:3000 reference an environment specific to the original author's machine. When a developer (or future Claude agent) picks up Phase 0, these references provide no actionable way to inspect or reproduce the demo. The same issue applies to /mnt/ssd/demos/ror-rspack-demo/ and /mnt/ssd/react-on-rails-rsc/ in §11.
If the pure-rspack demo was derived from rstackjs/rspack-rsc-examples (already cited in §11), that URL is the correct public pointer. Replace the local path with a link to the repo/commit used, or note which example was the starting point.
ReviewOverall: well-researched, ready to merge with one required fix and a few follow-up suggestions. The three docs together provide a solid grounding for the RSC + rspack implementation — the empirical 31-test suite is strong evidence, the risk matrix is thorough, and the phased plan correctly defers production code until the prototype resolves the seven known unknowns. Required before merge
The three new files are not listed in Add entries for all three: ```md
Should address (follow-up acceptable)Local Overlap between the two planning docs is potentially confusing. ```md
MinorFile naming inconsistency. Two docs are named Document freshness. Technical accuracy checkNo issues found. The compatibility scorecard (§2 in the plugin plan), the manifest schema comparison (§4), and the runtime ABI table (§1.3) are internally consistent with the empirical data from the test suite. The decision to avoid rspack's built-in RSC system is well-justified given the tight coupling to |
|
|
||
| ### 1.3 Webpack internals the plugin reaches into | ||
|
|
||
| From `packages/react-on-rails-rsc/src/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js` (vendored copy inside the `react-on-rails-rsc` repo at `/mnt/ssd/react-on-rails-rsc/src/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js:12-19`, 409 lines total): |
There was a problem hiding this comment.
This cites /mnt/ssd/react-on-rails-rsc/... as the source for the table of webpack internals. That path is a dev-machine-specific local clone and won't exist for any other contributor.
Replace with the GitHub path, e.g.:
From `packages/react-on-rails-rsc/src/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js:12-19`
(in the `shakacode/react_on_rails_rsc` repo — branch `test/rspack-compatibility` or main)
| ### External source files (outside this repo) | ||
|
|
||
| - `/mnt/ssd/react-on-rails-rsc/src/WebpackPlugin.ts` — `RSCWebpackPlugin` wrapper | ||
| - `/mnt/ssd/react-on-rails-rsc/src/WebpackLoader.ts` — the `use client` transform loader | ||
| - `/mnt/ssd/react-on-rails-rsc/src/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js` — **the critical 409-line vendored React plugin** using all the webpack internals | ||
| - `/mnt/ssd/react-on-rails-rsc/src/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.production.js:58-112` — proof that runtime uses `__webpack_require__` et al | ||
| - `/mnt/ssd/react-on-rails-rsc/src/{client.node,client.browser,server.node}.ts` — the shakacode-owned API surface on top of React's internals | ||
| - `/mnt/ssd/shakacode-related/shakapacker/package/plugins/rspack.ts`, `rules/rspack.ts` — reference for how shakapacker wires rspack today | ||
| - `/mnt/ssd/shakacode-related/shakapacker/package.json:54-55, 102-103` — rspack peer-dep `^1.0.0` (needs bump for v2) | ||
|
|
||
| ### Rspack references | ||
|
|
||
| - Rspack docs: https://rspack.dev/ | ||
| - Rspack webpack API compatibility: https://rspack.dev/guide/migration/webpack-compat |
There was a problem hiding this comment.
This whole "External source files" block lists /mnt/ssd/... dev-machine paths. They're meaningless to anyone else and will mislead a future agent trying to follow these references.
Each entry has a corresponding GitHub location:
react-on-rails-rscfiles →https://github.com/shakacode/react_on_rails_rsc/blob/<branch>/src/...shakapackerfiles →https://github.com/shakacode/shakapacker/blob/main/package/...
Either replace with the GitHub URLs or drop this section and rely on the repo link in §8's "Tracking issues and PRs".
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
WalkthroughThree new planning and investigation documents have been added to the Changes
Estimated code review effort🎯 1 (Trivial) | ⏱️ ~5 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsTimed out fetching pipeline failures after 30000ms Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| constructor(options: Options) { | ||
| const bundler = options.bundler || require('webpack'); | ||
| const isRspack = typeof bundler.rspackVersion === 'string'; | ||
|
|
There was a problem hiding this comment.
The rspackVersion property used here as a discriminator is not part of rspack's documented public API. Before shipping this, confirm it's stable across rspack versions by checking @rspack/core's main export (node_modules/@rspack/core/dist/index.js).
If it is present and stable, add a comment explaining the heuristic so a future maintainer doesn't silently break it:
// rspack exposes `rspackVersion` on its top-level export; webpack does not.
// This is our bundler-detection heuristic — verify it exists in @rspack/core ≥ 1.x.
const isRspack = typeof bundler.rspackVersion === 'string';If it turns out to be undocumented, a more robust alternative is checking the module ID:
const isRspack = require.resolve('@rspack/core') === require.resolve(bundler) ||
typeof bundler.rspackVersion === 'string';|
|
||
| ### Working prototype (for reference during Phase 0) | ||
| - `/mnt/ssd/demos/rspack-rsc-pure/` — running pure-rspack RSC app | ||
| - `/mnt/ssd/demos/ror-rspack-demo/` — React on Rails + rspack + no RSC (from earlier in this investigation) |
There was a problem hiding this comment.
Same issue as the implementation plan's "External source files" section: these are dev-machine local paths (/mnt/ssd/...) that won't exist for another contributor or a CI agent following this plan.
Replace with GitHub links:
rspack-rsc-puredemo → link to therstackjs/rspack-rsc-examplesrepo cited in §11 References (already there)react-on-rails-rsctest branch →https://github.com/shakacode/react_on_rails_rsc/tree/test/rspack-compatibility(already cited in §1.1)
This section can probably just be dropped since the GitHub links appear in §11 already.
Implements the rspack equivalent of RSCWebpackPlugin, producing the same manifest JSON schema React on Rails already consumes. Zero dependencies on rspack's experimental RSC system (rspackExperiments.reactServerComponents, experiments.rsc.createPlugins, react-server-dom-rspack). Uses only standard bundler-compatible public APIs, so it works on both rspack and webpack 5. Architecture (follows plan in shakacode/react_on_rails#3147): - loader.ts — tiny "use client" detector. Checks for a directive at the top of each source (single/double quote, optional semicolon, allows leading whitespace/BOM/shebang). Records tagged module's resource path on the compilation object. - shared.ts — single string constant for the loader↔plugin channel key. - plugin.ts — the main plugin. On apply(): 1. Injects the loader rule at position 0 with `enforce: 'pre'` so it runs before transpilers and sees original source. 2. Hooks `compilation.hooks.processAssets` at PROCESS_ASSETS_STAGE_REPORT to walk tagged modules, look up their chunks via `compilation.chunkGraph.getModuleChunks(module)`, and emit the manifest JSON via `compilation.emitAsset`. 3. Resolves bundler namespace automatically (compiler.rspack or compiler.webpack); falls back to require('@rspack/core') or require('webpack'). Key design choices: - Module graph walk instead of FS walk. Dead code is automatically excluded (unreachable "use client" files aren't parsed → not tagged). Verified by tests/rspack-plugin/fixtures/dead-code. - No custom ModuleDependency subclass. Avoids the webpack-internal API that rspack doesn't expose. Relies on chunk-graph walking instead. - Manifest schema unchanged from the webpack plugin: { moduleLoading: { prefix, crossOrigin }, filePathToModuleMetadata: { "file:///...": { id, chunks, name } } } so server.node.ts / client.node.ts / client.browser.ts work unchanged. - No chunk force-splitting in initial impl. Manifest is still correct (chunks point to whatever chunks actually contain the tagged modules). AsyncDependenciesBlock-based splitting can be added later as an optimization without breaking the schema. Testing (tests/rspack-plugin/ — 29 tests, all passing): - manifest emission (5 tests) — default filenames for client vs. server, custom filename, valid JSON, deterministic. - top-level shape (7 tests) — exact keys, moduleLoading.prefix reflects publicPath, crossOrigin handles all three values (false/anonymous/ use-credentials). - client-module detection (5 tests) — includes "use client" files, excludes non-"use client" files, handles nested files, excludes dead code (unreachable files), produces empty manifest when no clients. - directive edge cases (7 tests) — single/double quote, with/without semicolon, leading whitespace, rejects directive in comment, rejects directive after an import (must be first statement). - per-entry shape (5 tests) — keys are file:// URLs, entries have id/chunks/name, name is "*", id is non-empty string, chunks is a flat pair-array of [id, filename, ...]. - option validation — throws on missing or non-boolean isServer. Tests follow rspack-manifest-plugin's pattern: fixture-per-scenario, helper compile() function that spawns rspack in a child process, assertions on parsed manifest JSON. Total suite runtime: ~8s. Exports added via package.json: react-on-rails-rsc/RspackPlugin — the plugin class react-on-rails-rsc/RspackLoader — the loader (for users who need it directly; the plugin injects it automatically) Related: - shakacode/react_on_rails#3147 — design doc and investigation - shakacode/react_on_rails#3141 — parent issue - test/rspack-compatibility branch — 31 tests proving the other 4 components of react-on-rails-rsc are rspack-compatible unchanged Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Captures the investigation of #3141 and a concrete plan for implementing rspack support in the RSC plugin. Docs only — no code changes. Adds three documents under
.claude/docs/that together answer: what rspack's RSC story looks like today, which parts ofreact-on-rails-rscare already compatible, and exactly how to make the remaining part (the plugin) work.Key findings
RSCWebpackPluginneeds rewriting. The other four components inreact-on-rails-rsc—WebpackLoader,server.node,client.node,client.browser— work with rspack as-is. Proven empirically by a 31-test suite onshakacode/react_on_rails_rsc#test/rspack-compatibilityincluding a full encode→decode round trip with both sides bundled by rspack.rspackExperiments.reactServerComponents+experiments.rsc.createPlugins()+react-server-dom-rspack). It's too coupled to rspack's experimental Layers + Rsbuild path. Instead, write our own plugin using standard bundler-compatible APIs.__webpack_require__,__webpack_chunk_load__, and a mutable__webpack_require__.u— the three globals React's Flight client relies on — with identical semantics.asyncfield). Our rspack plugin emits RoR's existing schema; no changes toserver.node.ts,client.node.ts, orclient.browser.ts.react-server-dom-rspack@0.0.2ships a 394-line chunk-cache patch overreact-server-dom-webpack. May or may not affect RoR — decide in prototype.Documents added
rspack-rsc-support-state.md(533 lines) — state of official rspack RSC support as of 2026-04-14: verifiedreact-server-dom-rspackexists, rspack v2 built-in RSC is live (PR web-infra-dev/rspack#12012), working pure-rspack demo built and validated.rsc-rspack-implementation-plan.md(46KB, 8 sections) — original high-level investigation: current architecture, compatibility matrix, 7 critical discussion points, 6 refactoring opportunities, 4-phase plan, 14-row risk matrix.rsc-rspack-plugin-implementation.md(NEW, 618 lines) — focused plugin plan; the decision doc for implementation. Covers:AsyncDependenciesBlockwithout custom dep subclass)Why this is docs-only
The implementation work itself is blocked on Phase 0 (prototype) to validate seven specific unknowns (rspack dep-type for
AsyncDependenciesBlock.addDependency, the chunk-cache issue, module ID formats, etc.). Rather than guess at these, the plan commits them to a short prototype and defers implementation until we have answers.Test plan
yarn testlocally — no code changes, nothing to run*WebpackConfig.js) — must NOT land concurrentlyNext steps after merge
rsc-rspack-plugin-implementation.md🤖 Generated with Claude Code
Summary by CodeRabbit