Skip to content

Commit b7ee06b

Browse files
justin808claude
andcommitted
Add 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. Key improvements: - Executes precompile_hook once before launching Procfile processes - Sets SHAKAPACKER_SKIP_PRECOMPILE_HOOK environment variable - All spawned processes inherit the variable to prevent re-execution - Displays warning for Shakapacker versions below 9.4.0 - Skips hook execution for help and kill commands This eliminates the need for: - Manual coordination in Procfile.dev - Sleep hacks to sequence tasks - Duplicate task calls across processes - Race conditions when multiple processes generate files Users can now configure expensive build tasks (locale generation, ReScript compilation, etc.) once in config/shakapacker.yml and bin/dev handles the coordination automatically. Changes: - lib/react_on_rails/dev/server_manager.rb: Added hook execution logic with version warning for Shakapacker < 9.4.0 - spec/react_on_rails/dev/server_manager_spec.rb: Added comprehensive tests for hook execution, environment variable setting, and version warnings - docs/building-features/process-managers.md: Updated documentation with precompile hook integration details and version requirements - CHANGELOG.md: Added entry for automatic precompile hook coordination Addresses #2091 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 33f38e7 commit b7ee06b

File tree

4 files changed

+266
-8
lines changed

4 files changed

+266
-8
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ 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).
29+
2830
- **Idempotent Locale Generation**: The `react_on_rails:locale` rake task is now idempotent, automatically skipping generation when locale files are already up-to-date. This makes it safe to call multiple times (e.g., in Shakapacker's `precompile_hook`) without duplicate work. Added `force=true` option to override timestamp checking. [PR 2092](https://github.com/shakacode/react_on_rails/pull/2092) by [justin808](https://github.com/justin808).
2931

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

docs/building-features/process-managers.md

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,46 @@ React on Rails includes `bin/dev` which automatically uses Overmind or Foreman:
1616

1717
This script will:
1818

19-
1. Try to use Overmind (if installed)
20-
2. Fall back to Foreman (if installed)
21-
3. Show installation instructions if neither is found
19+
1. Run Shakapacker's `precompile_hook` once (if configured in `config/shakapacker.yml`)
20+
2. Set `SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true` to prevent duplicate execution
21+
3. Try to use Overmind (if installed)
22+
4. Fall back to Foreman (if installed)
23+
5. Show installation instructions if neither is found
24+
25+
### Precompile Hook Integration
26+
27+
If you have configured a `precompile_hook` in `config/shakapacker.yml`, `bin/dev` will automatically:
28+
29+
- Execute the hook **once** before starting development processes
30+
- Set the `SHAKAPACKER_SKIP_PRECOMPILE_HOOK` environment variable
31+
- Pass this environment variable to all spawned processes (Rails, webpack, etc.)
32+
- Prevent webpack processes from re-running the hook independently
33+
34+
**Note:** The `SHAKAPACKER_SKIP_PRECOMPILE_HOOK` environment variable is supported in Shakapacker 9.4.0 and later. If you're using an earlier version, `bin/dev` will display a warning recommending you upgrade to avoid duplicate hook execution.
35+
36+
This eliminates the need for manual coordination in your `Procfile.dev`. For example:
37+
38+
**Before (manual coordination with sleep hacks):**
39+
40+
```procfile
41+
# Procfile.dev
42+
wp-server: sleep 15 && bundle exec rake react_on_rails:locale && bin/shakapacker --watch
43+
```
44+
45+
**After (automatic coordination via bin/dev):**
46+
47+
```procfile
48+
# Procfile.dev
49+
wp-server: bin/shakapacker --watch
50+
```
51+
52+
```yaml
53+
# config/shakapacker.yml
54+
default: &default
55+
precompile_hook: 'bundle exec rake react_on_rails:locale'
56+
```
57+
58+
See the [i18n documentation](./i18n.md#internationalization) for more details on configuring the precompile hook.
2259
2360
## Installing a Process Manager
2461

lib/react_on_rails/dev/server_manager.rb

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
module ReactOnRails
99
module Dev
1010
class ServerManager
11+
HELP_FLAGS = ["-h", "--help"].freeze
12+
1113
class << self
1214
def start(mode = :development, procfile = nil, verbose: false, route: nil, rails_env: nil)
1315
case mode
@@ -145,10 +147,17 @@ def show_help
145147
puts help_troubleshooting
146148
end
147149

148-
# rubocop:disable Metrics/AbcSize
150+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
149151
def run_from_command_line(args = ARGV)
150152
require "optparse"
151153

154+
# Get the command early to check for help/kill before running hooks
155+
# We need to do this before OptionParser processes flags like -h/--help
156+
command = args.find { |arg| !arg.start_with?("--") && !arg.start_with?("-") }
157+
158+
# Check if help flags are present in args (before OptionParser processes them)
159+
help_requested = args.any? { |arg| HELP_FLAGS.include?(arg) }
160+
152161
options = { route: nil, rails_env: nil, verbose: false }
153162

154163
OptionParser.new do |opts|
@@ -172,8 +181,12 @@ def run_from_command_line(args = ARGV)
172181
end
173182
end.parse!(args)
174183

175-
# Get the command (anything that's not parsed as an option)
176-
command = args[0]
184+
# Run precompile hook once before starting any mode (except kill/help)
185+
# Then set environment variable to prevent duplicate execution in spawned processes
186+
unless %w[kill help].include?(command) || help_requested
187+
run_precompile_hook_if_present
188+
ENV["SHAKAPACKER_SKIP_PRECOMPILE_HOOK"] = "true"
189+
end
177190

178191
# Main execution
179192
case command
@@ -184,7 +197,7 @@ def run_from_command_line(args = ARGV)
184197
start(:static, "Procfile.dev-static-assets", verbose: options[:verbose], route: options[:route])
185198
when "kill"
186199
kill_processes
187-
when "help", "--help", "-h"
200+
when "help"
188201
show_help
189202
when "hmr", nil
190203
start(:development, "Procfile.dev", verbose: options[:verbose], route: options[:route])
@@ -194,10 +207,55 @@ def run_from_command_line(args = ARGV)
194207
exit 1
195208
end
196209
end
197-
# rubocop:enable Metrics/AbcSize
210+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
198211

199212
private
200213

214+
# rubocop:disable Metrics/AbcSize
215+
def run_precompile_hook_if_present
216+
hook_value = PackerUtils.shakapacker_precompile_hook_value
217+
return unless hook_value
218+
219+
# Warn if Shakapacker version doesn't support SHAKAPACKER_SKIP_PRECOMPILE_HOOK
220+
warn_if_shakapacker_version_too_old
221+
222+
puts Rainbow("🔧 Running Shakapacker precompile hook...").cyan
223+
puts Rainbow(" Command: #{hook_value}").cyan
224+
puts ""
225+
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+
puts ""
231+
puts Rainbow("💡 Fix the hook command in config/shakapacker.yml or remove it to continue").yellow
232+
exit 1
233+
end
234+
235+
puts Rainbow("✅ Precompile hook completed successfully").green
236+
puts ""
237+
end
238+
# rubocop:enable Metrics/AbcSize
239+
240+
# rubocop:disable Metrics/AbcSize
241+
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")
244+
245+
puts ""
246+
puts Rainbow("⚠️ Warning: Shakapacker #{PackerUtils.shakapacker_version} detected").yellow.bold
247+
puts ""
248+
puts Rainbow(" The SHAKAPACKER_SKIP_PRECOMPILE_HOOK environment variable is not").yellow
249+
puts Rainbow(" supported in Shakapacker versions below 9.4.0. This may cause the").yellow
250+
puts Rainbow(" precompile_hook to run multiple times (once by bin/dev, and again").yellow
251+
puts Rainbow(" by each webpack process).").yellow
252+
puts ""
253+
puts Rainbow(" Recommendation: Upgrade to Shakapacker 9.4.0 or later:").cyan
254+
puts Rainbow(" bundle update shakapacker").cyan.bold
255+
puts ""
256+
end
257+
# rubocop:enable Metrics/AbcSize
258+
201259
def help_usage
202260
Rainbow("📋 Usage: bin/dev [command] [options]").bold
203261
end

spec/react_on_rails/dev/server_manager_spec.rb

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,4 +268,165 @@ def mock_system_calls
268268
expect { described_class.show_help }.to output(%r{Usage: bin/dev \[command\]}).to_stdout_from_any_process
269269
end
270270
end
271+
272+
describe ".run_from_command_line with precompile hook" do
273+
before do
274+
mock_system_calls
275+
# Clear environment variable before each test
276+
ENV.delete("SHAKAPACKER_SKIP_PRECOMPILE_HOOK")
277+
end
278+
279+
context "when precompile hook is configured" do
280+
before do
281+
# Default to a version that supports the skip flag (no warning)
282+
allow(ReactOnRails::PackerUtils).to receive_messages(
283+
shakapacker_precompile_hook_value: "bundle exec rake react_on_rails:locale", shakapacker_version: "9.4.0"
284+
)
285+
allow(ReactOnRails::PackerUtils).to receive(:shakapacker_version_requirement_met?)
286+
.with("9.0.0").and_return(true)
287+
allow(ReactOnRails::PackerUtils).to receive(:shakapacker_version_requirement_met?)
288+
.with("9.4.0").and_return(true)
289+
end
290+
291+
it "runs the hook and sets environment variable for development mode" do
292+
expect_any_instance_of(Kernel)
293+
.to receive(:system)
294+
.with("bundle exec rake react_on_rails:locale")
295+
.and_return(true)
296+
297+
described_class.run_from_command_line([])
298+
299+
expect(ENV.fetch("SHAKAPACKER_SKIP_PRECOMPILE_HOOK", nil)).to eq("true")
300+
end
301+
302+
it "runs the hook and sets environment variable for static mode" do
303+
expect_any_instance_of(Kernel)
304+
.to receive(:system)
305+
.with("bundle exec rake react_on_rails:locale")
306+
.and_return(true)
307+
308+
described_class.run_from_command_line(["static"])
309+
310+
expect(ENV.fetch("SHAKAPACKER_SKIP_PRECOMPILE_HOOK", nil)).to eq("true")
311+
end
312+
313+
it "runs the hook and sets environment variable for prod mode" do
314+
env = { "NODE_ENV" => "production" }
315+
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])
318+
319+
expect_any_instance_of(Kernel)
320+
.to receive(:system)
321+
.with("bundle exec rake react_on_rails:locale")
322+
.and_return(true)
323+
324+
described_class.run_from_command_line(["prod"])
325+
326+
expect(ENV.fetch("SHAKAPACKER_SKIP_PRECOMPILE_HOOK", nil)).to eq("true")
327+
end
328+
329+
it "exits when hook fails" do
330+
expect_any_instance_of(Kernel)
331+
.to receive(:system)
332+
.with("bundle exec rake react_on_rails:locale")
333+
.and_return(false)
334+
expect_any_instance_of(Kernel).to receive(:exit).with(1)
335+
336+
described_class.run_from_command_line([])
337+
end
338+
339+
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")
341+
342+
described_class.run_from_command_line(["kill"])
343+
344+
expect(ENV.fetch("SHAKAPACKER_SKIP_PRECOMPILE_HOOK", nil)).to be_nil
345+
end
346+
347+
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")
349+
350+
described_class.run_from_command_line(["help"])
351+
352+
expect(ENV.fetch("SHAKAPACKER_SKIP_PRECOMPILE_HOOK", nil)).to be_nil
353+
end
354+
355+
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")
357+
358+
# The -h flag is handled by OptionParser and calls exit during option parsing
359+
# We need to mock exit to prevent the test from actually exiting
360+
allow_any_instance_of(Kernel).to receive(:exit)
361+
362+
described_class.run_from_command_line(["-h"])
363+
364+
expect(ENV.fetch("SHAKAPACKER_SKIP_PRECOMPILE_HOOK", nil)).to be_nil
365+
end
366+
367+
context "with Shakapacker version below 9.4.0" do
368+
before do
369+
allow(ReactOnRails::PackerUtils).to receive(:shakapacker_version).and_return("9.3.0")
370+
allow(ReactOnRails::PackerUtils).to receive(:shakapacker_version_requirement_met?)
371+
.with("9.0.0").and_return(true)
372+
allow(ReactOnRails::PackerUtils).to receive(:shakapacker_version_requirement_met?)
373+
.with("9.4.0").and_return(false)
374+
end
375+
376+
it "displays warning about unsupported SHAKAPACKER_SKIP_PRECOMPILE_HOOK" do
377+
expect_any_instance_of(Kernel)
378+
.to receive(:system)
379+
.with("bundle exec rake react_on_rails:locale")
380+
.and_return(true)
381+
382+
expect do
383+
described_class.run_from_command_line([])
384+
end.to output(/Warning: Shakapacker 9\.3\.0 detected/).to_stdout_from_any_process
385+
386+
expect(ENV.fetch("SHAKAPACKER_SKIP_PRECOMPILE_HOOK", nil)).to eq("true")
387+
end
388+
end
389+
390+
context "with Shakapacker version 9.4.0 or later" do
391+
before do
392+
allow(ReactOnRails::PackerUtils).to receive(:shakapacker_version).and_return("9.4.0")
393+
allow(ReactOnRails::PackerUtils).to receive(:shakapacker_version_requirement_met?)
394+
.with("9.0.0").and_return(true)
395+
allow(ReactOnRails::PackerUtils).to receive(:shakapacker_version_requirement_met?)
396+
.with("9.4.0").and_return(true)
397+
end
398+
399+
it "does not display warning" do
400+
expect_any_instance_of(Kernel)
401+
.to receive(:system)
402+
.with("bundle exec rake react_on_rails:locale")
403+
.and_return(true)
404+
405+
expect do
406+
described_class.run_from_command_line([])
407+
end.not_to output(/Warning: Shakapacker/).to_stdout_from_any_process
408+
end
409+
end
410+
end
411+
412+
context "when no precompile hook is configured" do
413+
before do
414+
allow(ReactOnRails::PackerUtils).to receive(:shakapacker_precompile_hook_value).and_return(nil)
415+
end
416+
417+
it "does not run any hook but still sets environment variable for development mode" do
418+
expect_any_instance_of(Kernel).not_to receive(:system)
419+
420+
described_class.run_from_command_line([])
421+
422+
expect(ENV.fetch("SHAKAPACKER_SKIP_PRECOMPILE_HOOK", nil)).to eq("true")
423+
end
424+
425+
it "does not set environment variable for kill command" do
426+
described_class.run_from_command_line(["kill"])
427+
428+
expect(ENV.fetch("SHAKAPACKER_SKIP_PRECOMPILE_HOOK", nil)).to be_nil
429+
end
430+
end
431+
end
271432
end

0 commit comments

Comments
 (0)