Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion lib/react_on_rails/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,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.
Expand Down
6 changes: 6 additions & 0 deletions lib/react_on_rails/dev/pack_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,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 = run_pack_generation
Expand Down
21 changes: 21 additions & 0 deletions lib/react_on_rails/packer_utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -166,5 +166,26 @@ 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)

# Access config data using private :data method since there's no public API
# to access the raw configuration hash needed for hook detection
config_data = ::Shakapacker.config.send(:data)

# Try symbol keys first (Shakapacker's internal format), then fall back to string keys
hooks = config_data&.dig(:hooks, :precompile) || config_data&.dig("hooks", "precompile")

return false if hooks.nil?

# Check if any hook contains the generate_packs rake task using word boundary
# to avoid false positives from comments or similar strings
Array(hooks).any? { |hook| hook.to_s.match?(/\breact_on_rails:generate_packs\b/) }
rescue StandardError
false
end
end
end
270 changes: 147 additions & 123 deletions spec/react_on_rails/dev/pack_generator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,166 +5,190 @@

RSpec.describe ReactOnRails::Dev::PackGenerator do
describe ".generate" 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
end
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
# 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)
allow(ReactOnRails::PackerUtils).to receive(:shakapacker_precompile_hook_configured?).and_return(true)
end

it "runs pack generation successfully in verbose mode using direct rake execution" do
it "skips pack generation in verbose mode" do
expect { described_class.generate(verbose: true) }
.to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process

expect(mock_task).to have_received(:invoke)
expect(mock_rails_app).to have_received(:load_tasks)
.to output(/⏭️ Skipping pack generation \(handled by shakapacker precompile hook\)/)
.to_stdout_from_any_process
end

it "runs pack generation successfully in quiet mode using direct rake execution" do
it "skips pack generation in quiet mode" do
expect { described_class.generate(verbose: false) }
.to output(/📦 Generating packs\.\.\. ✅/).to_stdout_from_any_process

expect(mock_task).to have_received(:invoke)
.not_to output.to_stdout_from_any_process
end
end

it "exits with error when pack generation fails" do
allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Task failed"))
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
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
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)
end

expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
expect(error_output.join("\n")).to match(/Error generating packs: Task failed/)
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 "outputs errors to stderr even in silent mode" do
allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Silent mode error"))
expect(mock_task).to have_received(:invoke)
expect(mock_rails_app).to have_received(:load_tasks)
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 "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 { described_class.generate(verbose: false) }.to raise_error(SystemExit)
expect(error_output.join("\n")).to match(/Error generating packs: Silent mode error/)
end
expect(mock_task).to have_received(:invoke)
end

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"))
it "exits with error when pack generation fails" do
allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Task failed"))

# 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
# 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

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) }.to raise_error(SystemExit)
expect(error_output.join("\n")).to match(/Error generating packs: Task failed/)
end

it "suppresses stdout in silent mode" do
# Mock task to produce output
allow(mock_task).to receive(:invoke) do
puts "This should be suppressed"
it "outputs errors to stderr even in silent mode" do
allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Silent mode error"))

# 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

expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
expect(error_output.join("\n")).to match(/Error generating packs: Silent mode error/)
end

expect { described_class.generate(verbose: false) }
.not_to output(/This should be suppressed/).to_stdout_from_any_process
end
end
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"))

context "when not in Bundler context" do
before do
# Ensure we're not in Bundler context
hide_const("Bundler") if defined?(Bundler)
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 "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: 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: true) }
.to output(/📦 Generating React on Rails packs.../).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).to have_received(:system)
.with("bundle", "exec", "rake", "react_on_rails:generate_packs")
expect { described_class.generate(verbose: false) }
.not_to output(/This should be suppressed/).to_stdout_from_any_process
end
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)
context "when not in Bundler context" do
before do
# Ensure we're not in Bundler context
hide_const("Bundler") if defined?(Bundler)
end

expect { described_class.generate(verbose: false) }
.to output(/📦 Generating packs\.\.\. ✅/).to_stdout_from_any_process
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).to have_received(:system)
.with("bundle", "exec", "rake", "react_on_rails:generate_packs",
out: File::NULL, err: File::NULL)
end
expect { described_class.generate(verbose: true) }
.to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process

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).to have_received(:system)
.with("bundle", "exec", "rake", "react_on_rails:generate_packs")
end

expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
end
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)

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")
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

# Rails not available
hide_const("Rails") if defined?(Rails)
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
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)
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")

expect { described_class.generate(verbose: true) }
.to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process
# 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)

expect(described_class).to have_received(:system)
.with("bundle", "exec", "rake", "react_on_rails:generate_packs")
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
end
end
end
Expand Down
Loading
Loading