Skip to content

Commit 5280682

Browse files
justin808claude
andcommitted
feat(generator): scaffold native RSCRspackPlugin for rspack RSC (#3488)
When `assets_bundler: rspack` is detected, the RSC generator now scaffolds the native `RSCRspackPlugin` (from `react-on-rails-rsc/RspackPlugin`) for the server and client bundles instead of `RSCWebpackPlugin`. Webpack projects are unchanged. This is the verified-correct manifest path for Rspack: a controlled A/B on the marketplace demo showed the webpack plugin under Rspack's compat layer producing valid-looking manifests that still failed ~7/11 RSC routes at runtime, while the native `RSCRspackPlugin` (standard rspack public APIs, same manifest schema) rendered and hydrated all routes. The two plugins share the same `{ isServer, clientReferences }` API, so this is a drop-in swap. Changes: - Add `rsc_plugin_class_name` / `rsc_plugin_import_path` helpers to GeneratorHelper (single source of truth, keyed on `using_rspack?`). - Branch the base server/client webpack-config templates on those helpers. - Parameterize the standalone-upgrade migration (rsc_setup + client_references) by bundler, and broaden the plugin-detection regex to match both `RSCWebpackPlugin` and `RSCRspackPlugin` so re-run idempotency stays correct (the fresh `--rsc --rspack` install runs the migration on top of the template). - Update user-facing migration warnings to name the active bundler's plugin. - Update docs/pro/.../rspack-compatibility.md to reflect the native-plugin pivot (kept "experimental" — the stable package + CI gate are still pending in #3488). Targets `react-on-rails-rsc@19.0.5-rc.5`, which exports `./RspackPlugin`. Refs #3488 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b83980f commit 5280682

11 files changed

Lines changed: 182 additions & 119 deletions

File tree

Lines changed: 54 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
# Rspack Compatibility with React Server Components
22

3-
> **Status**: Experimental — generator support is complete; runtime verification is in progress.
3+
> **Status**: Experimental — the generator scaffolds the native Rspack plugin and the proven runtime path, but the end-to-end gate is not yet wired into this repo's CI. See [issue #3488](https://github.com/shakacode/react_on_rails/issues/3488).
44
55
This page documents the compatibility status of [Rspack](https://rspack.dev/) with React on Rails Pro's React Server Components (RSC) implementation.
66

77
## Overview
88

99
React on Rails Pro's RSC implementation uses a three-bundle architecture (client, server, RSC).
10-
The generator already supports Rspack — when `assets_bundler: rspack` is detected in `shakapacker.yml`, all RSC config files are created in `config/rspack/` instead of `config/webpack/`.
10+
The generator supports Rspack — when `assets_bundler: rspack` is detected in `shakapacker.yml`, all RSC config files are created in `config/rspack/` instead of `config/webpack/`, and the server/client configs are scaffolded with the **native `RSCRspackPlugin`** instead of `RSCWebpackPlugin`.
1111

12-
The RSC implementation depends on the `react-on-rails-rsc` npm package, which provides:
12+
The RSC implementation depends on the `react-on-rails-rsc` npm package, which provides bundler-specific manifest plugins plus a shared loader:
1313

14-
- **WebpackPlugin** — generates client/server component manifest files
15-
- **WebpackLoader** — transforms `'use client'` files into client reference proxies in the RSC bundle
14+
- **WebpackPlugin** (`react-on-rails-rsc/WebpackPlugin`) — generates client/server component manifest files under webpack.
15+
- **RspackPlugin** (`react-on-rails-rsc/RspackPlugin`) — the rspack-native equivalent (`RSCRspackPlugin`). It emits the **same manifest JSON schema** using only standard rspack public APIs, so the RSC runtime resolves client references identically. Exported from `react-on-rails-rsc` 19.0.5 and later.
16+
- **WebpackLoader** (`react-on-rails-rsc/WebpackLoader`) — transforms `'use client'` files into client reference proxies in the RSC bundle. Works under both webpack and rspack.
1617

1718
## Compatibility Matrix
1819

@@ -24,54 +25,53 @@ The RSC implementation depends on the `react-on-rails-rsc` npm package, which pr
2425
| **`resolve.alias`** (`react-dom/server: false`) | Yes | Rspack supports alias to `false` |
2526
| **`LimitChunkCountPlugin`** | Yes | Generated configs use bundler-agnostic `bundler.optimize.LimitChunkCountPlugin` |
2627
| **Loader chain (SWC + Babel)** | Yes | Generated config handles both `function` and `Array` `rule.use` styles |
27-
| **WebpackPlugin** (`react-on-rails-rsc/WebpackPlugin`) | Needs verification | Uses webpack internal APIs (see below) |
28+
| **Manifest plugin** (`RSCRspackPlugin`) | Yes | Native rspack plugin; emits the same manifest schema (see below) |
29+
| **WebpackPlugin** (`react-on-rails-rsc/WebpackPlugin`) | Not used on rspack | Replaced by `RSCRspackPlugin` under rspack; remains the webpack-only plugin |
2830
| **Three-bundle build** (`RSC_BUNDLE_ONLY`, `SERVER_BUNDLE_ONLY`) | Yes | Environment variable routing is bundler-agnostic |
2931

30-
## WebpackPlugin Compatibility Details
32+
## Manifest Plugin: Native `RSCRspackPlugin`
3133

32-
The `RSCWebpackPlugin` is the critical compatibility question. It wraps React's
33-
`react-server-dom-webpack/plugin`, which uses these webpack internal APIs:
34+
The manifest plugin is the critical compatibility question. It generates
35+
`react-client-manifest.json` and `react-server-client-manifest.json`, which map client
36+
component file paths to their chunk IDs and bundle filenames. Without these manifests, the
37+
RSC runtime cannot resolve `'use client'` component references during streaming.
3438

35-
- `webpack/lib/dependencies/ModuleDependency` — base class for dependency tracking
36-
- `webpack/lib/dependencies/NullDependency` — null dependency template
37-
- `webpack/lib/Template` — template utilities
38-
- `webpack.AsyncDependenciesBlock` — async dependency blocks
39-
- `webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT` — asset processing hooks
40-
- `webpack.sources.RawSource` — source output
41-
- Compiler hooks: `beforeCompile`, `thisCompilation`, `make`
42-
- Compilation hooks: `processAssets`
43-
- Parser hooks: `program`
44-
- `compilation.chunkGraph` — chunk/module graph traversal
39+
Under **webpack**, this is `RSCWebpackPlugin`, which wraps React's
40+
`react-server-dom-webpack/plugin` and depends on webpack-internal APIs
41+
(`webpack/lib/dependencies/*`, `webpack.AsyncDependenciesBlock`,
42+
`compilation.chunkGraph`, the `processAssets`/`thisCompilation`/`make` hooks, etc.).
4543

46-
**Why this matters**: The plugin generates `react-client-manifest.json` and
47-
`react-server-client-manifest.json`, which map client component file paths to their chunk
48-
IDs and bundle filenames. Without these manifests, the RSC runtime cannot resolve
49-
`'use client'` component references during streaming.
50-
51-
**Rspack v2 compatibility**: Rspack v2 has significantly improved webpack plugin
52-
compatibility. The [Rspack team has confirmed](https://github.com/shakacode/react_on_rails/issues/1828#issuecomment-3350629010)
53-
that Rspack supports RSC with the JavaScript API. However, runtime verification
54-
with the specific `react-on-rails-rsc` plugin is still needed.
44+
Under **rspack**, the generator instead scaffolds the native **`RSCRspackPlugin`**
45+
(`react-on-rails-rsc/RspackPlugin`). Rather than rely on Rspack's webpack-compatibility
46+
layer for those internal APIs, the native plugin emits the **same manifest JSON schema**
47+
using only standard rspack public APIs. It discovers `'use client'` modules with a tagging
48+
loader, injects them as named async chunks, and walks `compilation.chunkGroups` at
49+
`processAssets`. Because the output schema is identical, the RSC runtime
50+
(`buildServerRenderer` / `buildClientRenderer`) works unchanged regardless of bundler. The
51+
two plugins share the same `{ isServer, clientReferences }` options.
5552

5653
> [!NOTE]
57-
> **Current status (open):** Building a real app under Rspack confirms the RSC bundle
58-
> compiles and server-renders, but the RSC manifest files (`react-client-manifest.json`
59-
> and `react-server-client-manifest.json`) are not yet emitted under Rspack. An
60-
> in-progress fix generates these manifests without the webpack plugin — see
61-
> [PR #3385](https://github.com/shakacode/react_on_rails/pull/3385) and
62-
> [issue #1828](https://github.com/shakacode/react_on_rails/issues/1828).
54+
> **Why native instead of the webpack plugin under Rspack's compat layer?** A controlled
55+
> A/B on a real app showed the webpack-plugin path producing valid-looking manifests that
56+
> still failed ~7/11 RSC routes at runtime under Rspack, while the native `RSCRspackPlugin`
57+
> rendered and hydrated every route. The native plugin is therefore the supported Rspack
58+
> path. The remaining work to drop the "experimental" label — publishing a stable
59+
> `react-on-rails-rsc` ≥ 19.0.5 and wiring the demo route-hydration gate into this repo's
60+
> CI — is tracked in [issue #3488](https://github.com/shakacode/react_on_rails/issues/3488)
61+
> (superseding the abandoned manifest-helper approach in
62+
> [PR #3385](https://github.com/shakacode/react_on_rails/pull/3385)).
6363
6464
## How the RSC Bundle Avoids the Plugin
6565

6666
The RSC bundle config (`rscWebpackConfig.js`) calls `serverWebpackConfig(true)`,
67-
which skips adding `RSCWebpackPlugin`. The RSC bundle only uses:
67+
which skips adding the manifest plugin (`RSCRspackPlugin` under rspack,
68+
`RSCWebpackPlugin` under webpack). The RSC bundle only uses:
6869

6970
1. The **WebpackLoader** to transform `'use client'` files into client reference proxies
7071
2. **`conditionNames: ['react-server', '...']`** to resolve React's server entry points
7172
3. **Aliases** to exclude `react-dom/server` from the RSC bundle
7273

73-
This means the RSC bundle itself should work with Rspack today. The plugin is only
74-
added to the **server** and **client** bundles for manifest generation.
74+
The manifest plugin is only added to the **server** and **client** bundles.
7575

7676
## Testing with Rspack
7777

@@ -82,34 +82,34 @@ To test RSC with Rspack in your project:
8282
3. Verify configs are in `config/rspack/`
8383
4. Build all three bundles and check for:
8484
- `rsc-bundle.js` in the output
85-
- `react-client-manifest.json` and `react-server-client-manifest.json` (from the plugin)
86-
- No webpack/Rspack compilation errors
85+
- `react-client-manifest.json` and `react-server-client-manifest.json` (from `RSCRspackPlugin`)
86+
- No Rspack compilation errors
8787

8888
## Known Limitations
8989

90-
1. **React on Rails uses the `react-server-dom-webpack` path, not Rspack's native RSC**:
91-
As of Rspack v2, Rspack ships its own [`react-server-dom-rspack` package with built-in
92-
RSC support](https://v2.rspack.rs/guide/tech/rsc) (driven by `builtin:swc-loader` and
90+
1. **React on Rails does not use Rspack's experimental native RSC system**:
91+
As of Rspack v2, Rspack ships its own [built-in RSC
92+
support](https://v2.rspack.rs/guide/tech/rsc) (driven by `builtin:swc-loader` and
9393
`rspackExperiments.reactServerComponents`). React on Rails Pro does **not** use that
94-
native path — its RSC integration is built on the `react-on-rails-rsc` package, which
95-
wraps React's `react-server-dom-webpack` loader and plugin. Running it under Rspack
96-
therefore relies on Rspack's webpack compatibility layer rather than Rspack's native
97-
RSC system.
94+
experimental path. Its RSC integration is built on the `react-on-rails-rsc` package:
95+
manifest generation uses the native **`RSCRspackPlugin`** (standard rspack public APIs,
96+
not Rspack's webpack-compatibility layer), while the RSC bundle still uses the
97+
`react-server-dom-webpack` loader and runtime.
9898

99-
2. **Plugin `require('webpack')` call**: The `react-server-dom-webpack/plugin`
100-
internally calls `require('webpack')`, which loads webpack even in Rspack projects.
101-
Rspack's compatibility layer must intercept the compiler/compilation interactions
102-
for the plugin to function correctly.
99+
2. **The RSC bundle still uses the `react-server-dom-webpack` loader**: Only the
100+
manifest plugin is bundler-native. The RSC bundle transforms `'use client'` files with
101+
`react-on-rails-rsc/WebpackLoader` (which wraps React's `react-server-dom-webpack`
102+
node loader) under both bundlers. This loader is compatible with rspack.
103103

104104
3. **No official React Rspack support**: The React team has not officially tested or
105-
endorsed `react-server-dom-webpack` with Rspack. Compatibility is provided by
106-
Rspack's webpack API compatibility layer.
105+
endorsed the `react-server-dom-webpack` runtime with Rspack. The native
106+
`RSCRspackPlugin` is maintained by ShakaCode in `react-on-rails-rsc`.
107107

108108
## Related Resources
109109

110+
- [Issue #3488: Rspack RSC path to production-ready (native RSCRspackPlugin)](https://github.com/shakacode/react_on_rails/issues/3488)
110111
- [Issue #1828: Rspack support for RSC](https://github.com/shakacode/react_on_rails/issues/1828)
111-
- [PR #3385: Fix Pro RSC manifests for Rspack builds](https://github.com/shakacode/react_on_rails/pull/3385)
112-
- [Rspack RSC support PR](https://github.com/web-infra-dev/rspack/pull/5824)
112+
- [PR #3385: Manifest-helper approach for Rspack builds (superseded by the native plugin)](https://github.com/shakacode/react_on_rails/pull/3385)
113113
- [Rspack v2 React Server Components guide](https://v2.rspack.rs/guide/tech/rsc)
114114
- [Three-bundle architecture](./how-react-server-components-work.md)
115115
- [Upgrading an existing Pro app to RSC](./upgrading-existing-pro-app.md)

react_on_rails/lib/generators/react_on_rails/generator_helper.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,26 @@ def destination_config_path(path)
197197
path.sub(%r{\Aconfig/webpack/}, "config/rspack/")
198198
end
199199

200+
# RSC client-manifest plugin class name for the active bundler.
201+
# Rspack uses the native `RSCRspackPlugin`; webpack uses `RSCWebpackPlugin`.
202+
# Both expose the same `{ isServer, clientReferences }` API and emit the same
203+
# manifest schema, so only the import path and class name differ.
204+
# Shared by the base webpack-config templates and the standalone RSC migration
205+
# so both paths scaffold the bundler-correct plugin from one source of truth.
206+
#
207+
# @return [String] "RSCRspackPlugin" when rspack, "RSCWebpackPlugin" otherwise
208+
def rsc_plugin_class_name
209+
using_rspack? ? "RSCRspackPlugin" : "RSCWebpackPlugin"
210+
end
211+
212+
# `react-on-rails-rsc` subpath that exports {#rsc_plugin_class_name}.
213+
#
214+
# @return [String] "react-on-rails-rsc/RspackPlugin" when rspack,
215+
# "react-on-rails-rsc/WebpackPlugin" otherwise
216+
def rsc_plugin_import_path
217+
using_rspack? ? "react-on-rails-rsc/RspackPlugin" : "react-on-rails-rsc/WebpackPlugin"
218+
end
219+
200220
# Detect the installed React version from package.json
201221
# Uses VERSION_PARTS_REGEX pattern from VersionChecker for consistency
202222
#

react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,12 @@ module JsDependencyManager
142142

143143
# RSC package releases follow the React 19.0.x line (independent from gem versioning).
144144
RSC_REACT_VERSION_RANGE = "~19.0.4"
145-
RSC_PACKAGE_VERSION_PIN = "19.0.4"
145+
# Pinned to 19.0.5-rc.x because the native rspack manifest plugin
146+
# (`react-on-rails-rsc/RspackPlugin`) the generator scaffolds for rspack projects is only
147+
# exported from 19.0.5 onward. This RC is backward-compatible for webpack projects (it still
148+
# exports `react-on-rails-rsc/WebpackPlugin`). Bump to the stable "19.0.5" once it is published
149+
# (tracked in https://github.com/shakacode/react_on_rails/issues/3488).
150+
RSC_PACKAGE_VERSION_PIN = "19.0.5-rc.5"
146151

147152
private
148153

react_on_rails/lib/generators/react_on_rails/rsc_setup.rb

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ def update_server_webpack_config_for_rsc
388388
return
389389
end
390390

391-
# Add RSCWebpackPlugin import after bundler require
391+
# Add the RSC plugin import after bundler require
392392
return unless rsc_client_references_setup_anchor_available?(
393393
config_path,
394394
content,
@@ -404,17 +404,17 @@ def update_server_webpack_config_for_rsc
404404
gsub_file(
405405
config_path,
406406
/^const configureServer = \(\) => \{/,
407-
"// rscBundle parameter: when true, skips RSCWebpackPlugin (RSC bundle doesn't need it)\n" \
407+
"// rscBundle parameter: when true, skips #{rsc_plugin_class_name} (RSC bundle doesn't need it)\n" \
408408
"const configureServer = (rscBundle = false) => {"
409409
)
410410

411-
# Add RSCWebpackPlugin to plugins before LimitChunkCountPlugin (matches template ordering)
411+
# Add the RSC plugin to plugins before LimitChunkCountPlugin (matches template ordering)
412412
client_references_option = setup_status == :scoped ? ", clientReferences: rscClientReferences" : ""
413413
rsc_plugin_code = "// Add RSC plugin for server bundle (handles client component references)\n " \
414-
"// Skip for RSC bundle - it doesn't need RSCWebpackPlugin\n " \
414+
"// Skip for RSC bundle - it doesn't need #{rsc_plugin_class_name}\n " \
415415
"if (!rscBundle) {\n " \
416416
"serverWebpackConfig.plugins.push(\n " \
417-
"new RSCWebpackPlugin({ isServer: true#{client_references_option} }),\n " \
417+
"new #{rsc_plugin_class_name}({ isServer: true#{client_references_option} }),\n " \
418418
");\n " \
419419
"}"
420420
gsub_file(
@@ -438,7 +438,7 @@ def update_client_webpack_config_for_rsc
438438
return
439439
end
440440

441-
# Add RSCWebpackPlugin import after commonWebpackConfig import
441+
# Add the RSC plugin import after commonWebpackConfig import
442442
return unless rsc_client_references_setup_anchor_available?(
443443
config_path,
444444
content,
@@ -450,11 +450,11 @@ def update_client_webpack_config_for_rsc
450450
setup_status = prepare_rsc_plugin_imports(config_path, content, existing_imports_content, is_server: false)
451451
return if setup_status == :failed
452452

453-
# Add RSCWebpackPlugin to client config before return statement
453+
# Add the RSC plugin to client config before return statement
454454
client_references_option = setup_status == :scoped ? ", clientReferences: rscClientReferences" : ""
455455
rsc_plugin_code = " // Add React Server Components plugin for client bundle\n " \
456456
"clientConfig.plugins.push(\n " \
457-
"new RSCWebpackPlugin({ isServer: false#{client_references_option} }),\n " \
457+
"new #{rsc_plugin_class_name}({ isServer: false#{client_references_option} }),\n " \
458458
");"
459459
gsub_file(
460460
config_path,
@@ -508,8 +508,9 @@ def rsc_server_signature_in_js_code?(content)
508508
def warn_incomplete_new_rsc_plugin_setup(config_path, is_server:)
509509
insertion_point = is_server ? "server webpack insertion points" : "client webpack return statement"
510510
GeneratorMessages.add_warning(
511-
"Could not finish adding RSCWebpackPlugin to #{config_path}: expected #{insertion_point} was not found. " \
512-
"Reverted partial RSC setup; please add RSCWebpackPlugin and clientReferences manually."
511+
"Could not finish adding #{rsc_plugin_class_name} to #{config_path}: expected #{insertion_point} " \
512+
"was not found. Reverted partial RSC setup; please add #{rsc_plugin_class_name} and " \
513+
"clientReferences manually."
513514
)
514515
end
515516

@@ -537,13 +538,13 @@ def check_rsc_server_config
537538

538539
content = File.read(path)
539540
missing = []
540-
if content.include?("RSCWebpackPlugin")
541+
if content.include?(rsc_plugin_class_name)
541542
warn_non_object_literal_rsc_plugin_options_for_config(content)
542543
unless rsc_plugin_client_references_configured?(content, is_server: true)
543544
missing << "generated scoped clientReferences in serverWebpackConfig.js"
544545
end
545546
else
546-
missing << "RSCWebpackPlugin in serverWebpackConfig.js"
547+
missing << "#{rsc_plugin_class_name} in serverWebpackConfig.js"
547548
end
548549
missing << "rscBundle parameter in serverWebpackConfig.js" unless content.include?("rscBundle")
549550
missing
@@ -555,13 +556,13 @@ def check_rsc_client_config
555556

556557
content = File.read(path)
557558
missing = []
558-
if content.include?("RSCWebpackPlugin")
559+
if content.include?(rsc_plugin_class_name)
559560
warn_non_object_literal_rsc_plugin_options_for_config(content)
560561
unless rsc_plugin_client_references_configured?(content, is_server: false)
561562
missing << "generated scoped clientReferences in clientWebpackConfig.js"
562563
end
563564
else
564-
missing << "RSCWebpackPlugin in clientWebpackConfig.js"
565+
missing << "#{rsc_plugin_class_name} in clientWebpackConfig.js"
565566
end
566567
missing
567568
end
@@ -585,9 +586,9 @@ def warn_non_object_literal_rsc_plugin_options_once
585586

586587
@non_object_literal_rsc_plugin_options_warned = true
587588
GeneratorMessages.add_warning(
588-
"RSCWebpackPlugin calls use non-object-literal options in one or more webpack configs, " \
589+
"#{rsc_plugin_class_name} calls use non-object-literal options in one or more bundler configs, " \
589590
"so the generator cannot verify whether scoped clientReferences are configured. " \
590-
"Please verify your webpack configs manually."
591+
"Please verify your bundler configs manually."
591592
)
592593
end
593594
end

0 commit comments

Comments
 (0)