Skip to content

Commit 2e26063

Browse files
justin808claude
andcommitted
fix(generator): delegate RSC plugin helpers for cleanup re-render + harden specs
Address review findings on PR #3590 (native RSCRspackPlugin scaffolding). The PR added `<%= rsc_plugin_class_name %>` / `<%= rsc_plugin_import_path %>` to the managed serverWebpackConfig.js.tt and clientWebpackConfig.js.tt templates. Those managed templates are re-rendered during stale-config cleanup (`rendered_template_for_cleanup`) against the restricted `TemplateRenderContext` binding, which did not expose those helpers. For any RSC project the cleanup render raised NameError, printed a "could not render template ... treating as non-removable" warning, and wrongly preserved the managed config files. Delegate both helpers in `TemplateRenderContext` (mirroring use_pro?/use_rsc?) to fix this, and add a regression spec asserting the RSC managed templates render (not the TEMPLATE_RENDER_FAILED sentinel) for both webpack and rspack projects. Also: - Make the rscWebpackConfig.js.tt skip comment bundler-neutral so rspack-generated configs no longer reference RSCWebpackPlugin. - Update two now-stale rsc_setup.rb docstrings that still described the RSC plugin detection/injection as webpack-only after the regex was broadened to match both plugin names. - Add an rspack dedup/idempotency spec mirroring the existing webpack contexts, exercising the rspack branch of RSC_PLUGIN_INVOCATION_REGEX (verified to fail if the regex regresses to webpack-only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5280682 commit 2e26063

5 files changed

Lines changed: 116 additions & 9 deletions

File tree

react_on_rails/lib/generators/react_on_rails/base_generator.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,14 @@ def use_rsc?
109109
def shakapacker_version_9_or_higher?
110110
generator.__send__(:shakapacker_version_9_or_higher?)
111111
end
112+
113+
def rsc_plugin_class_name
114+
generator.__send__(:rsc_plugin_class_name)
115+
end
116+
117+
def rsc_plugin_import_path
118+
generator.__send__(:rsc_plugin_import_path)
119+
end
112120
end
113121

114122
REMOVABLE_WEBPACK_FILES = (MANAGED_WEBPACK_FILE_TEMPLATES.keys +
@@ -963,8 +971,9 @@ def rendered_template_for_cleanup(template_path)
963971
# current run omits those options; in that case, we preserve the directory.
964972
# Templates rely on config[:message] plus a small helper subset exposed by
965973
# TemplateRenderContext (add_documentation_reference, use_pro?, use_rsc?,
966-
# shakapacker_version_9_or_higher?). Missing method delegates raise
967-
# NoMethodError and are caught below, treating the file as non-removable.
974+
# shakapacker_version_9_or_higher?, rsc_plugin_class_name, rsc_plugin_import_path).
975+
# Missing method delegates raise NoMethodError and are caught below, treating the
976+
# file as non-removable.
968977
# Missing config hash keys return nil silently, so any new config key
969978
# required by templates must be added to template_doc_config above.
970979
# Use TemplateRenderContext#erb_binding to avoid leaking method-local

react_on_rails/lib/generators/react_on_rails/rsc_setup.rb

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,8 @@ def add_rsc_routes
306306
#
307307
# Updates:
308308
# - ServerClientOrBoth.js: RSC imports, rscConfig, RSC_BUNDLE_ONLY handling
309-
# - serverWebpackConfig.js: RSCWebpackPlugin import, rscBundle param, plugin
310-
# - clientWebpackConfig.js: RSCWebpackPlugin import, plugin
309+
# - serverWebpackConfig.js: RSC plugin import (RSCRspackPlugin/RSCWebpackPlugin), rscBundle param, plugin
310+
# - clientWebpackConfig.js: RSC plugin import (RSCRspackPlugin/RSCWebpackPlugin), plugin
311311
def update_webpack_configs_for_rsc
312312
say "📝 Updating webpack configs for RSC...", :yellow
313313

@@ -489,10 +489,11 @@ def new_rsc_plugin_setup_complete?(content, is_server:)
489489
rsc_server_signature_in_js_code?(content)
490490
end
491491

492-
# Returns true when the file contains a real `new RSCWebpackPlugin(` invocation in actual JS
493-
# code — not inside a comment or string literal. Reuses `RSC_PLUGIN_INVOCATION_REGEX` from
494-
# the ClientReferences module so the routing check and the option-section partition match
495-
# the same set of invocations (including whitespace/newline variants).
492+
# Returns true when the file contains a real `new RSCWebpackPlugin(` / `new RSCRspackPlugin(`
493+
# invocation in actual JS code — not inside a comment or string literal. Reuses
494+
# `RSC_PLUGIN_INVOCATION_REGEX` from the ClientReferences module (which matches both bundler
495+
# plugin names) so the routing check and the option-section partition match the same set of
496+
# invocations (including whitespace/newline variants).
496497
def rsc_plugin_invocation_in_js_code?(content)
497498
content
498499
.to_enum(:scan, RSC_PLUGIN_INVOCATION_REGEX)

react_on_rails/lib/generators/react_on_rails/templates/rsc/base/config/webpack/rscWebpackConfig.js.tt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const extractLoader =
1919
});
2020

2121
const configureRsc = () => {
22-
// Pass true to skip RSCWebpackPlugin - RSC bundle doesn't need it
22+
// Pass true to skip the RSC manifest plugin - RSC bundle doesn't need it
2323
const rscConfig = serverWebpackConfig(true);
2424

2525
// Update the entry name to be `rsc-bundle` instead of `server-bundle`

react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,39 @@ def repo_pinned_pnpm_version
4444
match[:version]
4545
end
4646

47+
describe "RSC managed-template cleanup rendering" do
48+
# Regression: serverWebpackConfig.js.tt / clientWebpackConfig.js.tt interpolate
49+
# `<%= rsc_plugin_class_name %>` / `<%= rsc_plugin_import_path %>` inside their `use_rsc?`
50+
# blocks. The cleanup re-render path (rendered_template_for_cleanup) evaluates templates
51+
# against the restricted TemplateRenderContext binding, so those helpers must be delegated
52+
# there. If they are not, RSC managed configs render as TEMPLATE_RENDER_FAILED and are
53+
# wrongly treated as non-removable (with a NameError warning printed during install).
54+
render_failed_sentinel = ReactOnRails::Generators::BaseGenerator.const_get(:TEMPLATE_RENDER_FAILED)
55+
56+
%w[
57+
base/base/config/webpack/serverWebpackConfig.js.tt
58+
base/base/config/webpack/clientWebpackConfig.js.tt
59+
].each do |template_path|
60+
basename = File.basename(template_path, ".tt")
61+
62+
it "renders #{basename} for an RSC webpack project during cleanup" do
63+
rendered = render_stock_webpack_template(template_path, rsc: true, rspack: false)
64+
65+
expect(rendered).not_to equal(render_failed_sentinel)
66+
expect(rendered).to include("RSCWebpackPlugin")
67+
expect(rendered).to include("react-on-rails-rsc/WebpackPlugin")
68+
end
69+
70+
it "renders #{basename} for an RSC rspack project during cleanup" do
71+
rendered = render_stock_webpack_template(template_path, rsc: true, rspack: true)
72+
73+
expect(rendered).not_to equal(render_failed_sentinel)
74+
expect(rendered).to include("RSCRspackPlugin")
75+
expect(rendered).to include("react-on-rails-rsc/RspackPlugin")
76+
end
77+
end
78+
end
79+
4780
context "without args" do
4881
before(:all) { run_generator_test_with_args(%w[], package_json: true) }
4982

react_on_rails/spec/react_on_rails/generators/rsc_generator_spec.rb

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3657,6 +3657,70 @@ def index
36573657
end
36583658
end
36593659

3660+
# Rspack analogue of the webpack "already imports/invokes RSCWebpackPlugin" dedup contexts above.
3661+
# Exercises the Rspack branch of RSC_PLUGIN_INVOCATION_REGEX (/new\s+RSC(?:Webpack|Rspack)Plugin\s*\(/):
3662+
# if that regex regressed to webpack-only, the generator would not detect the existing
3663+
# `new RSCRspackPlugin(` call, would route to the add-plugin path, and inject a duplicate import —
3664+
# producing `Identifier 'RSCRspackPlugin' has already been declared` at build time.
3665+
context "when an existing rspack client config already imports and invokes RSCRspackPlugin" do
3666+
before(:all) do
3667+
prepare_destination
3668+
simulate_existing_rails_files(package_json: true)
3669+
simulate_npm_files(package_json: true)
3670+
simulate_existing_file("config/initializers/react_on_rails_pro.rb", <<~RUBY)
3671+
ReactOnRailsPro.configure do |config|
3672+
config.server_renderer = "NodeRenderer"
3673+
end
3674+
RUBY
3675+
simulate_existing_file("Procfile.dev", "rails: bin/rails s\n")
3676+
# Sets up the rspack shakapacker.yml so rspack_configured_in_project? is true,
3677+
# then overrides the client config with one that already has the native plugin.
3678+
simulate_rspack_pro_webpack_files
3679+
simulate_existing_file(
3680+
"config/rspack/clientWebpackConfig.js",
3681+
<<~JS
3682+
const commonWebpackConfig = require('./commonWebpackConfig');
3683+
const { RSCRspackPlugin } = require('react-on-rails-rsc/RspackPlugin');
3684+
3685+
const configureClient = () => {
3686+
const clientConfig = commonWebpackConfig();
3687+
delete clientConfig.entry['server-bundle'];
3688+
3689+
clientConfig.plugins.push(
3690+
new RSCRspackPlugin ({ isServer: false })
3691+
);
3692+
3693+
return clientConfig;
3694+
};
3695+
3696+
module.exports = configureClient;
3697+
JS
3698+
)
3699+
3700+
Dir.chdir(destination_root) do
3701+
run_generator(["--force"])
3702+
end
3703+
end
3704+
3705+
it "detects the existing native plugin and routes to the update path rather than duplicating the import" do
3706+
assert_file "config/rspack/clientWebpackConfig.js" do |content|
3707+
expect(content.scan(%r{require\(['"]react-on-rails-rsc/RspackPlugin['"]\)}).length).to eq(1)
3708+
expect(content.scan(/new\s+RSCRspackPlugin\s*\(/).length).to eq(1)
3709+
# The webpack plugin must never leak into an rspack config.
3710+
expect(content).not_to include("RSCWebpackPlugin")
3711+
expect(content).not_to include("react-on-rails-rsc/WebpackPlugin")
3712+
end
3713+
end
3714+
3715+
it "injects scoped clientReferences into the existing native plugin call" do
3716+
assert_file "config/rspack/clientWebpackConfig.js" do |content|
3717+
expect(content).to include("clientReferences: rscClientReferences")
3718+
expect(content).to include("const rscClientReferences")
3719+
expect(content).to include("directory: resolve(config.source_path)")
3720+
end
3721+
end
3722+
end
3723+
36603724
# Rspack + legacy Pro variant — same as the legacy webpack exports context below,
36613725
# but with Pro configs in config/rspack/ and rspack shakapacker.yml.
36623726
# Verifies that the backward-compatible rscWebpackConfig.js is created in the

0 commit comments

Comments
 (0)