Skip to content

Commit 0f6bdcc

Browse files
justin808claude
andauthored
Add Shakapacker precompile hook with ReScript support to Pro dummy app (#1977)
## Summary Adds a Shakapacker precompile hook to the Pro dummy app to support automatic ReScript builds and React on Rails pack generation before webpack compilation. ## Changes ### 1. Added Precompile Hook Script (`bin/shakapacker-precompile-hook`) - Cross-platform Ruby script that runs before Shakapacker compilation - **ReScript build support**: Detects both `bsconfig.json` (legacy) and `rescript.json` config files - **Automatic pack generation**: Runs `react_on_rails:generate_packs` when `auto_load_bundle` or `components_subdirectory` are configured - **Rails root resolution**: Properly finds Rails root directory and runs commands from correct location - **Error handling**: Graceful warnings and proper exit codes - **Package manager detection**: Supports both yarn and npm ### 2. Configured Shakapacker (`config/shakapacker.yml`) - Added `precompile_hook: 'bin/shakapacker-precompile-hook'` setting - Includes security comment about trusted script paths ## Technical Details ### ReScript Build Logic - Resolves Rails root directory early - Uses `File.join(rails_root, ...)` for proper path resolution - Executes build commands from Rails root using `Dir.chdir(rails_root)` - Ensures yarn/npm can find the correct `package.json` - Aborts gracefully if Rails root cannot be found ### Pack Generation Logic - Checks for React on Rails initializer existence - Parses config for `auto_load_bundle` or `components_subdirectory` settings (ignores comments) - Only runs if `react_on_rails:generate_packs` rake task exists - Uses array form for system calls to prevent shell injection ## Why This Matters The precompile hook ensures that: 1. ReScript code is compiled before webpack attempts to bundle it 2. Generated component packs are created before compilation in production 3. Developer experience is seamless - no manual build steps required 4. Works consistently across development and production environments ## Test Plan - [x] RuboCop passes with zero offenses - [x] Pre-commit hooks pass - [ ] Verify hook runs during `bin/shakapacker` compilation - [ ] Confirm ReScript builds when config files present - [ ] Check pack generation when auto_load_bundle configured - [ ] Test production asset precompilation workflow ## Related - Follows pattern from open-source dummy app - Aligns with Shakapacker 9.x precompile hook feature - See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- Reviewable:start --> - - - This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/shakacode/react_on_rails/1977) <!-- Reviewable:end --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Adds a configurable precompile hook to run before webpack compilation. * Conditional ReScript compilation is detected and built automatically when present. * React on Rails pack generation now auto-detects configuration and runs when needed; improved yarn/npm compatibility. * **Chores** * Template hook updated to a streamlined, shell-based implementation and dummy app now delegates to a shared precompile hook. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude <[email protected]>
1 parent d3ddb49 commit 0f6bdcc

File tree

4 files changed

+155
-94
lines changed

4 files changed

+155
-94
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
# Shakapacker precompile hook for React on Rails Pro test dummy app
5+
#
6+
# This script loads the shared test helper implementation.
7+
# For production apps, use the generator template which includes a standalone implementation.
8+
9+
# Find the gem root directory (four levels up from react_on_rails_pro/spec/dummy/bin)
10+
gem_root = File.expand_path("../../../..", __dir__)
11+
shared_hook = File.join(gem_root, "spec", "support", "shakapacker_precompile_hook_shared.rb")
12+
13+
unless File.exist?(shared_hook)
14+
warn "❌ Error: Shared precompile hook not found at #{shared_hook}"
15+
exit 1
16+
end
17+
18+
# Load and execute the shared hook
19+
load shared_hook

react_on_rails_pro/spec/dummy/config/shakapacker.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ default: &default
1919
# Reload manifest.json on all requests so we reload latest compiled packs
2020
cache_manifest: false
2121

22+
# Hook to run before webpack compilation (e.g., for generating dynamic entry points)
23+
# SECURITY: Only reference trusted scripts within your project. Ensure the hook path
24+
# points to a file within the project root that you control.
25+
precompile_hook: 'bin/shakapacker-precompile-hook'
26+
2227
# Extract and emit a css file
2328
extract_css: true
2429

Lines changed: 12 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,19 @@
11
#!/usr/bin/env ruby
22
# frozen_string_literal: true
33

4-
# Shakapacker precompile hook
5-
# This script runs before Shakapacker compilation in both development and production.
6-
# See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md
4+
# Shakapacker precompile hook for React on Rails test dummy app
5+
#
6+
# This script loads the shared test helper implementation.
7+
# For production apps, use the generator template which includes a standalone implementation.
78

8-
require "fileutils"
9+
# Find the gem root directory (three levels up from spec/dummy/bin)
10+
gem_root = File.expand_path("../../..", __dir__)
11+
shared_hook = File.join(gem_root, "spec", "support", "shakapacker_precompile_hook_shared.rb")
912

10-
# Find Rails root by walking upward looking for config/environment.rb
11-
def find_rails_root
12-
dir = Dir.pwd
13-
loop do
14-
return dir if File.exist?(File.join(dir, "config", "environment.rb"))
15-
16-
parent = File.dirname(dir)
17-
return nil if parent == dir # Reached filesystem root
18-
19-
dir = parent
20-
end
21-
end
22-
23-
# Build ReScript if needed
24-
def build_rescript_if_needed
25-
# Check for both old (bsconfig.json) and new (rescript.json) config files
26-
return unless File.exist?("bsconfig.json") || File.exist?("rescript.json")
27-
28-
puts "🔧 Building ReScript..."
29-
30-
# Cross-platform package manager detection
31-
yarn_available = system("yarn", "--version", out: File::NULL, err: File::NULL)
32-
npm_available = system("npm", "--version", out: File::NULL, err: File::NULL)
33-
34-
success = if yarn_available
35-
system("yarn", "build:rescript")
36-
elsif npm_available
37-
system("npm", "run", "build:rescript")
38-
else
39-
warn "⚠️ Warning: Neither yarn nor npm found. Skipping ReScript build."
40-
return
41-
end
42-
43-
if success
44-
puts "✅ ReScript build completed successfully"
45-
else
46-
warn "❌ ReScript build failed"
47-
exit 1
48-
end
49-
end
50-
51-
# Generate React on Rails packs if needed
52-
# rubocop:disable Metrics/CyclomaticComplexity
53-
def generate_packs_if_needed
54-
# Find Rails root directory
55-
rails_root = find_rails_root
56-
return unless rails_root
57-
58-
# Check if React on Rails initializer exists
59-
initializer_path = File.join(rails_root, "config", "initializers", "react_on_rails.rb")
60-
return unless File.exist?(initializer_path)
61-
62-
# Check if auto-pack generation is configured (match actual config assignments, not comments)
63-
config_file = File.read(initializer_path)
64-
# Match uncommented configuration lines only (lines not starting with #)
65-
has_auto_load = config_file =~ /^\s*(?!#).*config\.auto_load_bundle\s*=/
66-
has_components_subdir = config_file =~ /^\s*(?!#).*config\.components_subdirectory\s*=/
67-
return unless has_auto_load || has_components_subdir
68-
69-
puts "📦 Generating React on Rails packs..."
70-
71-
# Cross-platform bundle availability check
72-
bundle_available = system("bundle", "--version", out: File::NULL, err: File::NULL)
73-
return unless bundle_available
74-
75-
# Check if rake task exists (use array form for security)
76-
task_list = IO.popen(["bundle", "exec", "rails", "-T"], err: [:child, :out], &:read)
77-
return unless task_list.include?("react_on_rails:generate_packs")
78-
79-
# Use array form for better cross-platform support
80-
success = system("bundle", "exec", "rails", "react_on_rails:generate_packs")
81-
82-
if success
83-
puts "✅ Pack generation completed successfully"
84-
else
85-
warn "❌ Pack generation failed"
86-
exit 1
87-
end
88-
end
89-
# rubocop:enable Metrics/CyclomaticComplexity
90-
91-
# Main execution
92-
begin
93-
build_rescript_if_needed
94-
generate_packs_if_needed
95-
96-
exit 0
97-
rescue StandardError => e
98-
warn "❌ Precompile hook failed: #{e.message}"
99-
warn e.backtrace.join("\n")
13+
unless File.exist?(shared_hook)
14+
warn "❌ Error: Shared precompile hook not found at #{shared_hook}"
10015
exit 1
10116
end
17+
18+
# Load and execute the shared hook
19+
load shared_hook
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
# Shakapacker precompile hook for React on Rails - Shared Implementation
5+
#
6+
# This is the shared implementation used by both test dummy apps:
7+
# - spec/dummy/bin/shakapacker-precompile-hook
8+
# - react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
9+
#
10+
# This script runs before webpack compilation to:
11+
# 1. Build ReScript files (if configured)
12+
# 2. Generate pack files for auto-bundled components
13+
#
14+
# See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md
15+
16+
require "fileutils"
17+
require "json"
18+
19+
# Find Rails root by walking upward looking for config/environment.rb
20+
def find_rails_root
21+
dir = Dir.pwd
22+
while dir != "/"
23+
return dir if File.exist?(File.join(dir, "config", "environment.rb"))
24+
25+
dir = File.dirname(dir)
26+
end
27+
nil
28+
end
29+
30+
# Build ReScript if needed
31+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
32+
def build_rescript_if_needed
33+
rails_root = find_rails_root
34+
unless rails_root
35+
warn "⚠️ Warning: Could not find Rails root. Skipping ReScript build."
36+
return
37+
end
38+
39+
# Check for both old (bsconfig.json) and new (rescript.json) config files
40+
return unless File.exist?(File.join(rails_root, "bsconfig.json")) ||
41+
File.exist?(File.join(rails_root, "rescript.json"))
42+
43+
puts "🔧 Building ReScript..."
44+
45+
# Validate that build:rescript script exists in package.json
46+
package_json_path = File.join(rails_root, "package.json")
47+
unless File.exist?(package_json_path)
48+
warn "❌ Error: ReScript config found but package.json not found"
49+
warn " ReScript requires a package.json with a build:rescript script"
50+
exit 1
51+
end
52+
53+
package_json = JSON.parse(File.read(package_json_path))
54+
unless package_json.dig("scripts", "build:rescript")
55+
warn "❌ Error: ReScript config found but no build:rescript script in package.json"
56+
warn " Add this to your package.json scripts section:"
57+
warn ' "build:rescript": "rescript build"'
58+
exit 1
59+
end
60+
61+
Dir.chdir(rails_root) do
62+
# Cross-platform package manager detection
63+
if system("which yarn > /dev/null 2>&1")
64+
system("yarn", "build:rescript", exception: true)
65+
elsif system("which npm > /dev/null 2>&1")
66+
system("npm", "run", "build:rescript", exception: true)
67+
else
68+
warn "❌ Error: Neither yarn nor npm found but ReScript build required"
69+
warn " Install yarn or npm to build ReScript files"
70+
exit 1
71+
end
72+
73+
puts "✅ ReScript build completed successfully"
74+
end
75+
rescue JSON::ParserError => e
76+
warn "❌ Error: Invalid package.json: #{e.message}"
77+
exit 1
78+
rescue StandardError => e
79+
warn "❌ ReScript build failed: #{e.message}"
80+
exit 1
81+
end
82+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
83+
84+
# Generate React on Rails packs if needed
85+
def generate_packs_if_needed
86+
rails_root = find_rails_root
87+
return unless rails_root
88+
89+
initializer_path = File.join(rails_root, "config", "initializers", "react_on_rails.rb")
90+
return unless File.exist?(initializer_path)
91+
92+
# Check if auto-pack generation is configured
93+
# Match config lines that aren't commented out and allow flexible spacing
94+
initializer_content = File.read(initializer_path)
95+
return unless initializer_content.match?(/^\s*(?!#).*config\.auto_load_bundle\s*=/) ||
96+
initializer_content.match?(/^\s*(?!#).*config\.components_subdirectory\s*=/)
97+
98+
puts "📦 Generating React on Rails packs..."
99+
100+
Dir.chdir(rails_root) do
101+
# Skip validation during precompile hook execution
102+
ENV["REACT_ON_RAILS_SKIP_VALIDATION"] = "true"
103+
104+
# Run pack generation
105+
system("bundle", "exec", "rails", "react_on_rails:generate_packs", exception: true)
106+
puts "✅ Pack generation completed successfully"
107+
end
108+
rescue Errno::ENOENT => e
109+
warn "⚠️ Warning: #{e.message}"
110+
rescue StandardError => e
111+
warn "❌ Pack generation failed: #{e.message}"
112+
exit 1
113+
end
114+
115+
# Main execution (only if run directly, not when required)
116+
if __FILE__ == $PROGRAM_NAME
117+
build_rescript_if_needed
118+
generate_packs_if_needed
119+
end

0 commit comments

Comments
 (0)