diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 126c80b598..f5ac5f2b13 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -66,8 +66,10 @@ jobs: uses: actions/setup-node@v4 with: node-version: 20 - cache: yarn - cache-dependency-path: '**/yarn.lock' + # TODO: Re-enable yarn caching once Node.js V8 cache crash is fixed + # Tracking: https://github.com/actions/setup-node/issues/1028 + # cache: yarn + # cache-dependency-path: '**/yarn.lock' - name: Print system information run: | echo "Linux release: "; cat /etc/issue diff --git a/lib/generators/react_on_rails/base_generator.rb b/lib/generators/react_on_rails/base_generator.rb index 6f8746a08e..1205e32f1d 100644 --- a/lib/generators/react_on_rails/base_generator.rb +++ b/lib/generators/react_on_rails/base_generator.rb @@ -37,12 +37,16 @@ def copy_base_files app/views/layouts/hello_world.html.erb Procfile.dev Procfile.dev-static-assets - Procfile.dev-prod-assets] + Procfile.dev-prod-assets + bin/shakapacker-precompile-hook] base_templates = %w[config/initializers/react_on_rails.rb] base_files.each { |file| copy_file("#{base_path}#{file}", file) } base_templates.each do |file| template("#{base_path}/#{file}.tt", file) end + + # Make the hook script executable (copy_file guarantees it exists) + File.chmod(0o755, File.join(destination_root, "bin/shakapacker-precompile-hook")) end def copy_js_bundle_files diff --git a/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook b/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook new file mode 100755 index 0000000000..9e9632cc7a --- /dev/null +++ b/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook @@ -0,0 +1,30 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Shakapacker precompile hook for React on Rails +# +# This script runs before webpack compilation to generate pack files +# for auto-bundled components. It's called automatically by Shakapacker +# when configured in config/shakapacker.yml: +# precompile_hook: 'bin/shakapacker-precompile-hook' +# +# Emoji Scheme: +# 🔄 = Running/in-progress +# ✅ = Success +# ❌ = Error + +# Skip validation during precompile hook execution +# The hook runs early in the build process, potentially before full Rails initialization, +# and doesn't need package version validation since it's part of the build itself +ENV["REACT_ON_RAILS_SKIP_VALIDATION"] = "true" + +require_relative "../config/environment" + +begin + puts Rainbow("🔄 Running React on Rails precompile hook...").cyan + ReactOnRails::PacksGenerator.instance.generate_packs_if_stale +rescue StandardError => e + warn Rainbow("❌ Error in precompile hook: #{e.message}").red + warn e.backtrace.first(5).join("\n") + exit 1 +end diff --git a/lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml b/lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml index 8f76d350b2..7ce2699e68 100644 --- a/lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml +++ b/lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml @@ -42,6 +42,11 @@ default: &default # Raises an error if there is a mismatch in the shakapacker gem and npm package being used ensure_consistent_versioning: true + # Hook to run before webpack compilation (e.g., for generating dynamic entry points) + # SECURITY: Only reference trusted scripts within your project. Ensure the hook path + # points to a file within the project root that you control. + precompile_hook: 'bin/shakapacker-precompile-hook' + # Select whether the compiler will use SHA digest ('digest' option) or most recent modified timestamp ('mtime') to determine freshness compiler_strategy: digest diff --git a/lib/react_on_rails/dev/server_manager.rb b/lib/react_on_rails/dev/server_manager.rb index 29169e8578..64d0cdea17 100644 --- a/lib/react_on_rails/dev/server_manager.rb +++ b/lib/react_on_rails/dev/server_manager.rb @@ -248,7 +248,7 @@ def help_mode_details <<~MODES #{Rainbow('🔥 HMR Development mode (default)').cyan.bold} - #{Rainbow('Procfile.dev').green}: #{Rainbow('•').yellow} #{Rainbow('Hot Module Replacement (HMR) enabled').white} - #{Rainbow('•').yellow} #{Rainbow('React on Rails pack generation before Procfile start').white} + #{Rainbow('•').yellow} #{Rainbow('React on Rails pack generation (via precompile hook or bin/dev)').white} #{Rainbow('•').yellow} #{Rainbow('Webpack dev server for fast recompilation').white} #{Rainbow('•').yellow} #{Rainbow('Source maps for debugging').white} #{Rainbow('•').yellow} #{Rainbow('May have Flash of Unstyled Content (FOUC)').white} @@ -257,7 +257,7 @@ def help_mode_details #{Rainbow('📦 Static development mode').cyan.bold} - #{Rainbow('Procfile.dev-static-assets').green}: #{Rainbow('•').yellow} #{Rainbow('No HMR (static assets with auto-recompilation)').white} - #{Rainbow('•').yellow} #{Rainbow('React on Rails pack generation before Procfile start').white} + #{Rainbow('•').yellow} #{Rainbow('React on Rails pack generation (via precompile hook or bin/dev)').white} #{Rainbow('•').yellow} #{Rainbow('Webpack watch mode for auto-recompilation').white} #{Rainbow('•').yellow} #{Rainbow('CSS extracted to separate files (no FOUC)').white} #{Rainbow('•').yellow} #{Rainbow('Development environment (faster builds than production)').white} @@ -265,7 +265,7 @@ def help_mode_details #{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow('http://localhost:3000/').cyan.underline} #{Rainbow('🏭 Production-assets mode').cyan.bold} - #{Rainbow('Procfile.dev-prod-assets').green}: - #{Rainbow('•').yellow} #{Rainbow('React on Rails pack generation before Procfile start').white} + #{Rainbow('•').yellow} #{Rainbow('React on Rails pack generation (via precompile hook or assets:precompile)').white} #{Rainbow('•').yellow} #{Rainbow('Asset precompilation with NODE_ENV=production (webpack optimizations)').white} #{Rainbow('•').yellow} #{Rainbow('RAILS_ENV=development by default for assets:precompile (avoids credentials)').white} #{Rainbow('•').yellow} #{Rainbow('Use --rails-env=production for assets:precompile only (not server processes)').white} @@ -281,16 +281,20 @@ def help_mode_details def run_production_like(_verbose: false, route: nil, rails_env: nil) procfile = "Procfile.dev-prod-assets" + features = [ + "Precompiling assets with production optimizations", + "Running Rails server on port 3001", + "No HMR (Hot Module Replacement)", + "CSS extracted to separate files (no FOUC)" + ] + + # NOTE: Pack generation happens automatically during assets:precompile + # either via precompile hook or via the configuration.rb adjust_precompile_task + print_procfile_info(procfile, route: route) print_server_info( "🏭 Starting production-like development server...", - [ - "Generating React on Rails packs", - "Precompiling assets with production optimizations", - "Running Rails server on port 3001", - "No HMR (Hot Module Replacement)", - "CSS extracted to separate files (no FOUC)" - ], + features, 3001, route: route ) @@ -409,15 +413,22 @@ def run_production_like(_verbose: false, route: nil, rails_env: nil) def run_static_development(procfile, verbose: false, route: nil) print_procfile_info(procfile, route: route) + + features = [ + "Using shakapacker --watch (no HMR)", + "CSS extracted to separate files (no FOUC)", + "Development environment (source maps, faster builds)", + "Auto-recompiles on file changes" + ] + + # Add pack generation info if not using precompile hook + unless ReactOnRails::PackerUtils.shakapacker_precompile_hook_configured? + features.unshift("Generating React on Rails packs") + end + print_server_info( "⚡ Starting development server with static assets...", - [ - "Generating React on Rails packs", - "Using shakapacker --watch (no HMR)", - "CSS extracted to separate files (no FOUC)", - "Development environment (source maps, faster builds)", - "Auto-recompiles on file changes" - ], + features, route: route ) diff --git a/lib/react_on_rails/packer_utils.rb b/lib/react_on_rails/packer_utils.rb index e4a05ef761..c9af9510a2 100644 --- a/lib/react_on_rails/packer_utils.rb +++ b/lib/react_on_rails/packer_utils.rb @@ -197,8 +197,8 @@ def self.extract_precompile_hook config_data = ::Shakapacker.config.send(:data) # Try symbol keys first (Shakapacker's internal format), then fall back to string keys - # Note: Currently only one hook value is supported, but this will change to support lists - config_data&.dig(:hooks, :precompile) || config_data&.dig("hooks", "precompile") + # The key is 'precompile_hook' at the top level of the config + config_data&.[](:precompile_hook) || config_data&.[]("precompile_hook") end def self.hook_contains_generate_packs?(hook_value) diff --git a/spec/dummy/config/shakapacker.yml b/spec/dummy/config/shakapacker.yml index c9565dbb5f..3a9d457dfe 100644 --- a/spec/dummy/config/shakapacker.yml +++ b/spec/dummy/config/shakapacker.yml @@ -9,10 +9,6 @@ default: &default # SWC has issues with PropTypes handling javascript_transpiler: babel - # Hook to run before compilation (e.g., for ReScript builds, pack generation) - # See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md - precompile_hook: bin/shakapacker-precompile-hook - cache_path: tmp/cache/shakapacker webpack_compile_output: false ensure_consistent_versioning: true @@ -25,6 +21,11 @@ default: &default cache_manifest: false nested_entries: true + # Hook to run before webpack compilation (e.g., for generating dynamic entry points) + # SECURITY: Only reference trusted scripts within your project. Ensure the hook path + # points to a file within the project root that you control. + precompile_hook: 'bin/shakapacker-precompile-hook' + development: <<: *default # Turn this to true if you want to use the rails/shakapacker check that the test diff --git a/spec/react_on_rails/dev/pack_generator_spec.rb b/spec/react_on_rails/dev/pack_generator_spec.rb index 578df74991..19afb35df7 100644 --- a/spec/react_on_rails/dev/pack_generator_spec.rb +++ b/spec/react_on_rails/dev/pack_generator_spec.rb @@ -3,206 +3,198 @@ require_relative "../spec_helper" require "react_on_rails/dev/pack_generator" -# Tests for PackGenerator.generate which triggers pack generation for react_on_rails -# -# This class is responsible for: -# 1. Detecting if shakapacker has a precompile hook configured and skipping if so -# 2. Choosing the optimal execution strategy (direct Rake task vs bundle exec) -# 3. Handling errors appropriately and providing user-friendly output -# -# Test coverage includes: -# - Shakapacker hook detection (skip generation when hook is configured) -# - Direct Rake execution in Bundler context (faster, no subprocess overhead) -# - Fallback to bundle exec when Rails/Bundler not available -# - Error handling with proper stderr output -# - Silent mode output suppression RSpec.describe ReactOnRails::Dev::PackGenerator do describe ".generate" do - before do - # Mock the precompile hook check to return false by default - allow(ReactOnRails::PackerUtils).to receive(:shakapacker_precompile_hook_configured?).and_return(false) - end - context "when shakapacker precompile hook is configured" do before do - allow(ReactOnRails::PackerUtils).to receive_messages(shakapacker_precompile_hook_configured?: true, - shakapacker_precompile_hook_value: "bin/precompile_hook") + allow(ReactOnRails::PackerUtils).to receive(:shakapacker_precompile_hook_configured?).and_return(true) end - it "skips pack generation in verbose mode and shows hook value" do + it "skips pack generation in verbose mode" do expect { described_class.generate(verbose: true) } - .to output(%r{⏭️ Skipping pack generation \(handled by shakapacker precompile hook: bin/precompile_hook\)}) - .to_stdout_from_any_process + .to output(/⏭️ Skipping pack generation/).to_stdout_from_any_process end - it "skips pack generation in quiet mode" do + it "skips pack generation silently in quiet mode" do expect { described_class.generate(verbose: false) } .not_to output.to_stdout_from_any_process end + + it "does not invoke the rake task" do + # Mock the task to ensure it's not called + mock_task = instance_double(Rake::Task) + allow(Rake::Task).to receive(:[]).with("react_on_rails:generate_packs").and_return(mock_task) + allow(mock_task).to receive(:invoke) + + described_class.generate(verbose: false) + + expect(mock_task).not_to have_received(:invoke) + end end - context "when shakapacker precompile hook is not configured" do - context "when in Bundler context with Rails available" do - let(:mock_task) { instance_double(Rake::Task) } - let(:mock_rails_app) do - # rubocop:disable RSpec/VerifiedDoubles - double("Rails.application").tap do |app| - allow(app).to receive(:load_tasks) - allow(app).to receive(:respond_to?).with(:load_tasks).and_return(true) - end - # rubocop:enable RSpec/VerifiedDoubles + context "when in Bundler context with Rails available" do + let(:mock_task) { instance_double(Rake::Task) } + let(:mock_rails_app) do + # rubocop:disable RSpec/VerifiedDoubles + double("Rails.application").tap do |app| + allow(app).to receive(:load_tasks) + allow(app).to receive(:respond_to?).with(:load_tasks).and_return(true) end + # rubocop:enable RSpec/VerifiedDoubles + end - before do - # Setup Bundler context - stub_const("Bundler", Module.new) - allow(ENV).to receive(:[]).and_call_original - allow(ENV).to receive(:[]).with("BUNDLE_GEMFILE").and_return("/path/to/Gemfile") - - # Setup Rails availability - app = mock_rails_app - rails_module = Module.new do - define_singleton_method(:application) { app } - define_singleton_method(:respond_to?) { |method, *| method == :application } - end - stub_const("Rails", rails_module) - - # Mock Rake::Task at the boundary - allow(Rake::Task).to receive(:task_defined?).with("react_on_rails:generate_packs").and_return(false) - allow(Rake::Task).to receive(:[]).with("react_on_rails:generate_packs").and_return(mock_task) - allow(mock_task).to receive(:reenable) - allow(mock_task).to receive(:invoke) + before do + # Ensure precompile hook is not configured for these tests + allow(ReactOnRails::PackerUtils).to receive(:shakapacker_precompile_hook_configured?).and_return(false) + + # Setup Bundler context + stub_const("Bundler", Module.new) + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:[]).with("BUNDLE_GEMFILE").and_return("/path/to/Gemfile") + + # Setup Rails availability + app = mock_rails_app + rails_module = Module.new do + define_singleton_method(:application) { app } + define_singleton_method(:respond_to?) { |method, *| method == :application } end + stub_const("Rails", rails_module) - it "runs pack generation successfully in verbose mode using direct rake execution" do - expect { described_class.generate(verbose: true) } - .to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process + # Mock Rake::Task at the boundary + allow(Rake::Task).to receive(:task_defined?).with("react_on_rails:generate_packs").and_return(false) + allow(Rake::Task).to receive(:[]).with("react_on_rails:generate_packs").and_return(mock_task) + allow(mock_task).to receive(:reenable) + allow(mock_task).to receive(:invoke) + end - expect(mock_task).to have_received(:invoke) - expect(mock_rails_app).to have_received(:load_tasks) - end + it "runs pack generation successfully in verbose mode using direct rake execution" do + expect { described_class.generate(verbose: true) } + .to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process - it "runs pack generation successfully in quiet mode using direct rake execution" do - expect { described_class.generate(verbose: false) } - .to output(/📦 Generating packs\.\.\. ✅/).to_stdout_from_any_process + expect(mock_task).to have_received(:invoke) + expect(mock_rails_app).to have_received(:load_tasks) + end - expect(mock_task).to have_received(:invoke) - end + it "runs pack generation successfully in quiet mode using direct rake execution" do + expect { described_class.generate(verbose: false) } + .to output(/📦 Generating packs\.\.\. ✅/).to_stdout_from_any_process - it "exits with error when pack generation fails" do - allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Task failed")) + expect(mock_task).to have_received(:invoke) + end - # Mock STDERR.puts to capture output - error_output = [] - # rubocop:disable Style/GlobalStdStream - allow(STDERR).to receive(:puts) { |msg| error_output << msg } - # rubocop:enable Style/GlobalStdStream + it "exits with error when pack generation fails" do + allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Task failed")) - expect { described_class.generate(verbose: false) }.to raise_error(SystemExit) - expect(error_output.join("\n")).to match(/Error generating packs: Task failed/) - end + # Mock STDERR.puts to capture output + error_output = [] + # rubocop:disable Style/GlobalStdStream + allow(STDERR).to receive(:puts) { |msg| error_output << msg } + # rubocop:enable Style/GlobalStdStream - it "outputs errors to stderr even in silent mode" do - allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Silent mode error")) + expect { described_class.generate(verbose: false) }.to raise_error(SystemExit) + expect(error_output.join("\n")).to match(/Error generating packs: Task failed/) + end - # Mock STDERR.puts to capture output - error_output = [] - # rubocop:disable Style/GlobalStdStream - allow(STDERR).to receive(:puts) { |msg| error_output << msg } - # rubocop:enable Style/GlobalStdStream + it "outputs errors to stderr even in silent mode" do + allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Silent mode error")) - expect { described_class.generate(verbose: false) }.to raise_error(SystemExit) - expect(error_output.join("\n")).to match(/Error generating packs: Silent mode error/) - end + # Mock STDERR.puts to capture output + error_output = [] + # rubocop:disable Style/GlobalStdStream + allow(STDERR).to receive(:puts) { |msg| error_output << msg } + # rubocop:enable Style/GlobalStdStream - it "includes backtrace in error output when DEBUG env is set" do - allow(ENV).to receive(:[]).with("DEBUG").and_return("true") - allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Debug error")) + expect { described_class.generate(verbose: false) }.to raise_error(SystemExit) + expect(error_output.join("\n")).to match(/Error generating packs: Silent mode error/) + end - # Mock STDERR.puts to capture output - error_output = [] - # rubocop:disable Style/GlobalStdStream - allow(STDERR).to receive(:puts) { |msg| error_output << msg } - # rubocop:enable Style/GlobalStdStream + it "includes backtrace in error output when DEBUG env is set" do + allow(ENV).to receive(:[]).with("DEBUG").and_return("true") + allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Debug error")) - expect { described_class.generate(verbose: false) }.to raise_error(SystemExit) - expect(error_output.join("\n")).to match(/Error generating packs: Debug error.*pack_generator_spec\.rb/m) - end + # Mock STDERR.puts to capture output + error_output = [] + # rubocop:disable Style/GlobalStdStream + allow(STDERR).to receive(:puts) { |msg| error_output << msg } + # rubocop:enable Style/GlobalStdStream - it "suppresses stdout in silent mode" do - # Mock task to produce output - allow(mock_task).to receive(:invoke) do - puts "This should be suppressed" - end + expect { described_class.generate(verbose: false) }.to raise_error(SystemExit) + expect(error_output.join("\n")).to match(/Error generating packs: Debug error.*pack_generator_spec\.rb/m) + end - expect { described_class.generate(verbose: false) } - .not_to output(/This should be suppressed/).to_stdout_from_any_process + it "suppresses stdout in silent mode" do + # Mock task to produce output + allow(mock_task).to receive(:invoke) do + puts "This should be suppressed" end + + expect { described_class.generate(verbose: false) } + .not_to output(/This should be suppressed/).to_stdout_from_any_process end + end - context "when not in Bundler context" do - before do - # Ensure we're not in Bundler context - hide_const("Bundler") if defined?(Bundler) - end + context "when not in Bundler context" do + before do + # Ensure we're not in Bundler context + hide_const("Bundler") if defined?(Bundler) + end - it "runs pack generation successfully in verbose mode using bundle exec" do - allow(described_class).to receive(:system) - .with("bundle", "exec", "rake", "react_on_rails:generate_packs") - .and_return(true) + it "runs pack generation successfully in verbose mode using bundle exec" do + allow(described_class).to receive(:system) + .with("bundle", "exec", "rake", "react_on_rails:generate_packs") + .and_return(true) - expect { described_class.generate(verbose: true) } - .to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process + expect { described_class.generate(verbose: true) } + .to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process - expect(described_class).to have_received(:system) - .with("bundle", "exec", "rake", "react_on_rails:generate_packs") - end + expect(described_class).to have_received(:system) + .with("bundle", "exec", "rake", "react_on_rails:generate_packs") + end - it "runs pack generation successfully in quiet mode using bundle exec" do - allow(described_class).to receive(:system) - .with("bundle", "exec", "rake", "react_on_rails:generate_packs", - out: File::NULL, err: File::NULL) - .and_return(true) + it "runs pack generation successfully in quiet mode using bundle exec" do + allow(described_class).to receive(:system) + .with("bundle", "exec", "rake", "react_on_rails:generate_packs", + out: File::NULL, err: File::NULL) + .and_return(true) - expect { described_class.generate(verbose: false) } - .to output(/📦 Generating packs\.\.\. ✅/).to_stdout_from_any_process + expect { described_class.generate(verbose: false) } + .to output(/📦 Generating packs\.\.\. ✅/).to_stdout_from_any_process - expect(described_class).to have_received(:system) - .with("bundle", "exec", "rake", "react_on_rails:generate_packs", - out: File::NULL, err: File::NULL) - end + expect(described_class).to have_received(:system) + .with("bundle", "exec", "rake", "react_on_rails:generate_packs", + out: File::NULL, err: File::NULL) + end - it "exits with error when pack generation fails" do - allow(described_class).to receive(:system) - .with("bundle", "exec", "rake", "react_on_rails:generate_packs", - out: File::NULL, err: File::NULL) - .and_return(false) + it "exits with error when pack generation fails" do + allow(described_class).to receive(:system) + .with("bundle", "exec", "rake", "react_on_rails:generate_packs", + out: File::NULL, err: File::NULL) + .and_return(false) - expect { described_class.generate(verbose: false) }.to raise_error(SystemExit) - end + expect { described_class.generate(verbose: false) }.to raise_error(SystemExit) end + end - context "when Rails is not available" do - before do - stub_const("Bundler", Module.new) - allow(ENV).to receive(:[]).and_call_original - allow(ENV).to receive(:[]).with("BUNDLE_GEMFILE").and_return("/path/to/Gemfile") + context "when Rails is not available" do + before do + stub_const("Bundler", Module.new) + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:[]).with("BUNDLE_GEMFILE").and_return("/path/to/Gemfile") - # Rails not available - hide_const("Rails") if defined?(Rails) - end + # Rails not available + hide_const("Rails") if defined?(Rails) + end - it "falls back to bundle exec when Rails is not defined" do - allow(described_class).to receive(:system) - .with("bundle", "exec", "rake", "react_on_rails:generate_packs") - .and_return(true) + it "falls back to bundle exec when Rails is not defined" do + allow(described_class).to receive(:system) + .with("bundle", "exec", "rake", "react_on_rails:generate_packs") + .and_return(true) - expect { described_class.generate(verbose: true) } - .to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process + expect { described_class.generate(verbose: true) } + .to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process - expect(described_class).to have_received(:system) - .with("bundle", "exec", "rake", "react_on_rails:generate_packs") - end + expect(described_class).to have_received(:system) + .with("bundle", "exec", "rake", "react_on_rails:generate_packs") end end end diff --git a/spec/react_on_rails/packer_utils_spec.rb b/spec/react_on_rails/packer_utils_spec.rb index 3625082124..5c9ac4a795 100644 --- a/spec/react_on_rails/packer_utils_spec.rb +++ b/spec/react_on_rails/packer_utils_spec.rb @@ -2,7 +2,6 @@ require_relative "spec_helper" -# rubocop:disable Metrics/ModuleLength module ReactOnRails describe PackerUtils do describe ".shakapacker_version_requirement_met?" do @@ -148,137 +147,48 @@ module ReactOnRails allow(::Shakapacker).to receive(:config).and_return(mock_config) end - context "when shakapacker is not defined" do - before do - hide_const("::Shakapacker") + context "when precompile_hook is configured" do + it "returns true when hook command contains generate_packs rake task" do + hook_value = "bundle exec rake react_on_rails:generate_packs" + allow(mock_config).to receive(:send).with(:data) + .and_return({ precompile_hook: hook_value }) + expect(described_class.shakapacker_precompile_hook_configured?).to be true end - it "returns false" do - expect(described_class.shakapacker_precompile_hook_configured?).to be(false) + it "returns false when hook command doesn't contain generate_packs" do + allow(mock_config).to receive(:send).with(:data) + .and_return({ precompile_hook: "bin/some-other-command" }) + expect(described_class.shakapacker_precompile_hook_configured?).to be false end end - context "when precompile hook contains react_on_rails:generate_packs" do - it "returns true for direct command with symbol keys" do - allow(mock_config).to receive(:send).with(:data).and_return( - { hooks: { precompile: "bundle exec rake react_on_rails:generate_packs" } } - ) - - expect(described_class.shakapacker_precompile_hook_configured?).to be(true) - end - - it "returns true for direct command with string keys" do - allow(mock_config).to receive(:send).with(:data).and_return( - { "hooks" => { "precompile" => "bundle exec rake react_on_rails:generate_packs" } } - ) - - expect(described_class.shakapacker_precompile_hook_configured?).to be(true) - end - - it "returns true when hook points to script file containing the task" do - allow(mock_config).to receive(:send).with(:data).and_return( - { hooks: { precompile: "bin/shakapacker_precompile" } } - ) - - # Mock Rails.root - rails_root = instance_double(Pathname) - allow(Rails).to receive(:root).and_return(rails_root) - - script_path = instance_double(Pathname, file?: true) - allow(rails_root).to receive(:join).with("bin/shakapacker_precompile").and_return(script_path) - allow(File).to receive(:exist?).with(script_path).and_return(true) - allow(File).to receive(:read).with(script_path) - .and_return("#!/bin/bash\nbundle exec rake react_on_rails:generate_packs\n") - - expect(described_class.shakapacker_precompile_hook_configured?).to be(true) + context "when precompile_hook is not configured" do + it "returns false for nil" do + allow(mock_config).to receive(:send).with(:data).and_return({ precompile_hook: nil }) + expect(described_class.shakapacker_precompile_hook_configured?).to be false end - it "returns false when hook points to script file without the task" do - allow(mock_config).to receive(:send).with(:data).and_return( - { hooks: { precompile: "bin/other_script" } } - ) - - # Mock Rails.root - rails_root = instance_double(Pathname) - allow(Rails).to receive(:root).and_return(rails_root) - - script_path = instance_double(Pathname, file?: true) - allow(rails_root).to receive(:join).with("bin/other_script").and_return(script_path) - allow(File).to receive(:exist?).with(script_path).and_return(true) - allow(File).to receive(:read).with(script_path) - .and_return("#!/bin/bash\necho 'doing other stuff'\n") - - expect(described_class.shakapacker_precompile_hook_configured?).to be(false) + it "returns false for empty string" do + allow(mock_config).to receive(:send).with(:data).and_return({ precompile_hook: "" }) + expect(described_class.shakapacker_precompile_hook_configured?).to be false end end - context "when precompile hook does not contain react_on_rails:generate_packs" do - it "returns false for different hook" do - allow(mock_config).to receive(:send).with(:data).and_return( - { hooks: { precompile: "bundle exec rake some_other_task" } } - ) - - expect(described_class.shakapacker_precompile_hook_configured?).to be(false) - end + context "when Shakapacker is not available" do + before { hide_const("::Shakapacker") } - it "returns false for similar but different task name" do - allow(mock_config).to receive(:send).with(:data).and_return( - { hooks: { precompile: "bundle exec rake react_on_rails:generate_packs_extra" } } - ) - - expect(described_class.shakapacker_precompile_hook_configured?).to be(false) - end - - it "returns false when hooks is nil" do - allow(mock_config).to receive(:send).with(:data).and_return({}) - - expect(described_class.shakapacker_precompile_hook_configured?).to be(false) - end - - it "returns false when precompile hook is nil" do - allow(mock_config).to receive(:send).with(:data).and_return({ hooks: {} }) - - expect(described_class.shakapacker_precompile_hook_configured?).to be(false) + it "returns false" do + expect(described_class.shakapacker_precompile_hook_configured?).to be false end end - context "when an error occurs" do + context "when config.send raises an error" do it "returns false" do - allow(mock_config).to receive(:send).with(:data).and_raise(StandardError.new("test error")) - - expect(described_class.shakapacker_precompile_hook_configured?).to be(false) + allow(mock_config).to receive(:send).and_raise(NoMethodError) + expect(described_class.shakapacker_precompile_hook_configured?).to be false end end end - - describe ".shakapacker_precompile_hook_value" do - let(:mock_config) { instance_double("::Shakapacker::Config") } # rubocop:disable RSpec/VerifiedDoubleReference - - before do - allow(::Shakapacker).to receive(:config).and_return(mock_config) - end - - it "returns the hook value when configured" do - hook_value = "bin/shakapacker_precompile" - allow(mock_config).to receive(:send).with(:data).and_return( - { hooks: { precompile: hook_value } } - ) - - expect(described_class.shakapacker_precompile_hook_value).to eq(hook_value) - end - - it "returns nil when no hook is configured" do - allow(mock_config).to receive(:send).with(:data).and_return({}) - - expect(described_class.shakapacker_precompile_hook_value).to be_nil - end - - it "returns nil when an error occurs" do - allow(mock_config).to receive(:send).with(:data).and_raise(StandardError.new("test error")) - - expect(described_class.shakapacker_precompile_hook_value).to be_nil - end - end end describe "version constants validation" do @@ -312,4 +222,3 @@ module ReactOnRails end end end -# rubocop:enable Metrics/ModuleLength