Skip to content

Commit 5f35dc5

Browse files
justin808claude
andcommitted
Skip generate_packs when shakapacker precompile hook configured
This change prevents react_on_rails from running generate_packs twice when shakapacker has a precompile hook configured that already runs it. Changes: - Add PackerUtils.shakapacker_precompile_hook_configured? to detect hook - Skip generate_packs in assets:precompile if hook is configured - Skip generate_packs in bin/dev if hook is configured - Add comprehensive test coverage for new behavior This ensures generate_packs runs only once during both: - Production asset precompilation (assets:precompile) - Development server startup (bin/dev) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent ca6366e commit 5f35dc5

File tree

5 files changed

+240
-124
lines changed

5 files changed

+240
-124
lines changed

lib/react_on_rails/configuration.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,11 @@ def adjust_precompile_task
238238
raise(ReactOnRails::Error, compile_command_conflict_message) if ReactOnRails::PackerUtils.precompile?
239239

240240
precompile_tasks = lambda {
241-
Rake::Task["react_on_rails:generate_packs"].invoke
241+
# Skip generate_packs if shakapacker has a precompile hook configured
242+
unless ReactOnRails::PackerUtils.shakapacker_precompile_hook_configured?
243+
Rake::Task["react_on_rails:generate_packs"].invoke
244+
end
245+
242246
Rake::Task["react_on_rails:assets:webpack"].invoke
243247

244248
# VERSIONS is per the shakacode/shakapacker clean method definition.

lib/react_on_rails/dev/pack_generator.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ module Dev
88
class PackGenerator
99
class << self
1010
def generate(verbose: false)
11+
# Skip if shakapacker has a precompile hook configured
12+
if ReactOnRails::PackerUtils.shakapacker_precompile_hook_configured?
13+
puts "⏭️ Skipping pack generation (handled by shakapacker precompile hook)" if verbose
14+
return
15+
end
16+
1117
if verbose
1218
puts "📦 Generating React on Rails packs..."
1319
success = run_pack_generation

lib/react_on_rails/packer_utils.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,5 +166,21 @@ def self.raise_shakapacker_version_incompatible_for_basic_pack_generation
166166

167167
raise ReactOnRails::Error, msg
168168
end
169+
170+
# Check if shakapacker.yml has a precompile hook configured
171+
# This prevents react_on_rails from running generate_packs twice
172+
def self.shakapacker_precompile_hook_configured?
173+
return false unless defined?(::Shakapacker)
174+
175+
config_data = ::Shakapacker.config.send(:data)
176+
hooks = config_data.dig("hooks", "precompile")
177+
178+
return false unless hooks
179+
180+
# Check if any hook contains the generate_packs rake task
181+
Array(hooks).any? { |hook| hook.to_s.include?("react_on_rails:generate_packs") }
182+
rescue StandardError
183+
false
184+
end
169185
end
170186
end

spec/react_on_rails/dev/pack_generator_spec.rb

Lines changed: 147 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -5,166 +5,190 @@
55

66
RSpec.describe ReactOnRails::Dev::PackGenerator do
77
describe ".generate" do
8-
context "when in Bundler context with Rails available" do
9-
let(:mock_task) { instance_double(Rake::Task) }
10-
let(:mock_rails_app) do
11-
# rubocop:disable RSpec/VerifiedDoubles
12-
double("Rails.application").tap do |app|
13-
allow(app).to receive(:load_tasks)
14-
allow(app).to receive(:respond_to?).with(:load_tasks).and_return(true)
15-
end
16-
# rubocop:enable RSpec/VerifiedDoubles
17-
end
8+
before do
9+
# Mock the precompile hook check to return false by default
10+
allow(ReactOnRails::PackerUtils).to receive(:shakapacker_precompile_hook_configured?).and_return(false)
11+
end
1812

13+
context "when shakapacker precompile hook is configured" do
1914
before do
20-
# Setup Bundler context
21-
stub_const("Bundler", Module.new)
22-
allow(ENV).to receive(:[]).and_call_original
23-
allow(ENV).to receive(:[]).with("BUNDLE_GEMFILE").and_return("/path/to/Gemfile")
24-
25-
# Setup Rails availability
26-
app = mock_rails_app
27-
rails_module = Module.new do
28-
define_singleton_method(:application) { app }
29-
define_singleton_method(:respond_to?) { |method, *| method == :application }
30-
end
31-
stub_const("Rails", rails_module)
32-
33-
# Mock Rake::Task at the boundary
34-
allow(Rake::Task).to receive(:task_defined?).with("react_on_rails:generate_packs").and_return(false)
35-
allow(Rake::Task).to receive(:[]).with("react_on_rails:generate_packs").and_return(mock_task)
36-
allow(mock_task).to receive(:reenable)
37-
allow(mock_task).to receive(:invoke)
15+
allow(ReactOnRails::PackerUtils).to receive(:shakapacker_precompile_hook_configured?).and_return(true)
3816
end
3917

40-
it "runs pack generation successfully in verbose mode using direct rake execution" do
18+
it "skips pack generation in verbose mode" do
4119
expect { described_class.generate(verbose: true) }
42-
.to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process
43-
44-
expect(mock_task).to have_received(:invoke)
45-
expect(mock_rails_app).to have_received(:load_tasks)
20+
.to output(/⏭️ Skipping pack generation \(handled by shakapacker precompile hook\)/)
21+
.to_stdout_from_any_process
4622
end
4723

48-
it "runs pack generation successfully in quiet mode using direct rake execution" do
24+
it "skips pack generation in quiet mode" do
4925
expect { described_class.generate(verbose: false) }
50-
.to output(/📦 Generating packs\.\.\. ✅/).to_stdout_from_any_process
51-
52-
expect(mock_task).to have_received(:invoke)
26+
.not_to output.to_stdout_from_any_process
5327
end
28+
end
5429

55-
it "exits with error when pack generation fails" do
56-
allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Task failed"))
30+
context "when shakapacker precompile hook is not configured" do
31+
context "when in Bundler context with Rails available" do
32+
let(:mock_task) { instance_double(Rake::Task) }
33+
let(:mock_rails_app) do
34+
# rubocop:disable RSpec/VerifiedDoubles
35+
double("Rails.application").tap do |app|
36+
allow(app).to receive(:load_tasks)
37+
allow(app).to receive(:respond_to?).with(:load_tasks).and_return(true)
38+
end
39+
# rubocop:enable RSpec/VerifiedDoubles
40+
end
5741

58-
# Mock STDERR.puts to capture output
59-
error_output = []
60-
# rubocop:disable Style/GlobalStdStream
61-
allow(STDERR).to receive(:puts) { |msg| error_output << msg }
62-
# rubocop:enable Style/GlobalStdStream
42+
before do
43+
# Setup Bundler context
44+
stub_const("Bundler", Module.new)
45+
allow(ENV).to receive(:[]).and_call_original
46+
allow(ENV).to receive(:[]).with("BUNDLE_GEMFILE").and_return("/path/to/Gemfile")
47+
48+
# Setup Rails availability
49+
app = mock_rails_app
50+
rails_module = Module.new do
51+
define_singleton_method(:application) { app }
52+
define_singleton_method(:respond_to?) { |method, *| method == :application }
53+
end
54+
stub_const("Rails", rails_module)
55+
56+
# Mock Rake::Task at the boundary
57+
allow(Rake::Task).to receive(:task_defined?).with("react_on_rails:generate_packs").and_return(false)
58+
allow(Rake::Task).to receive(:[]).with("react_on_rails:generate_packs").and_return(mock_task)
59+
allow(mock_task).to receive(:reenable)
60+
allow(mock_task).to receive(:invoke)
61+
end
6362

64-
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
65-
expect(error_output.join("\n")).to match(/Error generating packs: Task failed/)
66-
end
63+
it "runs pack generation successfully in verbose mode using direct rake execution" do
64+
expect { described_class.generate(verbose: true) }
65+
.to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process
6766

68-
it "outputs errors to stderr even in silent mode" do
69-
allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Silent mode error"))
67+
expect(mock_task).to have_received(:invoke)
68+
expect(mock_rails_app).to have_received(:load_tasks)
69+
end
7070

71-
# Mock STDERR.puts to capture output
72-
error_output = []
73-
# rubocop:disable Style/GlobalStdStream
74-
allow(STDERR).to receive(:puts) { |msg| error_output << msg }
75-
# rubocop:enable Style/GlobalStdStream
71+
it "runs pack generation successfully in quiet mode using direct rake execution" do
72+
expect { described_class.generate(verbose: false) }
73+
.to output(/📦 Generating packs\.\.\. ✅/).to_stdout_from_any_process
7674

77-
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
78-
expect(error_output.join("\n")).to match(/Error generating packs: Silent mode error/)
79-
end
75+
expect(mock_task).to have_received(:invoke)
76+
end
8077

81-
it "includes backtrace in error output when DEBUG env is set" do
82-
allow(ENV).to receive(:[]).with("DEBUG").and_return("true")
83-
allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Debug error"))
78+
it "exits with error when pack generation fails" do
79+
allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Task failed"))
8480

85-
# Mock STDERR.puts to capture output
86-
error_output = []
87-
# rubocop:disable Style/GlobalStdStream
88-
allow(STDERR).to receive(:puts) { |msg| error_output << msg }
89-
# rubocop:enable Style/GlobalStdStream
81+
# Mock STDERR.puts to capture output
82+
error_output = []
83+
# rubocop:disable Style/GlobalStdStream
84+
allow(STDERR).to receive(:puts) { |msg| error_output << msg }
85+
# rubocop:enable Style/GlobalStdStream
9086

91-
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
92-
expect(error_output.join("\n")).to match(/Error generating packs: Debug error.*pack_generator_spec\.rb/m)
93-
end
87+
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
88+
expect(error_output.join("\n")).to match(/Error generating packs: Task failed/)
89+
end
9490

95-
it "suppresses stdout in silent mode" do
96-
# Mock task to produce output
97-
allow(mock_task).to receive(:invoke) do
98-
puts "This should be suppressed"
91+
it "outputs errors to stderr even in silent mode" do
92+
allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Silent mode error"))
93+
94+
# Mock STDERR.puts to capture output
95+
error_output = []
96+
# rubocop:disable Style/GlobalStdStream
97+
allow(STDERR).to receive(:puts) { |msg| error_output << msg }
98+
# rubocop:enable Style/GlobalStdStream
99+
100+
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
101+
expect(error_output.join("\n")).to match(/Error generating packs: Silent mode error/)
99102
end
100103

101-
expect { described_class.generate(verbose: false) }
102-
.not_to output(/This should be suppressed/).to_stdout_from_any_process
103-
end
104-
end
104+
it "includes backtrace in error output when DEBUG env is set" do
105+
allow(ENV).to receive(:[]).with("DEBUG").and_return("true")
106+
allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Debug error"))
105107

106-
context "when not in Bundler context" do
107-
before do
108-
# Ensure we're not in Bundler context
109-
hide_const("Bundler") if defined?(Bundler)
110-
end
108+
# Mock STDERR.puts to capture output
109+
error_output = []
110+
# rubocop:disable Style/GlobalStdStream
111+
allow(STDERR).to receive(:puts) { |msg| error_output << msg }
112+
# rubocop:enable Style/GlobalStdStream
111113

112-
it "runs pack generation successfully in verbose mode using bundle exec" do
113-
allow(described_class).to receive(:system)
114-
.with("bundle", "exec", "rake", "react_on_rails:generate_packs")
115-
.and_return(true)
114+
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
115+
expect(error_output.join("\n")).to match(/Error generating packs: Debug error.*pack_generator_spec\.rb/m)
116+
end
116117

117-
expect { described_class.generate(verbose: true) }
118-
.to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process
118+
it "suppresses stdout in silent mode" do
119+
# Mock task to produce output
120+
allow(mock_task).to receive(:invoke) do
121+
puts "This should be suppressed"
122+
end
119123

120-
expect(described_class).to have_received(:system)
121-
.with("bundle", "exec", "rake", "react_on_rails:generate_packs")
124+
expect { described_class.generate(verbose: false) }
125+
.not_to output(/This should be suppressed/).to_stdout_from_any_process
126+
end
122127
end
123128

124-
it "runs pack generation successfully in quiet mode using bundle exec" do
125-
allow(described_class).to receive(:system)
126-
.with("bundle", "exec", "rake", "react_on_rails:generate_packs",
127-
out: File::NULL, err: File::NULL)
128-
.and_return(true)
129+
context "when not in Bundler context" do
130+
before do
131+
# Ensure we're not in Bundler context
132+
hide_const("Bundler") if defined?(Bundler)
133+
end
129134

130-
expect { described_class.generate(verbose: false) }
131-
.to output(/📦 Generating packs\.\.\. ✅/).to_stdout_from_any_process
135+
it "runs pack generation successfully in verbose mode using bundle exec" do
136+
allow(described_class).to receive(:system)
137+
.with("bundle", "exec", "rake", "react_on_rails:generate_packs")
138+
.and_return(true)
132139

133-
expect(described_class).to have_received(:system)
134-
.with("bundle", "exec", "rake", "react_on_rails:generate_packs",
135-
out: File::NULL, err: File::NULL)
136-
end
140+
expect { described_class.generate(verbose: true) }
141+
.to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process
137142

138-
it "exits with error when pack generation fails" do
139-
allow(described_class).to receive(:system)
140-
.with("bundle", "exec", "rake", "react_on_rails:generate_packs",
141-
out: File::NULL, err: File::NULL)
142-
.and_return(false)
143+
expect(described_class).to have_received(:system)
144+
.with("bundle", "exec", "rake", "react_on_rails:generate_packs")
145+
end
143146

144-
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
145-
end
146-
end
147+
it "runs pack generation successfully in quiet mode using bundle exec" do
148+
allow(described_class).to receive(:system)
149+
.with("bundle", "exec", "rake", "react_on_rails:generate_packs",
150+
out: File::NULL, err: File::NULL)
151+
.and_return(true)
147152

148-
context "when Rails is not available" do
149-
before do
150-
stub_const("Bundler", Module.new)
151-
allow(ENV).to receive(:[]).and_call_original
152-
allow(ENV).to receive(:[]).with("BUNDLE_GEMFILE").and_return("/path/to/Gemfile")
153+
expect { described_class.generate(verbose: false) }
154+
.to output(/📦 Generating packs\.\.\. ✅/).to_stdout_from_any_process
155+
156+
expect(described_class).to have_received(:system)
157+
.with("bundle", "exec", "rake", "react_on_rails:generate_packs",
158+
out: File::NULL, err: File::NULL)
159+
end
153160

154-
# Rails not available
155-
hide_const("Rails") if defined?(Rails)
161+
it "exits with error when pack generation fails" do
162+
allow(described_class).to receive(:system)
163+
.with("bundle", "exec", "rake", "react_on_rails:generate_packs",
164+
out: File::NULL, err: File::NULL)
165+
.and_return(false)
166+
167+
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
168+
end
156169
end
157170

158-
it "falls back to bundle exec when Rails is not defined" do
159-
allow(described_class).to receive(:system)
160-
.with("bundle", "exec", "rake", "react_on_rails:generate_packs")
161-
.and_return(true)
171+
context "when Rails is not available" do
172+
before do
173+
stub_const("Bundler", Module.new)
174+
allow(ENV).to receive(:[]).and_call_original
175+
allow(ENV).to receive(:[]).with("BUNDLE_GEMFILE").and_return("/path/to/Gemfile")
162176

163-
expect { described_class.generate(verbose: true) }
164-
.to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process
177+
# Rails not available
178+
hide_const("Rails") if defined?(Rails)
179+
end
180+
181+
it "falls back to bundle exec when Rails is not defined" do
182+
allow(described_class).to receive(:system)
183+
.with("bundle", "exec", "rake", "react_on_rails:generate_packs")
184+
.and_return(true)
165185

166-
expect(described_class).to have_received(:system)
167-
.with("bundle", "exec", "rake", "react_on_rails:generate_packs")
186+
expect { described_class.generate(verbose: true) }
187+
.to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process
188+
189+
expect(described_class).to have_received(:system)
190+
.with("bundle", "exec", "rake", "react_on_rails:generate_packs")
191+
end
168192
end
169193
end
170194
end

0 commit comments

Comments
 (0)