diff --git a/lib/react_on_rails/configuration.rb b/lib/react_on_rails/configuration.rb index 3ef7332d2..552cb8b38 100644 --- a/lib/react_on_rails/configuration.rb +++ b/lib/react_on_rails/configuration.rb @@ -247,7 +247,11 @@ def adjust_precompile_task raise(ReactOnRails::Error, compile_command_conflict_message) if ReactOnRails::PackerUtils.precompile? precompile_tasks = lambda { - Rake::Task["react_on_rails:generate_packs"].invoke + # Skip generate_packs if shakapacker has a precompile hook configured + unless ReactOnRails::PackerUtils.shakapacker_precompile_hook_configured? + Rake::Task["react_on_rails:generate_packs"].invoke + end + Rake::Task["react_on_rails:assets:webpack"].invoke # VERSIONS is per the shakacode/shakapacker clean method definition. diff --git a/lib/react_on_rails/dev/pack_generator.rb b/lib/react_on_rails/dev/pack_generator.rb index 9e74bf063..aec873fe2 100644 --- a/lib/react_on_rails/dev/pack_generator.rb +++ b/lib/react_on_rails/dev/pack_generator.rb @@ -7,6 +7,12 @@ module Dev class PackGenerator class << self def generate(verbose: false) + # Skip if shakapacker has a precompile hook configured + if ReactOnRails::PackerUtils.shakapacker_precompile_hook_configured? + puts "⏭️ Skipping pack generation (handled by shakapacker precompile hook)" if verbose + return + end + if verbose puts "📦 Generating React on Rails packs..." success = system "bundle exec rake react_on_rails:generate_packs" diff --git a/lib/react_on_rails/packer_utils.rb b/lib/react_on_rails/packer_utils.rb index d1349466f..bebbbe451 100644 --- a/lib/react_on_rails/packer_utils.rb +++ b/lib/react_on_rails/packer_utils.rb @@ -162,5 +162,21 @@ def self.raise_shakapacker_version_incompatible_for_basic_pack_generation raise ReactOnRails::Error, msg end + + # Check if shakapacker.yml has a precompile hook configured + # This prevents react_on_rails from running generate_packs twice + def self.shakapacker_precompile_hook_configured? + return false unless defined?(::Shakapacker) + + config_data = ::Shakapacker.config.send(:data) + hooks = config_data.dig("hooks", "precompile") + + return false unless hooks + + # Check if any hook contains the generate_packs rake task + Array(hooks).any? { |hook| hook.to_s.include?("react_on_rails:generate_packs") } + rescue StandardError + false + end end end diff --git a/spec/react_on_rails/dev/pack_generator_spec.rb b/spec/react_on_rails/dev/pack_generator_spec.rb index a1bb42a20..44a168d6d 100644 --- a/spec/react_on_rails/dev/pack_generator_spec.rb +++ b/spec/react_on_rails/dev/pack_generator_spec.rb @@ -5,27 +5,50 @@ RSpec.describe ReactOnRails::Dev::PackGenerator do describe ".generate" do - it "runs pack generation successfully in verbose mode" do - command = "bundle exec rake react_on_rails:generate_packs" - allow(described_class).to receive(:system).with(command).and_return(true) - - expect { described_class.generate(verbose: true) } - .to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process + before do + allow(ReactOnRails::PackerUtils).to receive(:shakapacker_precompile_hook_configured?).and_return(false) end - it "runs pack generation successfully in quiet mode" do - command = "bundle exec rake react_on_rails:generate_packs > /dev/null 2>&1" - allow(described_class).to receive(:system).with(command).and_return(true) + context "when shakapacker precompile hook is configured" do + before do + allow(ReactOnRails::PackerUtils).to receive(:shakapacker_precompile_hook_configured?).and_return(true) + end + + it "skips pack generation in verbose mode" do + expect { described_class.generate(verbose: true) } + .to output(/⏭️ Skipping pack generation \(handled by shakapacker precompile hook\)/) + .to_stdout_from_any_process + end - expect { described_class.generate(verbose: false) } - .to output(/📦 Generating packs\.\.\. ✅/).to_stdout_from_any_process + it "skips pack generation in quiet mode" do + expect { described_class.generate(verbose: false) } + .not_to output.to_stdout_from_any_process + end end - it "exits with error when pack generation fails" do - command = "bundle exec rake react_on_rails:generate_packs > /dev/null 2>&1" - allow(described_class).to receive(:system).with(command).and_return(false) + context "when shakapacker precompile hook is not configured" do + it "runs pack generation successfully in verbose mode" do + command = "bundle exec rake react_on_rails:generate_packs" + allow(described_class).to receive(:system).with(command).and_return(true) + + expect { described_class.generate(verbose: true) } + .to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process + end + + it "runs pack generation successfully in quiet mode" do + command = "bundle exec rake react_on_rails:generate_packs > /dev/null 2>&1" + allow(described_class).to receive(:system).with(command).and_return(true) + + expect { described_class.generate(verbose: false) } + .to output(/📦 Generating packs\.\.\. ✅/).to_stdout_from_any_process + end + + it "exits with error when pack generation fails" do + command = "bundle exec rake react_on_rails:generate_packs > /dev/null 2>&1" + allow(described_class).to receive(:system).with(command).and_return(false) - expect { described_class.generate(verbose: false) }.to raise_error(SystemExit) + expect { described_class.generate(verbose: false) }.to raise_error(SystemExit) + end end end end diff --git a/spec/react_on_rails/packer_utils_spec.rb b/spec/react_on_rails/packer_utils_spec.rb index 6636eaa65..556ce65c2 100644 --- a/spec/react_on_rails/packer_utils_spec.rb +++ b/spec/react_on_rails/packer_utils_spec.rb @@ -139,6 +139,72 @@ module ReactOnRails expect(described_class.supports_autobundling?).to be(false) end end + + describe ".shakapacker_precompile_hook_configured?" do + let(:mock_config) { instance_double("::Shakapacker::Config") } # rubocop:disable RSpec/VerifiedDoubleReference + + before do + allow(::Shakapacker).to receive(:config).and_return(mock_config) + end + + context "when shakapacker is not defined" do + before do + hide_const("::Shakapacker") + end + + it "returns false" do + 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 single hook" 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 hook in array" do + allow(mock_config).to receive(:send).with(:data).and_return( + { "hooks" => { "precompile" => ["bundle exec rake react_on_rails:generate_packs", "echo done"] } } + ) + + expect(described_class.shakapacker_precompile_hook_configured?).to be(true) + 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 + + 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) + end + end + + context "when an error occurs" 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) + end + end + end end describe "version constants validation" do