Skip to content

Commit d433348

Browse files
justin808claude
andauthored
Add automatic precompile hook coordination in bin/dev (#2092)
## Summary Adds automatic precompile hook coordination to `bin/dev`, eliminating the need for manual coordination, sleep hacks, and duplicate task calls in `Procfile.dev`. **This PR is focused solely on the bin/dev coordination feature.** The complementary idempotent locale generation feature is in PR #2093. ## How It Works When you configure a `precompile_hook` in `config/shakapacker.yml`: ```yaml default: &default precompile_hook: 'bundle exec rake react_on_rails:locale' ``` `bin/dev` will now: 1. ✅ Run the hook **once** before starting development processes 2. ✅ Set `SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true` environment variable 3. ✅ Pass the env var to all spawned processes (Rails, webpack, etc.) 4. ✅ Prevent webpack processes from re-running the hook independently ## Before & After **Before (manual coordination with sleep hacks):** ```procfile # Procfile.dev wp-server: sleep 15 && bundle exec rake react_on_rails:locale && bin/shakapacker --watch ``` **After (automatic coordination via bin/dev):** ```procfile # Procfile.dev wp-server: bin/shakapacker --watch ``` ```yaml # config/shakapacker.yml default: &default precompile_hook: 'bundle exec rake react_on_rails:locale' ``` Clean, simple, reliable - no sleep hacks required! ## Key Features **Shakapacker Version Detection** - Checks if you're using Shakapacker < 9.4.0 - Displays friendly warning about `SHAKAPACKER_SKIP_PRECOMPILE_HOOK` support - Recommends upgrading to 9.4.0+ to avoid duplicate hook execution **Error Handling** - Exits immediately if precompile hook fails - Shows clear error message with the failed command - Suggests fixing or removing the hook from config **Smart Skipping** - Skips hook execution for `bin/dev kill` and `bin/dev help` commands - Only runs hook for actual development modes (hmr, static, prod) **Help Flag Handling** - Detects `-h`/`--help` flags early - Prevents hook execution when user just wants help - Maintains clean separation between help and runtime logic ## Use Cases **Locale generation (with PR #2093):** ```yaml precompile_hook: 'bundle exec rake react_on_rails:locale' ``` **ReScript compilation:** ```yaml precompile_hook: 'yarn rescript' ``` **Multiple tasks:** ```yaml precompile_hook: 'bundle exec rake react_on_rails:locale && yarn rescript' ``` Any expensive build task that needs to run before webpack starts! ## Implementation Details **Modified Files:** - `lib/react_on_rails/dev/server_manager.rb` - Core coordination logic - `spec/react_on_rails/dev/server_manager_spec.rb` - Comprehensive test coverage (161 lines) - `docs/building-features/process-managers.md` - Feature documentation - `docs/building-features/i18n.md` - Usage example with locale generation **Test Coverage:** - ✅ Hook execution for all modes (development, static, prod) - ✅ Environment variable setting across all modes - ✅ Skipping for kill/help commands - ✅ Help flag handling (-h/--help) - ✅ Shakapacker version warning (< 9.4.0) - ✅ Error handling when hook fails - ✅ No-hook configuration scenario ## Shakapacker Version Requirements - **9.3.0+**: `precompile_hook` configuration support - **9.4.0+**: `SHAKAPACKER_SKIP_PRECOMPILE_HOOK` env var support (prevents duplicate execution) If you're on Shakapacker < 9.4.0, `bin/dev` will warn you that the hook may run multiple times. ## Related PRs - PR #2093 - Idempotent locale generation (makes `react_on_rails:locale` safe to call multiple times) - Issue #2091 - Original feature request ## Breaking Changes None - this is purely additive functionality. ## Testing Run the test suite: ```bash bundle exec rspec spec/react_on_rails/dev/server_manager_spec.rb ``` Manual testing: ```bash # Configure a precompile_hook in config/shakapacker.yml echo 'default: &default\n precompile_hook: "echo Running hook..."' >> config/shakapacker.yml # Run bin/dev and verify: bin/dev # Should see: "🔧 Running Shakapacker precompile hook..." # Should see: "Running hook..." # Should see: "✅ Precompile hook completed successfully" ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2306825 commit d433348

File tree

5 files changed

+342
-11
lines changed

5 files changed

+342
-11
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ After a release, please make sure to run `bundle exec rake update_changelog`. Th
2323

2424
Changes since the last non-beta release.
2525

26+
#### Improved
27+
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)
34+
2635
### [v16.2.0.beta.12] - 2025-11-20
2736

2837
#### Added

docs/building-features/i18n.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,31 @@ You can use [Rails internationalization (i18n)](https://guides.rubyonrails.org/i
2121

2222
3. The locale files must be generated before `yarn build` using `rake react_on_rails:locale`.
2323

24-
For development, you should adjust your startup scripts (`Procfile`s) so that they run `bundle exec rake react_on_rails:locale` before running any Webpack watch process (`yarn run build:development`).
24+
**Recommended: Use Shakapacker's precompile_hook with bin/dev** (React on Rails 16.2+, Shakapacker 9.3+)
2525

26-
If you are not using the React on Rails test helper,
27-
you may need to configure your CI to run `bundle exec rake react_on_rails:locale` before any Webpack process as well.
26+
The locale generation task is idempotent and can be safely called multiple times. Configure it in Shakapacker's `precompile_hook` and `bin/dev` will handle coordination automatically:
27+
28+
```yaml
29+
# config/shakapacker.yml
30+
default: &default
31+
# Run locale generation before webpack compilation
32+
# Safe to run multiple times - will skip if already built
33+
precompile_hook: 'bundle exec rake react_on_rails:locale'
34+
```
35+
36+
With this configuration, `bin/dev` will:
37+
38+
- Run the precompile hook **once** before starting development processes
39+
- Set `SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true` to prevent duplicate execution
40+
- Pass the environment variable to all spawned processes (Rails, webpack, etc.)
41+
42+
This eliminates the need for sleep hacks and manual coordination in `Procfile.dev`. See the [Process Managers documentation](./process-managers.md#precompile-hook-integration) for details.
43+
44+
**Alternative: Manual coordination**
45+
46+
For development, you can adjust your startup scripts (`Procfile`s) so that they run `bundle exec rake react_on_rails:locale` before running any Webpack watch process (`yarn run build:development`).
47+
48+
If you are not using the React on Rails test helper, you may need to configure your CI to run `bundle exec rake react_on_rails:locale` before any Webpack process as well.
2849

2950
> [!NOTE]
3051
> If you try to lint before running tests, and you depend on the test helper to build your locales, linting will fail because the translations won't be built yet.

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: 97 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,15 @@ 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+
# 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.
189+
unless %w[kill help].include?(command) || help_requested
190+
run_precompile_hook_if_present
191+
ENV["SHAKAPACKER_SKIP_PRECOMPILE_HOOK"] = "true"
192+
end
177193

178194
# Main execution
179195
case command
@@ -184,7 +200,7 @@ def run_from_command_line(args = ARGV)
184200
start(:static, "Procfile.dev-static-assets", verbose: options[:verbose], route: options[:route])
185201
when "kill"
186202
kill_processes
187-
when "help", "--help", "-h"
203+
when "help"
188204
show_help
189205
when "hmr", nil
190206
start(:development, "Procfile.dev", verbose: options[:verbose], route: options[:route])
@@ -194,10 +210,86 @@ def run_from_command_line(args = ARGV)
194210
exit 1
195211
end
196212
end
197-
# rubocop:enable Metrics/AbcSize
213+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
198214

199215
private
200216

217+
def run_precompile_hook_if_present
218+
require "open3"
219+
require "shellwords"
220+
221+
hook_value = PackerUtils.shakapacker_precompile_hook_value
222+
return unless hook_value
223+
224+
# Warn if Shakapacker version doesn't support SHAKAPACKER_SKIP_PRECOMPILE_HOOK
225+
warn_if_shakapacker_version_too_old
226+
227+
puts Rainbow("🔧 Running Shakapacker precompile hook...").cyan
228+
puts Rainbow(" Command: #{hook_value}").cyan
229+
puts ""
230+
231+
# Capture stdout and stderr for better error reporting
232+
# Use Shellwords.split for safer command execution (prevents shell metacharacter interpretation)
233+
command_args = Shellwords.split(hook_value.to_s)
234+
stdout, stderr, status = Open3.capture3(*command_args)
235+
236+
if status.success?
237+
puts Rainbow("✅ Precompile hook completed successfully").green
238+
puts ""
239+
else
240+
handle_precompile_hook_failure(hook_value, stdout, stderr)
241+
end
242+
end
243+
244+
# rubocop:disable Metrics/AbcSize
245+
def handle_precompile_hook_failure(hook_value, stdout, stderr)
246+
puts ""
247+
puts Rainbow("❌ Precompile hook failed!").red.bold
248+
puts Rainbow(" Command: #{hook_value}").red
249+
puts ""
250+
251+
if stdout && !stdout.strip.empty?
252+
puts Rainbow(" Output:").yellow
253+
stdout.strip.split("\n").each { |line| puts Rainbow(" #{line}").yellow }
254+
puts ""
255+
end
256+
257+
if stderr && !stderr.strip.empty?
258+
puts Rainbow(" Error:").red
259+
stderr.strip.split("\n").each { |line| puts Rainbow(" #{line}").red }
260+
puts ""
261+
end
262+
263+
puts Rainbow("💡 Fix the hook command in config/shakapacker.yml or remove it to continue").yellow
264+
exit 1
265+
end
266+
# rubocop:enable Metrics/AbcSize
267+
268+
# rubocop:disable Metrics/AbcSize
269+
def warn_if_shakapacker_version_too_old
270+
# Only warn for Shakapacker versions in the range 9.0.0 to 9.3.x
271+
# Versions below 9.0.0 don't use the precompile_hook feature
272+
# Versions 9.4.0+ support SHAKAPACKER_SKIP_PRECOMPILE_HOOK environment variable
273+
has_precompile_hook_support = PackerUtils.shakapacker_version_requirement_met?("9.0.0")
274+
has_skip_env_var_support = PackerUtils.shakapacker_version_requirement_met?("9.4.0")
275+
276+
return unless has_precompile_hook_support
277+
return if has_skip_env_var_support
278+
279+
puts ""
280+
puts Rainbow("⚠️ Warning: Shakapacker #{PackerUtils.shakapacker_version} detected").yellow.bold
281+
puts ""
282+
puts Rainbow(" The SHAKAPACKER_SKIP_PRECOMPILE_HOOK environment variable is not").yellow
283+
puts Rainbow(" supported in Shakapacker versions below 9.4.0. This may cause the").yellow
284+
puts Rainbow(" precompile_hook to run multiple times (once by bin/dev, and again").yellow
285+
puts Rainbow(" by each webpack process).").yellow
286+
puts ""
287+
puts Rainbow(" Recommendation: Upgrade to Shakapacker 9.4.0 or later:").cyan
288+
puts Rainbow(" bundle update shakapacker").cyan.bold
289+
puts ""
290+
end
291+
# rubocop:enable Metrics/AbcSize
292+
201293
def help_usage
202294
Rainbow("📋 Usage: bin/dev [command] [options]").bold
203295
end

0 commit comments

Comments
 (0)