From 14f1b0c2a08143669a3b103da627610ee334e9d2 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Thu, 4 Jun 2026 23:23:14 -1000 Subject: [PATCH 1/6] Polish RSC generator follow-ups --- .../react_on_rails/generator_helper.rb | 2 +- .../react_on_rails/js_dependency_manager.rb | 107 ++++++++++-------- .../generators/react_on_rails/rsc_setup.rb | 19 +++- .../rsc_setup/client_references.rb | 35 +++--- .../generators/generator_helper_spec.rb | 3 +- .../generators/install_generator_spec.rb | 48 ++++++++ .../generators/js_dependency_manager_spec.rb | 15 +-- .../generators/rsc_generator_spec.rb | 65 +++++++++++ 8 files changed, 219 insertions(+), 75 deletions(-) diff --git a/react_on_rails/lib/generators/react_on_rails/generator_helper.rb b/react_on_rails/lib/generators/react_on_rails/generator_helper.rb index 2004d7859d..6f0fd8eb96 100644 --- a/react_on_rails/lib/generators/react_on_rails/generator_helper.rb +++ b/react_on_rails/lib/generators/react_on_rails/generator_helper.rb @@ -41,7 +41,7 @@ def add_npm_dependencies(packages, dev: false) result != false rescue StandardError => e say_status :warning, "Could not add packages via package_json gem: #{e.message}", :yellow - say_status :warning, "Will fall back to direct npm commands.", :yellow + say_status :warning, "Will fall back to direct package manager commands.", :yellow false end end diff --git a/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb b/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb index 265b36e25a..a9722eb812 100644 --- a/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb +++ b/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb @@ -226,14 +226,14 @@ def add_react_on_rails_package ⚠️ Failed to add react-on-rails package. You can install it manually by running: - npm install #{react_on_rails_pkg} + #{manual_add_packages_command([react_on_rails_pkg])} MSG rescue StandardError => e GeneratorMessages.add_warning(<<~MSG.strip) ⚠️ Error adding react-on-rails package: #{e.message} You can install it manually by running: - npm install #{react_on_rails_pkg} + #{manual_add_packages_command([react_on_rails_pkg])} MSG end @@ -255,14 +255,14 @@ def add_react_dependencies ⚠️ Failed to add React dependencies. You can install them manually by running: - npm install #{react_deps.join(' ')} + #{manual_add_packages_command(react_deps)} MSG rescue StandardError => e GeneratorMessages.add_warning(<<~MSG.strip) ⚠️ Error adding React dependencies: #{e.message} You can install them manually by running: - npm install #{react_deps.join(' ')} + #{manual_add_packages_command(react_deps)} MSG end @@ -274,14 +274,14 @@ def add_css_dependencies ⚠️ Failed to add CSS dependencies. You can install them manually by running: - npm install #{CSS_DEPENDENCIES.join(' ')} + #{manual_add_packages_command(CSS_DEPENDENCIES)} MSG rescue StandardError => e GeneratorMessages.add_warning(<<~MSG.strip) ⚠️ Error adding CSS dependencies: #{e.message} You can install them manually by running: - npm install #{CSS_DEPENDENCIES.join(' ')} + #{manual_add_packages_command(CSS_DEPENDENCIES)} MSG end @@ -293,14 +293,14 @@ def add_rspack_dependencies ⚠️ Failed to add Rspack dependencies. You can install them manually by running: - npm install #{RSPACK_DEPENDENCIES.join(' ')} + #{manual_add_packages_command(RSPACK_DEPENDENCIES)} MSG rescue StandardError => e GeneratorMessages.add_warning(<<~MSG.strip) ⚠️ Error adding Rspack dependencies: #{e.message} You can install them manually by running: - npm install #{RSPACK_DEPENDENCIES.join(' ')} + #{manual_add_packages_command(RSPACK_DEPENDENCIES)} MSG end @@ -313,14 +313,14 @@ def add_swc_dependencies SWC is the default JavaScript transpiler for Shakapacker 9.3.0+. You can install them manually by running: - npm install --save-dev #{SWC_DEPENDENCIES.join(' ')} + #{manual_add_packages_command(SWC_DEPENDENCIES, dev: true)} MSG rescue StandardError => e GeneratorMessages.add_warning(<<~MSG.strip) ⚠️ Error adding SWC dependencies: #{e.message} You can install them manually by running: - npm install --save-dev #{SWC_DEPENDENCIES.join(' ')} + #{manual_add_packages_command(SWC_DEPENDENCIES, dev: true)} MSG end @@ -332,7 +332,7 @@ def add_babel_react_dependencies ⚠️ Failed to add Babel React preset dependency. You can install it manually by running: - npm install --save-dev #{BABEL_REACT_DEPENDENCIES.join(' ')} + #{manual_add_packages_command(BABEL_REACT_DEPENDENCIES, dev: true)} MSG false rescue StandardError => e @@ -340,7 +340,7 @@ def add_babel_react_dependencies ⚠️ Error adding Babel React preset dependency: #{e.message} You can install it manually by running: - npm install --save-dev #{BABEL_REACT_DEPENDENCIES.join(' ')} + #{manual_add_packages_command(BABEL_REACT_DEPENDENCIES, dev: true)} MSG false end @@ -353,14 +353,14 @@ def add_typescript_dependencies ⚠️ Failed to add TypeScript dependencies. You can install them manually by running: - npm install --save-dev #{TYPESCRIPT_DEPENDENCIES.join(' ')} + #{manual_add_packages_command(TYPESCRIPT_DEPENDENCIES, dev: true)} MSG rescue StandardError => e GeneratorMessages.add_warning(<<~MSG.strip) ⚠️ Error adding TypeScript dependencies: #{e.message} You can install them manually by running: - npm install --save-dev #{TYPESCRIPT_DEPENDENCIES.join(' ')} + #{manual_add_packages_command(TYPESCRIPT_DEPENDENCIES, dev: true)} MSG end @@ -381,14 +381,14 @@ def add_pro_dependencies ⚠️ Failed to add React on Rails Pro dependencies. You can install them manually by running: - npm install #{pro_packages.join(' ')} + #{manual_add_packages_command(pro_packages)} MSG rescue StandardError => e GeneratorMessages.add_warning(<<~MSG.strip) ⚠️ Error adding React on Rails Pro dependencies: #{e.message} You can install them manually by running: - npm install #{PRO_DEPENDENCIES.join(' ')} + #{manual_add_packages_command(PRO_DEPENDENCIES)} MSG end @@ -410,42 +410,25 @@ def pro_packages_with_version def add_rsc_dependencies say "Installing React Server Components dependencies..." rsc_packages, used_version_pins = rsc_packages_with_version + GeneratorMessages.add_info(rsc_dependency_pin_info) if used_version_pins return if add_packages(rsc_packages) manual_install_packages = rsc_packages - if used_version_pins && using_rspack? - # Do NOT retry unversioned for rspack: the `latest` tag (currently 19.0.4) does not export - # react-on-rails-rsc/RspackPlugin, so an unversioned install would replace the pin already - # written to package.json with a known-incompatible version and silently break the build. - # Keep the pin and tell the user to finish the install manually. - GeneratorMessages.add_warning(rspack_rsc_dependency_pin_failed_warning) - elsif used_version_pins - warning_msg = "Could not install version-pinned RSC dependency. Retrying latest available package." - say_status :warning, - warning_msg, - :yellow - GeneratorMessages.add_warning( - "Warning: #{warning_msg} " \ - "The installed react-on-rails-rsc version may not match the expected compatibility pin." - ) - return if add_packages(RSC_DEPENDENCIES) - - manual_install_packages = RSC_DEPENDENCIES - end + GeneratorMessages.add_warning(rsc_dependency_pin_failed_warning) if used_version_pins GeneratorMessages.add_warning(<<~MSG.strip) ⚠️ Failed to add React Server Components dependencies. You can install them manually by running: - npm install #{manual_install_packages.join(' ')} + #{manual_add_packages_command(manual_install_packages)} MSG rescue StandardError => e - manual_install_packages = using_rspack? ? rsc_packages_with_pin : RSC_DEPENDENCIES + manual_install_packages = rsc_packages_with_pin GeneratorMessages.add_warning(<<~MSG.strip) ⚠️ Error adding React Server Components dependencies: #{e.message} You can install them manually by running: - npm install #{manual_install_packages.join(' ')} + #{manual_add_packages_command(manual_install_packages)} MSG end @@ -459,12 +442,21 @@ def rsc_packages_with_pin RSC_DEPENDENCIES.map { |pkg| "#{pkg}@#{RSC_PACKAGE_VERSION_PIN}" } end - def rspack_rsc_dependency_pin_failed_warning + def rsc_dependency_pin_info + "React Server Components package pin: all --rsc installs temporarily use " \ + "react-on-rails-rsc@#{RSC_PACKAGE_VERSION_PIN}, including Webpack projects. " \ + "This prerelease keeps react-on-rails-rsc/WebpackPlugin compatible while adding " \ + "react-on-rails-rsc/RspackPlugin. Keep the pin until stable react-on-rails-rsc@19.0.5 " \ + "is published and tagged latest." + end + + def rsc_dependency_pin_failed_warning "Warning: Could not install the pinned react-on-rails-rsc@#{RSC_PACKAGE_VERSION_PIN}. " \ - "Rspack RSC projects require that version (or newer) for react-on-rails-rsc/RspackPlugin, " \ - "and the unversioned `latest` tag does not export it, so the generator left the pin in " \ - "package.json rather than install an incompatible version. " \ - "Run npm install react-on-rails-rsc@#{RSC_PACKAGE_VERSION_PIN} to finish setup." + "All RSC projects are temporarily pinned to that version because the unversioned " \ + "`latest` tag may not export react-on-rails-rsc/RspackPlugin until stable 19.0.5 " \ + "is published, so the generator left the version pin in package.json rather than " \ + "install a potentially incompatible version. " \ + "Run #{manual_add_packages_command(rsc_packages_with_pin)} to finish setup." end def remove_base_package_if_present @@ -482,7 +474,7 @@ def remove_base_package_if_present ⚠️ Could not remove base 'react-on-rails' package: #{e.message} Please remove it manually: - npm uninstall react-on-rails + #{manual_remove_packages_command(['react-on-rails'])} MSG end @@ -498,14 +490,14 @@ def add_dev_dependencies ⚠️ Failed to add development dependencies. You can install them manually by running: - npm install --save-dev #{dev_deps.join(' ')} + #{manual_add_packages_command(dev_deps, dev: true)} MSG rescue StandardError => e GeneratorMessages.add_warning(<<~MSG.strip) ⚠️ Error adding development dependencies: #{e.message} You can install them manually by running: - npm install --save-dev #{dev_deps.join(' ')} + #{manual_add_packages_command(dev_deps, dev: true)} MSG end @@ -566,9 +558,7 @@ def install_js_dependencies This could be due to network issues or package manager problems. You can install dependencies manually later by running: - • npm install (if using npm) - • yarn install (if using yarn) - • pnpm install (if using pnpm) + #{manual_install_dependencies_command} MSG false end @@ -636,6 +626,25 @@ def build_install_args(package_manager, dev, packages) base_args + packages end + def manual_add_packages_command(packages, dev: false) + build_install_args(fallback_package_manager, dev, packages).join(" ") + end + + def manual_install_dependencies_command + "#{fallback_package_manager} install" + end + + def manual_remove_packages_command(packages) + base_commands = { + "npm" => %w[npm uninstall], + "yarn" => %w[yarn remove], + "pnpm" => %w[pnpm remove], + "bun" => %w[bun remove] + } + + (base_commands.fetch(fallback_package_manager).dup + packages).join(" ") + end + def dev_flag_for(package_manager) case package_manager when "npm", "pnpm" then "--save-dev" diff --git a/react_on_rails/lib/generators/react_on_rails/rsc_setup.rb b/react_on_rails/lib/generators/react_on_rails/rsc_setup.rb index 607bdbc557..89df8e6bfc 100644 --- a/react_on_rails/lib/generators/react_on_rails/rsc_setup.rb +++ b/react_on_rails/lib/generators/react_on_rails/rsc_setup.rb @@ -84,14 +84,14 @@ def warn_about_react_version_for_rsc(force: false) React 19.0.x. React 19.1.x and later are not yet supported. To install a compatible React version: - npm install react@~19.0.4 react-dom@~19.0.4 + #{manual_add_packages_command(['react@~19.0.4', 'react-dom@~19.0.4'])} MSG elsif patch < 4 GeneratorMessages.add_warning(<<~MSG.strip) ⚠️ React #{react_version} is below the recommended minimum for RSC. Please upgrade to at least React 19.0.4: - npm install react@19.0.4 react-dom@19.0.4 + #{manual_add_packages_command(['react@19.0.4', 'react-dom@19.0.4'])} react-server-dom-webpack 19.0.0–19.0.3 has known vulnerabilities (CVE-2025-55182, CVE-2025-67779, CVE-2026-23864) fixed in 19.0.4+. @@ -540,11 +540,12 @@ def check_rsc_server_config content = File.read(path) missing = [] if content.include?(rsc_plugin_class_name) + missing.concat(stale_inactive_rsc_plugin_messages(content, "serverWebpackConfig.js")) warn_non_object_literal_rsc_plugin_options_for_config(content) unless rsc_plugin_client_references_configured?(content, is_server: true) missing << "generated scoped clientReferences in serverWebpackConfig.js" end - elsif content.include?(inactive_rsc_plugin_class_name) + elsif inactive_rsc_plugin_symbol_in_js_code?(content) missing << "#{rsc_plugin_class_name} in serverWebpackConfig.js " \ "(found #{inactive_rsc_plugin_class_name} — wrong bundler plugin; replace it manually)" else @@ -561,11 +562,12 @@ def check_rsc_client_config content = File.read(path) missing = [] if content.include?(rsc_plugin_class_name) + missing.concat(stale_inactive_rsc_plugin_messages(content, "clientWebpackConfig.js")) warn_non_object_literal_rsc_plugin_options_for_config(content) unless rsc_plugin_client_references_configured?(content, is_server: false) missing << "generated scoped clientReferences in clientWebpackConfig.js" end - elsif content.include?(inactive_rsc_plugin_class_name) + elsif inactive_rsc_plugin_symbol_in_js_code?(content) missing << "#{rsc_plugin_class_name} in clientWebpackConfig.js " \ "(found #{inactive_rsc_plugin_class_name} — wrong bundler plugin; replace it manually)" else @@ -588,6 +590,15 @@ def warn_non_object_literal_rsc_plugin_options_for_config(content) warn_non_object_literal_rsc_plugin_options_once end + def stale_inactive_rsc_plugin_messages(content, config_filename) + return [] unless inactive_rsc_plugin_symbol_in_js_code?(content) + + [ + "stale #{inactive_rsc_plugin_class_name} in #{config_filename} " \ + "(found alongside #{rsc_plugin_class_name} — remove the inactive bundler plugin manually)" + ] + end + def warn_non_object_literal_rsc_plugin_options_once return if @non_object_literal_rsc_plugin_options_warned diff --git a/react_on_rails/lib/generators/react_on_rails/rsc_setup/client_references.rb b/react_on_rails/lib/generators/react_on_rails/rsc_setup/client_references.rb index 7e66ebb0aa..8d255765b3 100644 --- a/react_on_rails/lib/generators/react_on_rails/rsc_setup/client_references.rb +++ b/react_on_rails/lib/generators/react_on_rails/rsc_setup/client_references.rb @@ -273,11 +273,7 @@ def update_existing_rsc_webpack_config(config_path, content, is_server:) end def normalize_rsc_plugin_for_active_bundler(config_path, content) - normalized_content, active_plugin_binding_available = normalize_rsc_plugin_import_for_active_bundler( - content, - inactive_rsc_plugin_class_name, - inactive_rsc_plugin_import_path - ) + normalized_content, active_plugin_binding_available = normalize_rsc_plugin_import_for_active_bundler(content) if !active_plugin_binding_available && inactive_rsc_plugin_esm_import?(content) warn_unsupported_rsc_plugin_import_syntax(config_path) end @@ -286,27 +282,26 @@ def normalize_rsc_plugin_for_active_bundler(config_path, content) normalize_rsc_plugin_invocations_for_active_bundler(normalized_content, inactive_rsc_plugin_class_name) end - def normalize_rsc_plugin_import_for_active_bundler(content, inactive_plugin_class_name, - inactive_plugin_import_path) + def normalize_rsc_plugin_import_for_active_bundler(content) active_import_position = commonjs_named_import_position( content, rsc_plugin_import_path, rsc_plugin_class_name ) converted_inactive_import_position = nil - import_regex = rsc_plugin_commonjs_import_regex(inactive_plugin_import_path) + import_regex = rsc_plugin_commonjs_import_regex(inactive_rsc_plugin_import_path) normalized_content = content.gsub(import_regex) do |statement| statement_position = Regexp.last_match.begin(0) next statement unless js_top_level_position?(content, statement_position) if active_import_position && active_import_position < statement_position - remove_commonjs_named_import_binding(statement, inactive_plugin_class_name) + remove_commonjs_named_import_binding(statement, inactive_rsc_plugin_class_name) else converted_inactive_import_position ||= statement_position active_import_position = statement_position statement - .gsub(/\b#{Regexp.escape(inactive_plugin_class_name)}\b/, rsc_plugin_class_name) - .gsub(inactive_plugin_import_path, rsc_plugin_import_path) + .gsub(/\b#{Regexp.escape(inactive_rsc_plugin_class_name)}\b/, rsc_plugin_class_name) + .gsub(inactive_rsc_plugin_import_path, rsc_plugin_import_path) end end @@ -323,14 +318,28 @@ def normalize_rsc_plugin_import_for_active_bundler(content, inactive_plugin_clas end def rsc_plugin_commonjs_import_regex(import_path) - # Intentionally matches single-line destructuring only. Multi-line CommonJS destructuring is - # skipped safely because the lightweight regexp does not span newlines. + # Matches one CommonJS named-destructuring require statement. The binding list may span + # multiple lines (`[^}]*` crosses newlines), so multiline destructuring is normalized too; + # callers still guard matches with the JS top-level scanner before rewriting. Regexp.new( "^[ \\t]*(?:const|let|var)\\s+\\{[^}]*\\}\\s*=\\s*" \ "require\\(['\"]#{Regexp.escape(import_path)}['\"]\\);?[ \\t]*(?:\\r?\\n)?" ) end + def inactive_rsc_plugin_symbol_in_js_code?(content) + commonjs_named_imported?(content, inactive_rsc_plugin_import_path, inactive_rsc_plugin_class_name) || + inactive_rsc_plugin_esm_import?(content) || + inactive_rsc_plugin_invocation_in_js_code?(content) + end + + def inactive_rsc_plugin_invocation_in_js_code?(content) + pattern = /\bnew\s+#{Regexp.escape(inactive_rsc_plugin_class_name)}\s*\(/ + content.to_enum(:scan, pattern).any? do + js_code_position?(content, Regexp.last_match.begin(0)) + end + end + def inactive_rsc_plugin_esm_import?(content) pattern = Regexp.new( "^[ \\t]*import\\s+\\{[^}]*\\b#{Regexp.escape(inactive_rsc_plugin_class_name)}\\b[^}]*\\}" \ diff --git a/react_on_rails/spec/react_on_rails/generators/generator_helper_spec.rb b/react_on_rails/spec/react_on_rails/generators/generator_helper_spec.rb index 149baa961e..6c3f7bb94c 100644 --- a/react_on_rails/spec/react_on_rails/generators/generator_helper_spec.rb +++ b/react_on_rails/spec/react_on_rails/generators/generator_helper_spec.rb @@ -136,7 +136,8 @@ def options result = add_npm_dependencies(packages) expect(result).to be false expect(say_status_calls).to include(a_hash_including(message: a_string_matching(/Could not add packages/))) - expect(say_status_calls).to include(a_hash_including(message: "Will fall back to direct npm commands.")) + expect(say_status_calls) + .to include(a_hash_including(message: "Will fall back to direct package manager commands.")) end end end diff --git a/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb b/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb index 3b59fd4884..edbd4251fe 100644 --- a/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb +++ b/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb @@ -1988,6 +1988,54 @@ class ActiveSupport::TestCase end end + describe "#add_rsc_dependencies" do + let(:install_generator) { described_class.new([], { rsc: true }, destination_root: destination_root) } + let(:rsc_pin) { ReactOnRails::Generators::JsDependencyManager::RSC_PACKAGE_VERSION_PIN } + + before do + allow(install_generator).to receive(:say) + allow(install_generator).to receive(:fallback_package_manager).and_return("pnpm") + end + + it "explains why every RSC install is temporarily pinned to the prerelease package" do + allow(install_generator).to receive(:add_packages).and_return(true) + + install_generator.send(:add_rsc_dependencies) + + message_text = GeneratorMessages.messages.join("\n") + expect(message_text).to include("all --rsc installs") + expect(message_text).to include("react-on-rails-rsc@#{rsc_pin}") + expect(message_text).to include("stable react-on-rails-rsc@19.0.5") + expect(message_text).to include("react-on-rails-rsc/RspackPlugin") + expect(message_text).to include("Webpack") + end + + it "keeps the version pin and uses the detected package manager when manual RSC recovery is needed" do + allow(install_generator).to receive(:add_packages).and_return(false) + + install_generator.send(:add_rsc_dependencies) + + warning_text = GeneratorMessages.messages.join("\n") + expect(warning_text).to include("pnpm add --save-exact react-on-rails-rsc@#{rsc_pin}") + expect(warning_text).to include("left the version pin in package.json") + expect(warning_text).not_to include("npm install react-on-rails-rsc") + expect(warning_text).not_to include("Retrying latest available package") + end + + it "uses yarn add syntax in the RSC pin failure warning when yarn is detected" do + allow(install_generator).to receive_messages( + add_packages: false, + fallback_package_manager: "yarn" + ) + + install_generator.send(:add_rsc_dependencies) + + warning_text = GeneratorMessages.messages.join("\n") + expect(warning_text).to include("yarn add --exact react-on-rails-rsc@#{rsc_pin}") + expect(warning_text).not_to include("npm install react-on-rails-rsc") + end + end + context "with --new-app and a preexisting root route" do before(:all) do run_generator_test_with_args(%w[--new-app], package_json: true) do diff --git a/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb b/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb index b1fdd12f1f..7fb5e4bd95 100644 --- a/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb +++ b/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb @@ -503,7 +503,8 @@ def errors instance.send(:add_react_dependencies) expect(warnings.size).to be > 0 - expect(warnings.first.to_s).to include("npm install react@~19.0.4 react-dom@~19.0.4 prop-types@^15.0.0") + expect(warnings.first.to_s) + .to include("npm install --save-exact react@~19.0.4 react-dom@~19.0.4 prop-types@^15.0.0") end end @@ -668,7 +669,7 @@ def errors ) end - it "falls back to unversioned package when pinned install fails" do + it "keeps the pin instead of falling back to unversioned latest when pinned install fails" do rsc_package = "react-on-rails-rsc@#{ReactOnRails::Generators::JsDependencyManager::RSC_PACKAGE_VERSION_PIN}" allow(instance).to receive(:rsc_packages_with_version).and_return([[rsc_package], true]) @@ -678,8 +679,8 @@ def errors instance.send(:add_rsc_dependencies) expect(instance).to have_received(:add_packages).with([rsc_package]) - expect(instance).to have_received(:add_packages).with(["react-on-rails-rsc"]) - expect(warnings.join("\n")).to include("installed react-on-rails-rsc version may not match") + expect(instance).not_to have_received(:add_packages).with(["react-on-rails-rsc"]) + expect(warnings.join("\n")).to include("left the version pin in package.json") end it "skips the unversioned fallback for rspack and keeps the pin when the pinned install fails" do @@ -700,7 +701,7 @@ def errors expect(warning_text).to include("Could not install the pinned react-on-rails-rsc@19.0.5-rc.6") expect(warning_text).to include("react-on-rails-rsc/RspackPlugin") # the manual instruction points at the pinned version, not the unversioned package - expect(warning_text).to include("npm install react-on-rails-rsc@19.0.5-rc.6") + expect(warning_text).to include("npm install --save-exact react-on-rails-rsc@19.0.5-rc.6") end it "keeps the rspack pin in the manual install instruction when the pinned install raises" do @@ -718,7 +719,7 @@ def errors warning_text = warnings.join("\n") expect(warning_text).to include("Error adding React Server Components dependencies: network down") - expect(warning_text).to include("npm install react-on-rails-rsc@19.0.5-rc.6") + expect(warning_text).to include("npm install --save-exact react-on-rails-rsc@19.0.5-rc.6") end end @@ -779,7 +780,7 @@ def errors instance.send(:add_react_dependencies) warning = warnings.first - expect(warning.to_s).to include("npm install") + expect(warning.to_s).to include("npm install --save-exact") expect(warning.to_s).to include("manually") end end diff --git a/react_on_rails/spec/react_on_rails/generators/rsc_generator_spec.rb b/react_on_rails/spec/react_on_rails/generators/rsc_generator_spec.rb index 5fa636497d..9d38e3a909 100644 --- a/react_on_rails/spec/react_on_rails/generators/rsc_generator_spec.rb +++ b/react_on_rails/spec/react_on_rails/generators/rsc_generator_spec.rb @@ -2686,6 +2686,71 @@ ) end + it "reports a stale inactive import when an rspack client config also has the active plugin" do + allow(generator).to receive(:using_rspack?).and_return(true) + simulate_existing_file( + "config/rspack/clientWebpackConfig.js", + <<~JS + const { config } = require('shakapacker'); + const { resolve } = require('path'); + const { RSCRspackPlugin } = require('react-on-rails-rsc/RspackPlugin'); + const { RSCWebpackPlugin } = require('react-on-rails-rsc/WebpackPlugin'); + const rscClientReferences = { directory: resolve(config.source_path) }; + clientConfig.plugins.push( + new RSCRspackPlugin({ isServer: false, clientReferences: rscClientReferences }), + ); + JS + ) + + expect(generator.send(:check_rsc_client_config)).to include( + "stale RSCWebpackPlugin in clientWebpackConfig.js " \ + "(found alongside RSCRspackPlugin — remove the inactive bundler plugin manually)" + ) + end + + it "reports a stale inactive invocation when an rspack server config also has the active plugin" do + allow(generator).to receive(:using_rspack?).and_return(true) + simulate_existing_file( + "config/rspack/serverWebpackConfig.js", + <<~JS + const { config } = require('shakapacker'); + const { resolve } = require('path'); + const { RSCRspackPlugin } = require('react-on-rails-rsc/RspackPlugin'); + const rscClientReferences = { directory: resolve(config.source_path) }; + const rscBundle = false; + serverWebpackConfig.plugins.push( + new RSCRspackPlugin({ isServer: true, clientReferences: rscClientReferences }), + ); + serverWebpackConfig.plugins.push(new RSCWebpackPlugin({ isServer: true })); + JS + ) + + expect(generator.send(:check_rsc_server_config)).to include( + "stale RSCWebpackPlugin in serverWebpackConfig.js " \ + "(found alongside RSCRspackPlugin — remove the inactive bundler plugin manually)" + ) + end + + it "ignores inactive plugin names in comments and strings during mixed-plugin verification" do + allow(generator).to receive(:using_rspack?).and_return(true) + simulate_existing_file( + "config/rspack/clientWebpackConfig.js", + <<~JS + const { config } = require('shakapacker'); + const { resolve } = require('path'); + const { RSCRspackPlugin } = require('react-on-rails-rsc/RspackPlugin'); + const rscClientReferences = { directory: resolve(config.source_path) }; + // Previous import: const { RSCWebpackPlugin } = require('react-on-rails-rsc/WebpackPlugin'); + const migrationNote = "new RSCWebpackPlugin({ isServer: false })"; + clientConfig.plugins.push( + new RSCRspackPlugin({ isServer: false, clientReferences: rscClientReferences }), + ); + JS + ) + + expect(generator.send(:check_rsc_client_config)).to eq([]) + end + it "does not inject duplicate imports when existing bindings are indented" do config_path = "config/webpack/clientWebpackConfig.js" simulate_existing_file( From 3c99c285749c52300ea8cc5434964f4b1e3f8343 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Thu, 4 Jun 2026 23:49:29 -1000 Subject: [PATCH 2/6] Avoid duplicate RSC install warning command --- .../lib/generators/react_on_rails/js_dependency_manager.rb | 3 +-- .../react_on_rails/generators/js_dependency_manager_spec.rb | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb b/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb index a9722eb812..30fc7ec822 100644 --- a/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb +++ b/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb @@ -455,8 +455,7 @@ def rsc_dependency_pin_failed_warning "All RSC projects are temporarily pinned to that version because the unversioned " \ "`latest` tag may not export react-on-rails-rsc/RspackPlugin until stable 19.0.5 " \ "is published, so the generator left the version pin in package.json rather than " \ - "install a potentially incompatible version. " \ - "Run #{manual_add_packages_command(rsc_packages_with_pin)} to finish setup." + "install a potentially incompatible version." end def remove_base_package_if_present diff --git a/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb b/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb index 7fb5e4bd95..c17690323d 100644 --- a/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb +++ b/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb @@ -702,6 +702,7 @@ def errors expect(warning_text).to include("react-on-rails-rsc/RspackPlugin") # the manual instruction points at the pinned version, not the unversioned package expect(warning_text).to include("npm install --save-exact react-on-rails-rsc@19.0.5-rc.6") + expect(warning_text.scan("npm install --save-exact react-on-rails-rsc@19.0.5-rc.6").size).to eq(1) end it "keeps the rspack pin in the manual install instruction when the pinned install raises" do From 53627c920195c6c903284c8c582c422b5612b031 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Fri, 5 Jun 2026 00:04:53 -1000 Subject: [PATCH 3/6] Address RSC generator review follow-ups --- .../react_on_rails/js_dependency_manager.rb | 47 ++++++++++++------- .../generators/js_dependency_manager_spec.rb | 27 +++-------- 2 files changed, 35 insertions(+), 39 deletions(-) diff --git a/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb b/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb index 30fc7ec822..5840a5f0fa 100644 --- a/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb +++ b/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb @@ -413,16 +413,16 @@ def add_rsc_dependencies GeneratorMessages.add_info(rsc_dependency_pin_info) if used_version_pins return if add_packages(rsc_packages) - manual_install_packages = rsc_packages GeneratorMessages.add_warning(rsc_dependency_pin_failed_warning) if used_version_pins GeneratorMessages.add_warning(<<~MSG.strip) ⚠️ Failed to add React Server Components dependencies. You can install them manually by running: - #{manual_add_packages_command(manual_install_packages)} + #{manual_add_packages_command(rsc_packages)} MSG rescue StandardError => e + GeneratorMessages.add_warning(rsc_dependency_pin_failed_warning) if used_version_pins manual_install_packages = rsc_packages_with_pin GeneratorMessages.add_warning(<<~MSG.strip) ⚠️ Error adding React Server Components dependencies: #{e.message} @@ -613,18 +613,15 @@ def fallback_package_manager end def build_install_args(package_manager, dev, packages) - base_commands = { - "npm" => %w[npm install --save-exact], - "yarn" => %w[yarn add --exact], - "pnpm" => %w[pnpm add --save-exact], - "bun" => %w[bun add --exact] - } - - base_args = base_commands.fetch(package_manager).dup + base_args = package_manager_commands(package_manager).fetch(:install).dup base_args << dev_flag_for(package_manager) if dev base_args + packages end + def build_remove_args(package_manager, packages) + package_manager_commands(package_manager).fetch(:remove) + packages + end + def manual_add_packages_command(packages, dev: false) build_install_args(fallback_package_manager, dev, packages).join(" ") end @@ -634,14 +631,28 @@ def manual_install_dependencies_command end def manual_remove_packages_command(packages) - base_commands = { - "npm" => %w[npm uninstall], - "yarn" => %w[yarn remove], - "pnpm" => %w[pnpm remove], - "bun" => %w[bun remove] - } - - (base_commands.fetch(fallback_package_manager).dup + packages).join(" ") + build_remove_args(fallback_package_manager, packages).join(" ") + end + + def package_manager_commands(package_manager) + { + "npm" => { + install: %w[npm install --save-exact], + remove: %w[npm uninstall] + }, + "yarn" => { + install: %w[yarn add --exact], + remove: %w[yarn remove] + }, + "pnpm" => { + install: %w[pnpm add --save-exact], + remove: %w[pnpm remove] + }, + "bun" => { + install: %w[bun add --exact], + remove: %w[bun remove] + } + }.fetch(package_manager) end def dev_flag_for(package_manager) diff --git a/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb b/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb index c17690323d..6f9af2fa90 100644 --- a/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb +++ b/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb @@ -680,33 +680,17 @@ def errors expect(instance).to have_received(:add_packages).with([rsc_package]) expect(instance).not_to have_received(:add_packages).with(["react-on-rails-rsc"]) - expect(warnings.join("\n")).to include("left the version pin in package.json") - end - - it "skips the unversioned fallback for rspack and keeps the pin when the pinned install fails" do - instance.using_rspack = true - allow(instance) - .to receive(:rsc_packages_with_version) - .and_return([["react-on-rails-rsc@19.0.5-rc.6"], true]) - - allow(instance).to receive(:add_packages).with(["react-on-rails-rsc@19.0.5-rc.6"]).and_return(false) - allow(instance).to receive(:add_packages).with(["react-on-rails-rsc"]).and_return(true) - - instance.send(:add_rsc_dependencies) - - # rspack must NOT retry the unversioned `latest` (it lacks RspackPlugin) - expect(instance).not_to have_received(:add_packages).with(["react-on-rails-rsc"]) warning_text = warnings.join("\n") expect(warning_text).to include("Could not install the pinned react-on-rails-rsc@19.0.5-rc.6") + expect(warning_text).to include("left the version pin in package.json") expect(warning_text).to include("react-on-rails-rsc/RspackPlugin") - # the manual instruction points at the pinned version, not the unversioned package - expect(warning_text).to include("npm install --save-exact react-on-rails-rsc@19.0.5-rc.6") - expect(warning_text.scan("npm install --save-exact react-on-rails-rsc@19.0.5-rc.6").size).to eq(1) + manual_command = "npm install --save-exact react-on-rails-rsc@19.0.5-rc.6" + expect(warning_text).to include(manual_command) + expect(warning_text.scan(manual_command).size).to eq(1) end - it "keeps the rspack pin in the manual install instruction when the pinned install raises" do - instance.using_rspack = true + it "keeps the pinned manual install instruction when the pinned install raises" do allow(instance) .to receive(:rsc_packages_with_version) .and_return([["react-on-rails-rsc@19.0.5-rc.6"], true]) @@ -719,6 +703,7 @@ def errors expect(instance).not_to have_received(:add_packages).with(["react-on-rails-rsc"]) warning_text = warnings.join("\n") + expect(warning_text).to include("Could not install the pinned react-on-rails-rsc@19.0.5-rc.6") expect(warning_text).to include("Error adding React Server Components dependencies: network down") expect(warning_text).to include("npm install --save-exact react-on-rails-rsc@19.0.5-rc.6") end From ff79f65fc704ba10d58fa38ff1c7258c7a43e2af Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Fri, 5 Jun 2026 00:42:10 -1000 Subject: [PATCH 4/6] Derive RSC stable target from pin --- .../react_on_rails/js_dependency_manager.rb | 10 ++++++++-- .../generators/install_generator_spec.rb | 3 ++- .../generators/js_dependency_manager_spec.rb | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb b/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb index 5840a5f0fa..4edaae0b45 100644 --- a/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb +++ b/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb @@ -442,18 +442,24 @@ def rsc_packages_with_pin RSC_DEPENDENCIES.map { |pkg| "#{pkg}@#{RSC_PACKAGE_VERSION_PIN}" } end + def rsc_stable_package_version_target + RSC_PACKAGE_VERSION_PIN.split("-", 2).first + end + def rsc_dependency_pin_info "React Server Components package pin: all --rsc installs temporarily use " \ "react-on-rails-rsc@#{RSC_PACKAGE_VERSION_PIN}, including Webpack projects. " \ "This prerelease keeps react-on-rails-rsc/WebpackPlugin compatible while adding " \ - "react-on-rails-rsc/RspackPlugin. Keep the pin until stable react-on-rails-rsc@19.0.5 " \ + "react-on-rails-rsc/RspackPlugin. Keep the pin until stable " \ + "react-on-rails-rsc@#{rsc_stable_package_version_target} " \ "is published and tagged latest." end def rsc_dependency_pin_failed_warning "Warning: Could not install the pinned react-on-rails-rsc@#{RSC_PACKAGE_VERSION_PIN}. " \ "All RSC projects are temporarily pinned to that version because the unversioned " \ - "`latest` tag may not export react-on-rails-rsc/RspackPlugin until stable 19.0.5 " \ + "`latest` tag may not export react-on-rails-rsc/RspackPlugin until stable " \ + "#{rsc_stable_package_version_target} " \ "is published, so the generator left the version pin in package.json rather than " \ "install a potentially incompatible version." end diff --git a/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb b/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb index edbd4251fe..0b3216a64c 100644 --- a/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb +++ b/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb @@ -1991,6 +1991,7 @@ class ActiveSupport::TestCase describe "#add_rsc_dependencies" do let(:install_generator) { described_class.new([], { rsc: true }, destination_root: destination_root) } let(:rsc_pin) { ReactOnRails::Generators::JsDependencyManager::RSC_PACKAGE_VERSION_PIN } + let(:rsc_stable_target) { rsc_pin.split("-", 2).first } before do allow(install_generator).to receive(:say) @@ -2005,7 +2006,7 @@ class ActiveSupport::TestCase message_text = GeneratorMessages.messages.join("\n") expect(message_text).to include("all --rsc installs") expect(message_text).to include("react-on-rails-rsc@#{rsc_pin}") - expect(message_text).to include("stable react-on-rails-rsc@19.0.5") + expect(message_text).to include("stable react-on-rails-rsc@#{rsc_stable_target}") expect(message_text).to include("react-on-rails-rsc/RspackPlugin") expect(message_text).to include("Webpack") end diff --git a/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb b/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb index 6f9af2fa90..b57534b6d7 100644 --- a/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb +++ b/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb @@ -657,6 +657,22 @@ def errors end end + describe "RSC package pin messages" do + it "derives the stable release target from the prerelease package pin" do + stub_const("ReactOnRails::Generators::JsDependencyManager::RSC_PACKAGE_VERSION_PIN", "19.1.0-rc.1") + + info = instance.send(:rsc_dependency_pin_info) + warning = instance.send(:rsc_dependency_pin_failed_warning) + + expect(info).to include("react-on-rails-rsc@19.1.0-rc.1") + expect(info).to include("stable react-on-rails-rsc@19.1.0") + expect(info).not_to include("stable react-on-rails-rsc@19.0.5") + expect(warning).to include("pinned react-on-rails-rsc@19.1.0-rc.1") + expect(warning).to include("stable 19.1.0") + expect(warning).not_to include("stable 19.0.5") + end + end + describe "#add_rsc_dependencies" do it "installs version-pinned rsc dependency" do rsc_package = "react-on-rails-rsc@#{ReactOnRails::Generators::JsDependencyManager::RSC_PACKAGE_VERSION_PIN}" From d6748c9dd6ba973fc406f6610b7dac63c4e14d54 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Fri, 5 Jun 2026 01:26:27 -1000 Subject: [PATCH 5/6] Address RSC generator review comments --- .../react_on_rails/js_dependency_manager.rb | 23 +++++++++------ .../generators/install_generator_spec.rb | 2 +- .../generators/js_dependency_manager_spec.rb | 29 +++++++++++++++++++ 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb b/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb index 4edaae0b45..4b57f03a64 100644 --- a/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb +++ b/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb @@ -408,27 +408,25 @@ def pro_packages_with_version end def add_rsc_dependencies + rsc_packages = RSC_DEPENDENCIES + used_version_pins = false say "Installing React Server Components dependencies..." rsc_packages, used_version_pins = rsc_packages_with_version GeneratorMessages.add_info(rsc_dependency_pin_info) if used_version_pins return if add_packages(rsc_packages) - GeneratorMessages.add_warning(rsc_dependency_pin_failed_warning) if used_version_pins - GeneratorMessages.add_warning(<<~MSG.strip) ⚠️ Failed to add React Server Components dependencies. - + #{rsc_dependency_pin_failure_details(used_version_pins)} You can install them manually by running: #{manual_add_packages_command(rsc_packages)} MSG rescue StandardError => e - GeneratorMessages.add_warning(rsc_dependency_pin_failed_warning) if used_version_pins - manual_install_packages = rsc_packages_with_pin GeneratorMessages.add_warning(<<~MSG.strip) ⚠️ Error adding React Server Components dependencies: #{e.message} - + #{rsc_dependency_pin_failure_details(used_version_pins)} You can install them manually by running: - #{manual_add_packages_command(manual_install_packages)} + #{manual_add_packages_command(rsc_packages)} MSG end @@ -457,13 +455,20 @@ def rsc_dependency_pin_info def rsc_dependency_pin_failed_warning "Warning: Could not install the pinned react-on-rails-rsc@#{RSC_PACKAGE_VERSION_PIN}. " \ - "All RSC projects are temporarily pinned to that version because the unversioned " \ - "`latest` tag may not export react-on-rails-rsc/RspackPlugin until stable " \ + "All RSC projects are temporarily pinned to that version: the prerelease keeps " \ + "react-on-rails-rsc/WebpackPlugin compatible while adding react-on-rails-rsc/RspackPlugin, " \ + "and the unversioned `latest` tag may not include both until stable " \ "#{rsc_stable_package_version_target} " \ "is published, so the generator left the version pin in package.json rather than " \ "install a potentially incompatible version." end + def rsc_dependency_pin_failure_details(used_version_pins) + return "" unless used_version_pins + + "\n#{rsc_dependency_pin_failed_warning}\n" + end + def remove_base_package_if_present pj = package_json return unless pj diff --git a/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb b/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb index 0b3216a64c..74005a856a 100644 --- a/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb +++ b/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb @@ -1991,7 +1991,7 @@ class ActiveSupport::TestCase describe "#add_rsc_dependencies" do let(:install_generator) { described_class.new([], { rsc: true }, destination_root: destination_root) } let(:rsc_pin) { ReactOnRails::Generators::JsDependencyManager::RSC_PACKAGE_VERSION_PIN } - let(:rsc_stable_target) { rsc_pin.split("-", 2).first } + let(:rsc_stable_target) { install_generator.send(:rsc_stable_package_version_target) } before do allow(install_generator).to receive(:say) diff --git a/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb b/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb index b57534b6d7..ed6fd07a5a 100644 --- a/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb +++ b/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb @@ -668,6 +668,8 @@ def errors expect(info).to include("stable react-on-rails-rsc@19.1.0") expect(info).not_to include("stable react-on-rails-rsc@19.0.5") expect(warning).to include("pinned react-on-rails-rsc@19.1.0-rc.1") + expect(warning).to include("react-on-rails-rsc/WebpackPlugin") + expect(warning).to include("react-on-rails-rsc/RspackPlugin") expect(warning).to include("stable 19.1.0") expect(warning).not_to include("stable 19.0.5") end @@ -698,8 +700,10 @@ def errors expect(instance).not_to have_received(:add_packages).with(["react-on-rails-rsc"]) warning_text = warnings.join("\n") + expect(warnings.size).to eq(1) expect(warning_text).to include("Could not install the pinned react-on-rails-rsc@19.0.5-rc.6") expect(warning_text).to include("left the version pin in package.json") + expect(warning_text).to include("react-on-rails-rsc/WebpackPlugin") expect(warning_text).to include("react-on-rails-rsc/RspackPlugin") manual_command = "npm install --save-exact react-on-rails-rsc@19.0.5-rc.6" expect(warning_text).to include(manual_command) @@ -719,10 +723,35 @@ def errors expect(instance).not_to have_received(:add_packages).with(["react-on-rails-rsc"]) warning_text = warnings.join("\n") + expect(warnings.size).to eq(1) expect(warning_text).to include("Could not install the pinned react-on-rails-rsc@19.0.5-rc.6") expect(warning_text).to include("Error adding React Server Components dependencies: network down") expect(warning_text).to include("npm install --save-exact react-on-rails-rsc@19.0.5-rc.6") end + + it "uses computed rsc packages for manual recovery when installation raises" do + rsc_package = "custom-rsc-package@1.2.3" + allow(instance).to receive(:rsc_packages_with_version).and_return([[rsc_package], true]) + allow(instance).to receive(:add_packages).with([rsc_package]).and_raise("network down") + allow(instance).to receive(:rsc_packages_with_pin).and_raise("should not be called") + + instance.send(:add_rsc_dependencies) + + warning_text = warnings.join("\n") + expect(warning_text).to include("Error adding React Server Components dependencies: network down") + expect(warning_text).to include("npm install --save-exact #{rsc_package}") + end + + it "falls back to unversioned rsc packages when version resolution raises" do + allow(instance).to receive(:rsc_packages_with_version).and_raise("pin lookup failed") + allow(instance).to receive(:rsc_packages_with_pin).and_raise("should not be called") + + instance.send(:add_rsc_dependencies) + + warning_text = warnings.join("\n") + expect(warning_text).to include("Error adding React Server Components dependencies: pin lookup failed") + expect(warning_text).to include("npm install --save-exact react-on-rails-rsc") + end end describe "#add_babel_react_dependencies" do From f17926ae022ccb8df7f2867d3eef6d63a12df4e3 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Fri, 5 Jun 2026 02:28:39 -1000 Subject: [PATCH 6/6] Address RSC generator review follow-ups --- .../react_on_rails/js_dependency_manager.rb | 61 ++++++++++++++----- .../generators/install_generator_spec.rb | 1 + .../generators/js_dependency_manager_spec.rb | 21 +++++-- 3 files changed, 64 insertions(+), 19 deletions(-) diff --git a/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb b/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb index 4b57f03a64..dda8a57d62 100644 --- a/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb +++ b/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb @@ -415,19 +415,21 @@ def add_rsc_dependencies GeneratorMessages.add_info(rsc_dependency_pin_info) if used_version_pins return if add_packages(rsc_packages) - GeneratorMessages.add_warning(<<~MSG.strip) - ⚠️ Failed to add React Server Components dependencies. - #{rsc_dependency_pin_failure_details(used_version_pins)} - You can install them manually by running: - #{manual_add_packages_command(rsc_packages)} - MSG + GeneratorMessages.add_warning( + rsc_dependency_failure_message( + "⚠️ Failed to add React Server Components dependencies.", + used_version_pins, + rsc_packages + ) + ) rescue StandardError => e - GeneratorMessages.add_warning(<<~MSG.strip) - ⚠️ Error adding React Server Components dependencies: #{e.message} - #{rsc_dependency_pin_failure_details(used_version_pins)} - You can install them manually by running: - #{manual_add_packages_command(rsc_packages)} - MSG + GeneratorMessages.add_warning( + rsc_dependency_failure_message( + "⚠️ Error adding React Server Components dependencies: #{e.message}", + used_version_pins, + rsc_packages + ) + ) end # Returns [pinned_packages, used_version_pins]. used_version_pins is always true here; @@ -464,9 +466,18 @@ def rsc_dependency_pin_failed_warning end def rsc_dependency_pin_failure_details(used_version_pins) - return "" unless used_version_pins + return unless used_version_pins + + rsc_dependency_pin_failed_warning + end - "\n#{rsc_dependency_pin_failed_warning}\n" + def rsc_dependency_failure_message(summary, used_version_pins, rsc_packages) + [ + summary, + rsc_dependency_pin_failure_details(used_version_pins), + "You can install them manually by running:", + " #{manual_add_packages_command(rsc_packages)}" + ].compact.join("\n") end def remove_base_package_if_present @@ -625,6 +636,7 @@ def fallback_package_manager def build_install_args(package_manager, dev, packages) base_args = package_manager_commands(package_manager).fetch(:install).dup + base_args -= exact_install_flags_for(package_manager) if packages_include_semver_ranges?(packages) base_args << dev_flag_for(package_manager) if dev base_args + packages end @@ -675,6 +687,27 @@ def dev_flag_for(package_manager) end end + def exact_install_flags_for(package_manager) + case package_manager + when "npm", "pnpm" then ["--save-exact"] + when "yarn", "bun" then ["--exact"] + else + raise ArgumentError, "Unknown package manager for exact install flag: #{package_manager}" + end + end + + def packages_include_semver_ranges?(packages) + packages.any? { |package_spec| package_uses_semver_range?(package_spec) } + end + + def package_uses_semver_range?(package_spec) + package_name_and_version = package_name_and_version_from_spec(package_spec) + return false unless package_name_and_version + + _package_name, package_version = package_name_and_version + package_version.start_with?("~", "^") + end + def filter_missing_packages(packages) existing = existing_package_names return packages if existing.empty? diff --git a/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb b/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb index 74005a856a..e4a1ea9610 100644 --- a/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb +++ b/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb @@ -1994,6 +1994,7 @@ class ActiveSupport::TestCase let(:rsc_stable_target) { install_generator.send(:rsc_stable_package_version_target) } before do + GeneratorMessages.clear allow(install_generator).to receive(:say) allow(install_generator).to receive(:fallback_package_manager).and_return("pnpm") end diff --git a/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb b/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb index ed6fd07a5a..7b33963e00 100644 --- a/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb +++ b/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb @@ -260,14 +260,24 @@ def errors expect(instance.system_calls).to eq([]) end - it "does not skip fallback install for versioned package specs" do + it "preserves semver ranges in fallback install commands without exact-save flags" do instance.add_npm_dependencies_result = false allow(instance).to receive(:existing_package_names).and_return(%w[react]) result = instance.send(:add_packages, ["react@~19.0.4"]) expect(result).to be(true) - expect(instance.system_calls).to include(%w[npm install --save-exact react@~19.0.4]) + expect(instance.system_calls).to include(%w[npm install react@~19.0.4]) + end + + it "uses exact-save fallback commands for exact package pins" do + instance.add_npm_dependencies_result = false + allow(instance).to receive(:existing_package_names).and_return(%w[react-on-rails-rsc]) + + result = instance.send(:add_packages, ["react-on-rails-rsc@19.0.5-rc.6"]) + + expect(result).to be(true) + expect(instance.system_calls).to include(%w[npm install --save-exact react-on-rails-rsc@19.0.5-rc.6]) end end @@ -495,7 +505,7 @@ def errors expect(warnings.first.to_s).to include("Failed to add React dependencies") end - it "warns with the pinned React install command when the RSC add fails" do + it "warns with a range-based React install command when the RSC add fails" do instance.use_rsc = true instance.add_npm_dependencies_result = false instance.system_result = false @@ -504,7 +514,7 @@ def errors expect(warnings.size).to be > 0 expect(warnings.first.to_s) - .to include("npm install --save-exact react@~19.0.4 react-dom@~19.0.4 prop-types@^15.0.0") + .to include("npm install react@~19.0.4 react-dom@~19.0.4 prop-types@^15.0.0") end end @@ -751,6 +761,7 @@ def errors warning_text = warnings.join("\n") expect(warning_text).to include("Error adding React Server Components dependencies: pin lookup failed") expect(warning_text).to include("npm install --save-exact react-on-rails-rsc") + expect(warning_text).not_to include("pin lookup failed\n\nYou can install") end end @@ -811,7 +822,7 @@ def errors instance.send(:add_react_dependencies) warning = warnings.first - expect(warning.to_s).to include("npm install --save-exact") + expect(warning.to_s).to include("npm install") expect(warning.to_s).to include("manually") end end