Skip to content

Commit 3100a08

Browse files
justin808claude
andcommitted
Improve precompile hook error reporting and code clarity
This commit enhances the bin/dev precompile hook feature with better error reporting and clearer code documentation based on code review feedback. Key improvements: - Enhanced error reporting: Hook failures now display stdout and stderr output to help users debug issues, using Open3.capture3 instead of system() - Improved documentation: Added comments explaining why SHAKAPACKER_SKIP_PRECOMPILE_HOOK is always set (provides consistent signal for custom scripts) - Better code clarity: Added descriptive variable names in version check (has_precompile_hook_support, has_skip_env_var_support) - Structured changelog: Broke long entry into scannable sub-bullets - Updated tests: All 7 test cases now use Open3.capture3 with proper status mocks - Reduced complexity: Extracted error handling into separate method to fix cyclomatic complexity All tests pass (35 examples, 0 failures) and RuboCop is clean (153 files, no offenses). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 02b6100 commit 3100a08

File tree

3 files changed

+77
-36
lines changed

3 files changed

+77
-36
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ Changes since the last non-beta release.
2525

2626
#### Improved
2727

28-
- **Automatic Precompile Hook Coordination in bin/dev**: The `bin/dev` command now automatically runs Shakapacker's `precompile_hook` once before starting development processes and sets `SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true` to prevent duplicate execution in spawned webpack processes. This eliminates the need for manual coordination, sleep hacks, and duplicate task calls in Procfile.dev. Users can now configure expensive build tasks (like locale generation or ReScript compilation) once in `config/shakapacker.yml` and `bin/dev` will handle the coordination automatically. The feature includes a warning for users on Shakapacker versions below 9.4.0, as the `SHAKAPACKER_SKIP_PRECOMPILE_HOOK` environment variable is only supported in 9.4.0+. Addresses [2091](https://github.com/shakacode/react_on_rails/issues/2091) by [justin808](https://github.com/justin808).
28+
- **Automatic Precompile Hook Coordination in bin/dev**: The `bin/dev` command now automatically runs Shakapacker's `precompile_hook` once before starting development processes and sets `SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true` to prevent duplicate execution in spawned webpack processes.
29+
- Eliminates the need for manual coordination, sleep hacks, and duplicate task calls in Procfile.dev
30+
- Users can configure expensive build tasks (like locale generation or ReScript compilation) once in `config/shakapacker.yml` and `bin/dev` handles coordination automatically
31+
- Includes warning for Shakapacker versions below 9.4.0 (the `SHAKAPACKER_SKIP_PRECOMPILE_HOOK` environment variable is only supported in 9.4.0+)
32+
- The `SHAKAPACKER_SKIP_PRECOMPILE_HOOK` environment variable is set for all spawned processes, making it available for custom scripts that need to detect when `bin/dev` is managing the precompile hook
33+
- Addresses [2091](https://github.com/shakacode/react_on_rails/issues/2091) by [justin808](https://github.com/justin808)
2934

3035
### [v16.2.0.beta.12] - 2025-11-20
3136

lib/react_on_rails/dev/server_manager.rb

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,10 @@ def run_from_command_line(args = ARGV)
182182
end.parse!(args)
183183

184184
# Run precompile hook once before starting any mode (except kill/help)
185-
# Then set environment variable to prevent duplicate execution in spawned processes
185+
# Then set environment variable to prevent duplicate execution in spawned processes.
186+
# Note: We always set SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true (even when no hook is configured)
187+
# to provide a consistent signal that bin/dev is managing the precompile lifecycle.
188+
# This allows custom scripts to detect bin/dev's presence and adjust behavior accordingly.
186189
unless %w[kill help].include?(command) || help_requested
187190
run_precompile_hook_if_present
188191
ENV["SHAKAPACKER_SKIP_PRECOMPILE_HOOK"] = "true"
@@ -211,8 +214,9 @@ def run_from_command_line(args = ARGV)
211214

212215
private
213216

214-
# rubocop:disable Metrics/AbcSize
215217
def run_precompile_hook_if_present
218+
require "open3"
219+
216220
hook_value = PackerUtils.shakapacker_precompile_hook_value
217221
return unless hook_value
218222

@@ -223,24 +227,51 @@ def run_precompile_hook_if_present
223227
puts Rainbow(" Command: #{hook_value}").cyan
224228
puts ""
225229

226-
unless system(hook_value.to_s)
227-
puts ""
228-
puts Rainbow("❌ Precompile hook failed!").red.bold
229-
puts Rainbow(" Command: #{hook_value}").red
230+
# Capture stdout and stderr for better error reporting
231+
stdout, stderr, status = Open3.capture3(hook_value.to_s)
232+
233+
if status.success?
234+
puts Rainbow("✅ Precompile hook completed successfully").green
230235
puts ""
231-
puts Rainbow("💡 Fix the hook command in config/shakapacker.yml or remove it to continue").yellow
232-
exit 1
236+
else
237+
handle_precompile_hook_failure(hook_value, stdout, stderr)
233238
end
239+
end
234240

235-
puts Rainbow("✅ Precompile hook completed successfully").green
241+
# rubocop:disable Metrics/AbcSize
242+
def handle_precompile_hook_failure(hook_value, stdout, stderr)
243+
puts ""
244+
puts Rainbow("❌ Precompile hook failed!").red.bold
245+
puts Rainbow(" Command: #{hook_value}").red
236246
puts ""
247+
248+
if stdout && !stdout.strip.empty?
249+
puts Rainbow(" Output:").yellow
250+
stdout.strip.split("\n").each { |line| puts Rainbow(" #{line}").yellow }
251+
puts ""
252+
end
253+
254+
if stderr && !stderr.strip.empty?
255+
puts Rainbow(" Error:").red
256+
stderr.strip.split("\n").each { |line| puts Rainbow(" #{line}").red }
257+
puts ""
258+
end
259+
260+
puts Rainbow("💡 Fix the hook command in config/shakapacker.yml or remove it to continue").yellow
261+
exit 1
237262
end
238263
# rubocop:enable Metrics/AbcSize
239264

240265
# rubocop:disable Metrics/AbcSize
241266
def warn_if_shakapacker_version_too_old
242-
return unless PackerUtils.shakapacker_version_requirement_met?("9.0.0")
243-
return if PackerUtils.shakapacker_version_requirement_met?("9.4.0")
267+
# Only warn for Shakapacker versions in the range 9.0.0 to 9.3.x
268+
# Versions below 9.0.0 don't use the precompile_hook feature
269+
# Versions 9.4.0+ support SHAKAPACKER_SKIP_PRECOMPILE_HOOK environment variable
270+
has_precompile_hook_support = PackerUtils.shakapacker_version_requirement_met?("9.0.0")
271+
has_skip_env_var_support = PackerUtils.shakapacker_version_requirement_met?("9.4.0")
272+
273+
return unless has_precompile_hook_support
274+
return if has_skip_env_var_support
244275

245276
puts ""
246277
puts Rainbow("⚠️ Warning: Shakapacker #{PackerUtils.shakapacker_version} detected").yellow.bold

spec/react_on_rails/dev/server_manager_spec.rb

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -289,21 +289,21 @@ def mock_system_calls
289289
end
290290

291291
it "runs the hook and sets environment variable for development mode" do
292-
expect_any_instance_of(Kernel)
293-
.to receive(:system)
292+
status_double = instance_double(Process::Status, success?: true)
293+
expect(Open3).to receive(:capture3)
294294
.with("bundle exec rake react_on_rails:locale")
295-
.and_return(true)
295+
.and_return(["", "", status_double])
296296

297297
described_class.run_from_command_line([])
298298

299299
expect(ENV.fetch("SHAKAPACKER_SKIP_PRECOMPILE_HOOK", nil)).to eq("true")
300300
end
301301

302302
it "runs the hook and sets environment variable for static mode" do
303-
expect_any_instance_of(Kernel)
304-
.to receive(:system)
303+
status_double = instance_double(Process::Status, success?: true)
304+
expect(Open3).to receive(:capture3)
305305
.with("bundle exec rake react_on_rails:locale")
306-
.and_return(true)
306+
.and_return(["", "", status_double])
307307

308308
described_class.run_from_command_line(["static"])
309309

@@ -313,47 +313,50 @@ def mock_system_calls
313313
it "runs the hook and sets environment variable for prod mode" do
314314
env = { "NODE_ENV" => "production" }
315315
argv = ["bundle", "exec", "rails", "assets:precompile"]
316-
status_double = instance_double(Process::Status, success?: true)
317-
expect(Open3).to receive(:capture3).with(env, *argv).and_return(["output", "", status_double])
316+
assets_status_double = instance_double(Process::Status, success?: true)
317+
hook_status_double = instance_double(Process::Status, success?: true)
318318

319-
expect_any_instance_of(Kernel)
320-
.to receive(:system)
319+
# Expect both Open3.capture3 calls: one for the hook, one for assets:precompile
320+
expect(Open3).to receive(:capture3)
321321
.with("bundle exec rake react_on_rails:locale")
322-
.and_return(true)
322+
.and_return(["", "", hook_status_double])
323+
expect(Open3).to receive(:capture3)
324+
.with(env, *argv)
325+
.and_return(["output", "", assets_status_double])
323326

324327
described_class.run_from_command_line(["prod"])
325328

326329
expect(ENV.fetch("SHAKAPACKER_SKIP_PRECOMPILE_HOOK", nil)).to eq("true")
327330
end
328331

329332
it "exits when hook fails" do
330-
expect_any_instance_of(Kernel)
331-
.to receive(:system)
333+
status_double = instance_double(Process::Status, success?: false)
334+
expect(Open3).to receive(:capture3)
332335
.with("bundle exec rake react_on_rails:locale")
333-
.and_return(false)
336+
.and_return(["", "", status_double])
334337
expect_any_instance_of(Kernel).to receive(:exit).with(1)
335338

336339
described_class.run_from_command_line([])
337340
end
338341

339342
it "does not run hook or set environment variable for kill command" do
340-
expect_any_instance_of(Kernel).not_to receive(:system).with("bundle exec rake react_on_rails:locale")
343+
expect(Open3).not_to receive(:capture3)
341344

342345
described_class.run_from_command_line(["kill"])
343346

344347
expect(ENV.fetch("SHAKAPACKER_SKIP_PRECOMPILE_HOOK", nil)).to be_nil
345348
end
346349

347350
it "does not run hook or set environment variable for help command" do
348-
expect_any_instance_of(Kernel).not_to receive(:system).with("bundle exec rake react_on_rails:locale")
351+
expect(Open3).not_to receive(:capture3)
349352

350353
described_class.run_from_command_line(["help"])
351354

352355
expect(ENV.fetch("SHAKAPACKER_SKIP_PRECOMPILE_HOOK", nil)).to be_nil
353356
end
354357

355358
it "does not run hook or set environment variable for -h flag" do
356-
expect_any_instance_of(Kernel).not_to receive(:system).with("bundle exec rake react_on_rails:locale")
359+
expect(Open3).not_to receive(:capture3)
357360

358361
# The -h flag is handled by OptionParser and calls exit during option parsing
359362
# We need to mock exit to prevent the test from actually exiting
@@ -374,10 +377,10 @@ def mock_system_calls
374377
end
375378

376379
it "displays warning about unsupported SHAKAPACKER_SKIP_PRECOMPILE_HOOK" do
377-
expect_any_instance_of(Kernel)
378-
.to receive(:system)
380+
status_double = instance_double(Process::Status, success?: true)
381+
expect(Open3).to receive(:capture3)
379382
.with("bundle exec rake react_on_rails:locale")
380-
.and_return(true)
383+
.and_return(["", "", status_double])
381384

382385
expect do
383386
described_class.run_from_command_line([])
@@ -397,10 +400,10 @@ def mock_system_calls
397400
end
398401

399402
it "does not display warning" do
400-
expect_any_instance_of(Kernel)
401-
.to receive(:system)
403+
status_double = instance_double(Process::Status, success?: true)
404+
expect(Open3).to receive(:capture3)
402405
.with("bundle exec rake react_on_rails:locale")
403-
.and_return(true)
406+
.and_return(["", "", status_double])
404407

405408
expect do
406409
described_class.run_from_command_line([])
@@ -414,7 +417,9 @@ def mock_system_calls
414417
allow(ReactOnRails::PackerUtils).to receive(:shakapacker_precompile_hook_value).and_return(nil)
415418
end
416419

417-
it "does not run any hook but still sets environment variable for development mode" do
420+
it "sets environment variable even when no hook is configured (provides consistent signal)" do
421+
# The environment variable is intentionally set even when no hook exists
422+
# to provide a consistent signal that bin/dev is managing the precompile lifecycle
418423
expect_any_instance_of(Kernel).not_to receive(:system)
419424

420425
described_class.run_from_command_line([])

0 commit comments

Comments
 (0)