diff --git a/.lychee.toml b/.lychee.toml index 730f373ce7..ed607496ec 100644 --- a/.lychee.toml +++ b/.lychee.toml @@ -80,6 +80,7 @@ exclude = [ # ============================================================================ '^https://blog\.shakacode\.com', # May block automated requests '^https?://(www\.)?foxford\.ru', # Russian site, often blocks bots + '^https://(www\.)?guavapass\.com', # TLS handshake fails from GitHub Actions (bot filter) '^https://(www\.)?airgoat\.com', # Returns 403 '^https://(www\.)?first\.io', # Returns 403 '^https://(www\.)?estately\.com', # Returns 403 diff --git a/CHANGELOG.md b/CHANGELOG.md index 470d279907..86b96ba30b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,10 @@ After a release, run `/update-changelog` in Claude Code to analyze commits, writ ### [Unreleased] +#### Removed + +- **[Pro]** **Removed the `--rsc-pro` install generator flag**: `--rsc` already implies Pro, so the separate mode was unnecessary. Behaviors previously gated on `--rsc-pro` (Pro verification checklist, prerelease install note, exact Pro gem pin on prereleases) now fire on `--rsc` installs. See also [Issue 3104](https://github.com/shakacode/react_on_rails/issues/3104), which tracks unrelated silent-failure bugs in the Pro upgrade automation. [PR 3105](https://github.com/shakacode/react_on_rails/pull/3105) by [ihabadham](https://github.com/ihabadham). + #### Changed - **[Pro]** **Pro generator now creates the Node Renderer at `renderer/node-renderer.js`**: The canonical location for the Node Renderer entry point is now a dedicated top-level `renderer/` directory instead of `client/`, making it straightforward to exclude from production Docker builds that strip JS sources after bundling. Docs and Pro `spec/dummy` now use the new path consistently. Existing apps are unaffected — the generator skips files that already exist (including a legacy `client/node-renderer.js`). Fixes [Issue 3073](https://github.com/shakacode/react_on_rails/issues/3073). [PR 3165](https://github.com/shakacode/react_on_rails/pull/3165) by [justin808](https://github.com/justin808). 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 ea884d562b..0036d20d94 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 @@ -152,28 +152,20 @@ def mark_pro_gem_installed! @pro_gem_installed = true end - # Check if first-class RSC Pro mode should be enabled. - # Returns true when --rsc-pro is set, or when users explicitly pass both --rsc and --pro. - # - # @return [Boolean] true if RSC Pro mode semantics should be applied - def use_rsc_pro_mode? - options[:rsc_pro] || (options[:rsc] && options[:pro]) - end - # Check if Pro features should be enabled. - # Returns true if --pro, --rsc, or --rsc-pro is set (RSC implies Pro). + # Returns true if --pro or --rsc is set (RSC implies Pro). # # @return [Boolean] true if Pro setup should be included def use_pro? - options[:pro] || options[:rsc] || options[:rsc_pro] + options[:pro] || options[:rsc] end - # Check if RSC (React Server Components) should be enabled - # Returns true if --rsc or --rsc-pro is explicitly set + # Check if RSC (React Server Components) should be enabled. + # Returns true if --rsc is set. # # @return [Boolean] true if RSC setup should be included def use_rsc? - options[:rsc] || options[:rsc_pro] + options[:rsc] end # Determine if the project is using rspack as the bundler. diff --git a/react_on_rails/lib/generators/react_on_rails/install_generator.rb b/react_on_rails/lib/generators/react_on_rails/install_generator.rb index fc2f502b4c..6a601b25d1 100644 --- a/react_on_rails/lib/generators/react_on_rails/install_generator.rb +++ b/react_on_rails/lib/generators/react_on_rails/install_generator.rb @@ -57,21 +57,13 @@ class InstallGenerator < Rails::Generators::Base class_option :pro, type: :boolean, default: false, - desc: "Install React on Rails Pro with Node Renderer. " \ - "Combined with --rsc, uses --rsc-pro mode. Default: false" + desc: "Install React on Rails Pro with Node Renderer. Default: false" # --rsc class_option :rsc, type: :boolean, default: false, - desc: "Install React Server Components support (includes Pro). " \ - "Combined with --pro, uses --rsc-pro mode. Default: false" - - # --rsc-pro - class_option :rsc_pro, - type: :boolean, - default: false, - desc: "Install first-class Pro RSC mode with matched Pro/RSC defaults. Default: false" + desc: "Install React Server Components support (includes Pro). Default: false" # Hidden option: allows tests (and advanced users) to signal that Shakapacker # was just installed, triggering force-overwrite of shakapacker.yml with RoR's template. @@ -644,7 +636,7 @@ def add_post_install_message ci_workflow_generated: @ci_workflow_generated == true, app_root: destination_root )) - GeneratorMessages.add_info(rsc_pro_verification_message) if use_rsc_pro_mode? + GeneratorMessages.add_info(rsc_verification_message) if use_rsc? end def shakapacker_setup_incomplete? @@ -658,9 +650,7 @@ def recovery_install_command flags << "--typescript" if options.typescript? flags << "--rspack" if options.rspack? - if use_rsc_pro_mode? - flags << "--rsc-pro" - elsif options.rsc? + if options.rsc? flags << "--rsc" elsif options.pro? flags << "--pro" @@ -669,7 +659,7 @@ def recovery_install_command ["rails generate react_on_rails:install", *flags].join(" ") end - def rsc_pro_verification_message + def rsc_verification_message <<~MSG 🔎 RSC Pro Verification: diff --git a/react_on_rails/lib/generators/react_on_rails/pro_setup.rb b/react_on_rails/lib/generators/react_on_rails/pro_setup.rb index 4242d8def0..d33e52f10e 100644 --- a/react_on_rails/lib/generators/react_on_rails/pro_setup.rb +++ b/react_on_rails/lib/generators/react_on_rails/pro_setup.rb @@ -60,14 +60,12 @@ def missing_pro_gem?(force: false) return false if pro_gem_installed? return false if attempt_pro_gem_auto_install - context_line = pro_gem_requirement_context_line - prerelease_note = rsc_pro_prerelease_note + optional_prerelease_line = prerelease_note.empty? ? "" : "\n#{prerelease_note}" GeneratorMessages.add_error(<<~MSG.strip) 🚫 Failed to auto-install #{PRO_GEM_NAME} gem. - #{context_line} - #{prerelease_note} + #{pro_gem_requirement_context_line}#{optional_prerelease_line} Please add manually to your Gemfile: gem '#{PRO_GEM_NAME}', '#{pro_gem_version_requirement}' @@ -94,20 +92,16 @@ def pro_flag_specified_for_context? end def pro_requirement_flag - return "--rsc-pro" if use_rsc_pro_mode? return "--rsc" if options[:rsc] "--pro" end - def rsc_pro_prerelease_note - return "" unless use_rsc_pro_mode? - return "" unless Gem::Version.new(ReactOnRails::VERSION).prerelease? + def prerelease_note + return "" unless prerelease_ror_version? "Note: #{PRO_GEM_NAME} #{ReactOnRails::VERSION} may not be published yet. " \ "If you are testing from source, use a local Gemfile `path:` option." - rescue ArgumentError - "" end # Attempt to auto-install the Pro gem via bundle add. @@ -537,13 +531,20 @@ def pro_gem_auto_install_command end def pro_gem_version_requirement - # RSC Pro uses exact pinning so the Pro gem version always matches the - # paired RSC package version generated in the same run. - return ReactOnRails::VERSION if use_rsc_pro_mode? + # Prerelease gem versions need an exact pin: Bundler's pessimistic operator + # (~>) does not match prerelease versions, so a stable range would fail to + # install during prerelease cycles. + return ReactOnRails::VERSION if prerelease_ror_version? "~> #{recommended_pro_gem_version}" end + def prerelease_ror_version? + Gem::Version.new(ReactOnRails::VERSION).prerelease? + rescue ArgumentError + false + end + # Keep manual fallback pinned to the latest stable release (drop pre-release suffixes like .rc.N). # react_on_rails_pro follows the same version number as react_on_rails by policy. # Both gems are released in lockstep; if this ever changes, replace with a dedicated constant. 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 a7c413637d..fafd808a2d 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 @@ -203,29 +203,26 @@ def self.read end end - describe "RSC Pro mode helpers" do - it "enables rsc-pro mode for explicit --rsc-pro flag" do - allow(self).to receive(:options).and_return({ rsc_pro: true, rsc: false, pro: false }) + describe "Pro/RSC flag helpers" do + it "treats --rsc as implying Pro" do + allow(self).to receive(:options).and_return({ rsc: true, pro: false }) - expect(use_rsc_pro_mode?).to be(true) expect(use_rsc?).to be(true) expect(use_pro?).to be(true) end - it "enables rsc-pro mode when --rsc and --pro are both set" do - allow(self).to receive(:options).and_return({ rsc_pro: false, rsc: true, pro: true }) + it "enables Pro without RSC for --pro alone" do + allow(self).to receive(:options).and_return({ rsc: false, pro: true }) - expect(use_rsc_pro_mode?).to be(true) - expect(use_rsc?).to be(true) + expect(use_rsc?).to be(false) expect(use_pro?).to be(true) end - it "does not enable rsc-pro mode for standalone --pro" do - allow(self).to receive(:options).and_return({ rsc_pro: false, rsc: false, pro: true }) + it "does not enable Pro or RSC for a plain install" do + allow(self).to receive(:options).and_return({ rsc: false, pro: false }) - expect(use_rsc_pro_mode?).to be(false) expect(use_rsc?).to be(false) - expect(use_pro?).to be(true) + expect(use_pro?).to be(false) 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 e24cffc09f..9b792be7ee 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 @@ -1575,13 +1575,16 @@ class ActiveSupport::TestCase end end - it "installs RSC npm dependencies" do + it "installs RSC npm dependencies with matched version pins" do + expected_npm_version = ReactOnRails::VersionSyntaxConverter.new.rubygem_to_npm(ReactOnRails::VERSION) + expected_rsc_npm_version = ReactOnRails::Generators::JsDependencyManager::RSC_PACKAGE_VERSION_PIN + assert_file "package.json" do |content| package_json = JSON.parse(content) deps = package_json["dependencies"] || {} - expect(deps).to include("react-on-rails-pro") - expect(deps).to include("react-on-rails-pro-node-renderer") - expect(deps).to include("react-on-rails-rsc") + expect(deps["react-on-rails-pro"]).to eq(expected_npm_version) + expect(deps["react-on-rails-pro-node-renderer"]).to eq(expected_npm_version) + expect(deps["react-on-rails-rsc"]).to eq(expected_rsc_npm_version) end end @@ -1724,91 +1727,6 @@ class ActiveSupport::TestCase end end - context "with --rsc-pro" do - before(:all) { run_generator_test_with_args(%w[--rsc-pro], package_json: true) } - - include_examples "rsc_common_files" - include_examples "rsc_hello_server_files" - - it "pins Pro dependencies and installs the RSC dependency" do - expected_npm_version = ReactOnRails::VersionSyntaxConverter.new.rubygem_to_npm(ReactOnRails::VERSION) - expected_rsc_npm_version = ReactOnRails::Generators::JsDependencyManager::RSC_PACKAGE_VERSION_PIN - - assert_file "package.json" do |content| - package_json = JSON.parse(content) - deps = package_json["dependencies"] || {} - expect(deps["react-on-rails-rsc"]).to eq(expected_rsc_npm_version) - expect(deps["react-on-rails-pro"]).to eq(expected_npm_version) - expect(deps["react-on-rails-pro-node-renderer"]).to eq(expected_npm_version) - end - end - - it "creates node-renderer.js" do - assert_file "renderer/node-renderer.js" do |content| - expect(content).to include("reactOnRailsProNodeRenderer") - expect(content).to include("require('react-on-rails-pro-node-renderer')") - end - end - - it "adds RSC bundle watcher to Procfile.dev" do - assert_file "Procfile.dev" do |content| - expect(content).to include("RSC_BUNDLE_ONLY=true") - expect(content).to include("rsc-bundle:") - expect(content).to include("bin/shakapacker-watch --watch") - end - end - - it "creates HelloServer instead of HelloWorld" do - assert_no_file "app/javascript/src/HelloWorld/ror_components/HelloWorld.client.jsx" - assert_no_file "app/javascript/src/HelloWorld/ror_components/HelloWorld.server.jsx" - assert_no_file "app/controllers/hello_world_controller.rb" - assert_file "config/routes.rb" do |content| - expect(content).not_to include("hello_world") - end - - assert_file "app/javascript/src/HelloServer/ror_components/HelloServer.jsx" - assert_file "app/javascript/src/HelloServer/components/HelloServer.jsx" - assert_file "app/javascript/src/HelloServer/components/LikeButton.jsx" - end - - it "adds HelloServer route and RSC payload route" do - assert_file "config/routes.rb" do |content| - expect(content).to include("hello_server") - expect(content).to include("rsc_payload") - end - end - - it "creates rscWebpackConfig.js" do - assert_file "config/webpack/rscWebpackConfig.js" do |content| - expect(content).to include("const serverWebpackModule = require('./serverWebpackConfig')") - expect(content).to include("const serverWebpackConfig = serverWebpackModule.default || serverWebpackModule") - expect(content).to include("serverWebpackConfig(true)") - expect(content).to include("rsc-bundle") - expect(content).to include("react-server") - end - end - - it "serverWebpackConfig includes RSCWebpackPlugin import" do - assert_file "config/webpack/serverWebpackConfig.js" do |content| - expect(content).to include("RSCWebpackPlugin") - expect(content).to include("react-on-rails-rsc/WebpackPlugin") - end - end - - it "serverWebpackConfig has rscBundle parameter" do - assert_file "config/webpack/serverWebpackConfig.js" do |content| - expect(content).to match(/configureServer\s*=\s*\(rscBundle\s*=\s*false\)/) - expect(content).to include("if (!rscBundle)") - end - end - - it "sets DEFAULT_ROUTE to hello_server in bin/dev" do - assert_file "bin/dev" do |content| - expect(content).to include('DEFAULT_ROUTE = "hello_server"') - end - end - end - context "with --rsc --redux" do before(:all) { run_generator_test_with_args(%w[--rsc --redux], package_json: true) } @@ -2246,13 +2164,12 @@ class ActiveSupport::TestCase command = install_generator.send(:recovery_install_command) - expect(command).to eq("rails generate react_on_rails:install --redux --typescript --rspack --rsc-pro") + expect(command).to eq("rails generate react_on_rails:install --redux --typescript --rspack --rsc") expect(command).not_to include("--ignore-warnings") expect(command).not_to include("--force") expect(command).not_to include("--skip") expect(command).not_to include("--pretend") expect(command).not_to include("--pro") - expect(command).not_to match(/\s--rsc(\s|$)/) end specify "recovery_install_command includes --pro when requested without --rsc" do @@ -2263,16 +2180,6 @@ class ActiveSupport::TestCase expect(command).to eq("rails generate react_on_rails:install --pro") end - specify "recovery_install_command prefers --rsc-pro over --rsc/--pro" do - install_generator = described_class.new([], { rsc_pro: true, rsc: true, pro: true }) - - command = install_generator.send(:recovery_install_command) - - expect(command).to eq("rails generate react_on_rails:install --rsc-pro") - expect(command).not_to match(/\s--rsc(\s|$)/) - expect(command).not_to match(/\s--pro(\s|$)/) - end - specify "shakapacker install error preserves original install flags" do install_generator = described_class.new([], { redux: true, typescript: true, ignore_warnings: true }) @@ -2295,8 +2202,8 @@ class ActiveSupport::TestCase expect(output_text).to include("Then re-run: rails generate react_on_rails:install --rspack --pro") end - specify "rsc-pro installs include a dedicated verification checklist message" do - run_generator_test_with_args(%w[--rsc-pro], package_json: true) do + specify "rsc installs include the Pro verification checklist message" do + run_generator_test_with_args(%w[--rsc], package_json: true) do simulate_existing_file("bin/shakapacker", "") simulate_existing_file("bin/shakapacker-dev-server", "") simulate_existing_file("config/shakapacker.yml", "default: {}\n") @@ -2406,40 +2313,6 @@ class ActiveSupport::TestCase redux_pro_rsc_install_generator.send(:invoke_generators) end - - it "treats --rsc-pro as pro+rsc when invoking sub-generators" do - rsc_pro_install_generator = described_class.new([], { pretend: true, rsc_pro: true }) - - allow(rsc_pro_install_generator).to receive(:ensure_shakapacker_installed) - allow(rsc_pro_install_generator).to receive(:setup_react_dependencies) - - expect(rsc_pro_install_generator).to receive(:invoke) - .with("react_on_rails:base", [], hash_including(pro: true, rsc: true, pretend: true)) - expect(rsc_pro_install_generator).to receive(:invoke) - .with("react_on_rails:pro", [], hash_including(pretend: true)) - expect(rsc_pro_install_generator).to receive(:invoke) - .with("react_on_rails:rsc", [], hash_including(pretend: true)) - - rsc_pro_install_generator.send(:invoke_generators) - end - - it "forwards RSC mode to the Redux generator for --rsc-pro --redux" do - rsc_pro_redux_install_generator = described_class.new([], { pretend: true, rsc_pro: true, redux: true }) - - allow(rsc_pro_redux_install_generator).to receive(:ensure_shakapacker_installed) - allow(rsc_pro_redux_install_generator).to receive(:setup_react_dependencies) - - expect(rsc_pro_redux_install_generator).to receive(:invoke) - .with("react_on_rails:base", [], hash_including(pro: true, rsc: true, pretend: true)) - expect(rsc_pro_redux_install_generator).to receive(:invoke) - .with("react_on_rails:react_with_redux", [], hash_including(rsc: true, pretend: true)) - expect(rsc_pro_redux_install_generator).to receive(:invoke) - .with("react_on_rails:pro", [], hash_including(pretend: true)) - expect(rsc_pro_redux_install_generator).to receive(:invoke) - .with("react_on_rails:rsc", [], hash_including(pretend: true)) - - rsc_pro_redux_install_generator.send(:invoke_generators) - end end context "when detecting existing bin-files on *nix" do @@ -3104,12 +2977,12 @@ class ActiveSupport::TestCase end end - context "when using --rsc-pro flag without Pro gem installed" do - let(:install_generator) { described_class.new([], { rsc_pro: true }) } - let(:expected_pro_version) { ReactOnRails::VERSION } + context "when using --rsc flag with a prerelease ReactOnRails version" do + let(:install_generator) { described_class.new([], { rsc: true }) } let(:fake_pid) { 12_345 } before do + stub_const("ReactOnRails::VERSION", "16.4.0.rc.5") allow(Gem).to receive(:loaded_specs).and_return({}) allow(install_generator).to receive(:gem_in_lockfile?).with("react_on_rails_pro").and_return(false) allow(Bundler).to receive(:with_unbundled_env).and_yield @@ -3118,28 +2991,14 @@ class ActiveSupport::TestCase .with(fake_pid).and_return(instance_double(Process::Status, success?: false)) end - specify "missing_pro_gem? uses rsc-pro flag context and exact bundle-add version" do - expect(install_generator.send(:missing_pro_gem?)).to be true - expect(Bundler).to have_received(:with_unbundled_env) - expect(Process).to have_received(:spawn) - .with("bundle add react_on_rails_pro --version='#{expected_pro_version}' --strict", - out: anything, - err: anything) - error_text = GeneratorMessages.messages.join("\n") - expect(error_text).to include("--rsc-pro") - expect(error_text).to include("gem 'react_on_rails_pro', '#{expected_pro_version}'") - expect(error_text).not_to include("~> #{expected_pro_version}") - end - - specify "missing_pro_gem? keeps prerelease suffix when rsc-pro exact pinning is used" do - stub_const("ReactOnRails::VERSION", "16.4.0.rc.5") - + specify "missing_pro_gem? uses exact version pin and surfaces a prerelease note" do expect(install_generator.send(:missing_pro_gem?)).to be true expect(Process).to have_received(:spawn) .with("bundle add react_on_rails_pro --version='16.4.0.rc.5' --strict", out: anything, err: anything) error_text = GeneratorMessages.messages.join("\n") + expect(error_text).to include("--rsc") expect(error_text).to include("gem 'react_on_rails_pro', '16.4.0.rc.5'") expect(error_text).to include("may not be published yet") expect(error_text).to include("path:") @@ -3232,7 +3091,7 @@ class ActiveSupport::TestCase end context "when force-checking Pro gem without pro-related flags" do - let(:install_generator) { described_class.new([], { pro: false, rsc: false, rsc_pro: false }) } + let(:install_generator) { described_class.new([], { pro: false, rsc: false }) } let(:fake_pid) { 12_345 } before do @@ -3293,26 +3152,6 @@ class ActiveSupport::TestCase end end - context "when --rsc-pro flag used on a dirty worktree without pro gem" do - let(:install_generator) { described_class.new([], { rsc_pro: true }) } - - before do - allow(ReactOnRails::GitUtils).to receive(:warn_if_uncommitted_changes).and_return(true) - allow(install_generator).to receive(:cli_exists?).with("git").and_return(true) - allow(install_generator).to receive_messages(missing_node?: false, missing_package_manager?: false) - allow(Gem).to receive(:loaded_specs).and_return({}) - allow(install_generator).to receive(:gem_in_lockfile?).with("react_on_rails_pro").and_return(false) - end - - specify "installation_prerequisites_met? returns false with clear error" do - expect(install_generator.send(:installation_prerequisites_met?)).to be false - error_text = GeneratorMessages.messages.join("\n") - expect(error_text).to include("react_on_rails_pro") - expect(error_text).to include("uncommitted changes") - expect(error_text).to include("--rsc-pro") - end - end - context "when --pro flag used on a dirty worktree with pro gem installed" do let(:install_generator) { described_class.new([], { pro: true }) }