From 850c7e9985a37f9f4ef05e438060197c30008b4e Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 10 Nov 2025 20:04:26 -1000 Subject: [PATCH 01/12] Fix Pro dummy app Playwright test timeouts by aligning with defer loading strategy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the same defer loading strategy fix from commit d1a8a1a4 to the Pro dummy app to resolve race condition causing Playwright test timeouts. ## Problem Playwright E2E tests for streaming were timing out waiting for console message "ToggleContainer with title", indicating React components weren't hydrating. ## Root Cause The Pro dummy app was still using async: true for javascript_pack_tag while the open-source dummy app was updated to defer: true in commit d1a8a1a4. This created a race condition where: - Generated component packs load asynchronously - Main client-bundle also loads asynchronously - If client-bundle executes before component registrations complete, React tries to hydrate unregistered components - ToggleContainer never hydrates, useEffect never runs, console.log never fires ## Solution 1. Changed javascript_pack_tag from async: true to defer: true in application.html.erb 2. Added precompile_hook to shakapacker.yml for pack generation 3. Added bin/shakapacker-precompile-hook script Using defer: true ensures script execution order - generated component packs load and register components before main bundle executes, preventing the race condition. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../app/views/layouts/application.html.erb | 9 +- .../dummy/bin/shakapacker-precompile-hook | 101 ++++++++++++++++++ .../spec/dummy/config/shakapacker.yml | 5 + 3 files changed, 110 insertions(+), 5 deletions(-) create mode 100755 react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook diff --git a/react_on_rails_pro/spec/dummy/app/views/layouts/application.html.erb b/react_on_rails_pro/spec/dummy/app/views/layouts/application.html.erb index 16f3041aa6..29030b497f 100644 --- a/react_on_rails_pro/spec/dummy/app/views/layouts/application.html.erb +++ b/react_on_rails_pro/spec/dummy/app/views/layouts/application.html.erb @@ -24,13 +24,12 @@ media: 'all', 'data-turbo-track': 'reload') %> - <%# async: true is the recommended approach for Shakapacker >= 8.2.0 (currently using 9.3.0). - It enables React 18's Selective Hydration and provides optimal Time to Interactive (TTI). - Use immediate_hydration feature to control hydration timing for Selective/Immediate Hydration. - See docs/building-features/streaming-server-rendering.md + <%# Use defer: true to ensure proper script execution order. + When using generated component packs (auto_load_bundle), defer ensures + component registrations complete before React hydration begins. skip_js_packs param is used for testing purposes to simulate hydration failure %> <% unless params[:skip_js_packs] == 'true' %> - <%= javascript_pack_tag('client-bundle', 'data-turbo-track': 'reload', async: true) %> + <%= javascript_pack_tag('client-bundle', 'data-turbo-track': 'reload', defer: true) %> <% end %> <%= csrf_meta_tags %> diff --git a/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook b/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook new file mode 100755 index 0000000000..b650b02db5 --- /dev/null +++ b/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook @@ -0,0 +1,101 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Shakapacker precompile hook +# This script runs before Shakapacker compilation in both development and production. +# See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md + +require "fileutils" + +# Find Rails root by walking upward looking for config/environment.rb +def find_rails_root + dir = Dir.pwd + loop do + return dir if File.exist?(File.join(dir, "config", "environment.rb")) + + parent = File.dirname(dir) + return nil if parent == dir # Reached filesystem root + + dir = parent + end +end + +# Build ReScript if needed +def build_rescript_if_needed + # Check for both old (bsconfig.json) and new (rescript.json) config files + return unless File.exist?("bsconfig.json") || File.exist?("rescript.json") + + puts "🔧 Building ReScript..." + + # Cross-platform package manager detection + yarn_available = system("yarn", "--version", out: File::NULL, err: File::NULL) + npm_available = system("npm", "--version", out: File::NULL, err: File::NULL) + + success = if yarn_available + system("yarn", "build:rescript") + elsif npm_available + system("npm", "run", "build:rescript") + else + warn "⚠️ Warning: Neither yarn nor npm found. Skipping ReScript build." + return + end + + if success + puts "✅ ReScript build completed successfully" + else + warn "❌ ReScript build failed" + exit 1 + end +end + +# Generate React on Rails packs if needed +# rubocop:disable Metrics/CyclomaticComplexity +def generate_packs_if_needed + # Find Rails root directory + rails_root = find_rails_root + return unless rails_root + + # Check if React on Rails initializer exists + initializer_path = File.join(rails_root, "config", "initializers", "react_on_rails.rb") + return unless File.exist?(initializer_path) + + # Check if auto-pack generation is configured (match actual config assignments, not comments) + config_file = File.read(initializer_path) + # Match uncommented configuration lines only (lines not starting with #) + has_auto_load = config_file =~ /^\s*(?!#).*config\.auto_load_bundle\s*=/ + has_components_subdir = config_file =~ /^\s*(?!#).*config\.components_subdirectory\s*=/ + return unless has_auto_load || has_components_subdir + + puts "📦 Generating React on Rails packs..." + + # Cross-platform bundle availability check + bundle_available = system("bundle", "--version", out: File::NULL, err: File::NULL) + return unless bundle_available + + # Check if rake task exists (use array form for security) + task_list = IO.popen(["bundle", "exec", "rails", "-T"], err: [:child, :out], &:read) + return unless task_list.include?("react_on_rails:generate_packs") + + # Use array form for better cross-platform support + success = system("bundle", "exec", "rails", "react_on_rails:generate_packs") + + if success + puts "✅ Pack generation completed successfully" + else + warn "❌ Pack generation failed" + exit 1 + end +end +# rubocop:enable Metrics/CyclomaticComplexity + +# Main execution +begin + build_rescript_if_needed + generate_packs_if_needed + + exit 0 +rescue StandardError => e + warn "❌ Precompile hook failed: #{e.message}" + warn e.backtrace.join("\n") + exit 1 +end diff --git a/react_on_rails_pro/spec/dummy/config/shakapacker.yml b/react_on_rails_pro/spec/dummy/config/shakapacker.yml index dab40bdf81..068bb30df4 100644 --- a/react_on_rails_pro/spec/dummy/config/shakapacker.yml +++ b/react_on_rails_pro/spec/dummy/config/shakapacker.yml @@ -19,6 +19,11 @@ default: &default # Reload manifest.json on all requests so we reload latest compiled packs cache_manifest: false + # Hook to run before webpack compilation (e.g., for generating dynamic entry points) + # SECURITY: Only reference trusted scripts within your project. Ensure the hook path + # points to a file within the project root that you control. + precompile_hook: 'bin/shakapacker-precompile-hook' + # Extract and emit a css file extract_css: true From c0e8e4fc270ea0e617a917de134659de22b9cd4f Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Wed, 12 Nov 2025 19:58:43 -1000 Subject: [PATCH 02/12] Fix RuboCop violations and add comment about defer loading strategy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unnecessary rubocop disable/enable directives - Fix Style/SymbolArray violation in shakapacker-precompile-hook - Add explanatory comment about generated_component_packs_loading_strategy defaulting to :defer to match OSS dummy app configuration Note: The failing "React Router Sixth Page" RSpec test is a known flaky test that also fails intermittently on master. This is not a regression from the defer loading strategy changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../spec/dummy/bin/shakapacker-precompile-hook | 5 +---- .../spec/dummy/config/initializers/react_on_rails.rb | 3 +++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook b/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook index b650b02db5..9efe8e9c99 100755 --- a/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook +++ b/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook @@ -49,7 +49,6 @@ def build_rescript_if_needed end # Generate React on Rails packs if needed -# rubocop:disable Metrics/CyclomaticComplexity def generate_packs_if_needed # Find Rails root directory rails_root = find_rails_root @@ -73,7 +72,7 @@ def generate_packs_if_needed return unless bundle_available # Check if rake task exists (use array form for security) - task_list = IO.popen(["bundle", "exec", "rails", "-T"], err: [:child, :out], &:read) + task_list = IO.popen(["bundle", "exec", "rails", "-T"], err: %i[child out], &:read) return unless task_list.include?("react_on_rails:generate_packs") # Use array form for better cross-platform support @@ -86,8 +85,6 @@ def generate_packs_if_needed exit 1 end end -# rubocop:enable Metrics/CyclomaticComplexity - # Main execution begin build_rescript_if_needed diff --git a/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb b/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb index ed8a0038ba..870a99d176 100644 --- a/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb +++ b/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb @@ -36,6 +36,9 @@ def self.adjust_props_for_client_side_hydration(_component_name, props) config.components_subdirectory = "ror-auto-load-components" config.auto_load_bundle = true + # Don't explicitly set generated_component_packs_loading_strategy - let it default to :defer + # which ensures generated component packs load and register components before main bundle executes + ################################################################################ # Pro Feature Testing: Server Bundle Security ################################################################################ From 6bbd3eb77d2b8c9c5a9c456dbff569445c15d954 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sat, 15 Nov 2025 21:56:27 -1000 Subject: [PATCH 03/12] Revert Pro app to async and fix ReScript build in precompile hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Revert Pro dummy app back to async: true (Pro supports Selective Hydration) - Fix ReScript build to run from Rails root instead of current directory - Use File.join for proper path resolution of config files - Wrap build commands in Dir.chdir(rails_root) for correct execution - Add early Rails root resolution with proper error handling - Remove unnecessary defer strategy comment from initializer - Add blank line before main execution section for style 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../app/views/layouts/application.html.erb | 9 +++--- .../dummy/bin/shakapacker-precompile-hook | 31 +++++++++++++------ .../config/initializers/react_on_rails.rb | 3 -- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/react_on_rails_pro/spec/dummy/app/views/layouts/application.html.erb b/react_on_rails_pro/spec/dummy/app/views/layouts/application.html.erb index 29030b497f..16f3041aa6 100644 --- a/react_on_rails_pro/spec/dummy/app/views/layouts/application.html.erb +++ b/react_on_rails_pro/spec/dummy/app/views/layouts/application.html.erb @@ -24,12 +24,13 @@ media: 'all', 'data-turbo-track': 'reload') %> - <%# Use defer: true to ensure proper script execution order. - When using generated component packs (auto_load_bundle), defer ensures - component registrations complete before React hydration begins. + <%# async: true is the recommended approach for Shakapacker >= 8.2.0 (currently using 9.3.0). + It enables React 18's Selective Hydration and provides optimal Time to Interactive (TTI). + Use immediate_hydration feature to control hydration timing for Selective/Immediate Hydration. + See docs/building-features/streaming-server-rendering.md skip_js_packs param is used for testing purposes to simulate hydration failure %> <% unless params[:skip_js_packs] == 'true' %> - <%= javascript_pack_tag('client-bundle', 'data-turbo-track': 'reload', defer: true) %> + <%= javascript_pack_tag('client-bundle', 'data-turbo-track': 'reload', async: true) %> <% end %> <%= csrf_meta_tags %> diff --git a/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook b/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook index 9efe8e9c99..f42ee7769a 100755 --- a/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook +++ b/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook @@ -22,8 +22,17 @@ end # Build ReScript if needed def build_rescript_if_needed + # Find Rails root directory + rails_root = find_rails_root + unless rails_root + warn "⚠️ Warning: Could not find Rails root. Skipping ReScript build." + return + end + # Check for both old (bsconfig.json) and new (rescript.json) config files - return unless File.exist?("bsconfig.json") || File.exist?("rescript.json") + bsconfig_path = File.join(rails_root, "bsconfig.json") + rescript_config_path = File.join(rails_root, "rescript.json") + return unless File.exist?(bsconfig_path) || File.exist?(rescript_config_path) puts "🔧 Building ReScript..." @@ -31,14 +40,17 @@ def build_rescript_if_needed yarn_available = system("yarn", "--version", out: File::NULL, err: File::NULL) npm_available = system("npm", "--version", out: File::NULL, err: File::NULL) - success = if yarn_available - system("yarn", "build:rescript") - elsif npm_available - system("npm", "run", "build:rescript") - else - warn "⚠️ Warning: Neither yarn nor npm found. Skipping ReScript build." - return - end + # Run build command from Rails root directory + success = Dir.chdir(rails_root) do + if yarn_available + system("yarn", "build:rescript") + elsif npm_available + system("npm", "run", "build:rescript") + else + warn "⚠️ Warning: Neither yarn nor npm found. Skipping ReScript build." + return false + end + end if success puts "✅ ReScript build completed successfully" @@ -85,6 +97,7 @@ def generate_packs_if_needed exit 1 end end + # Main execution begin build_rescript_if_needed diff --git a/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb b/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb index 870a99d176..ed8a0038ba 100644 --- a/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb +++ b/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb @@ -36,9 +36,6 @@ def self.adjust_props_for_client_side_hydration(_component_name, props) config.components_subdirectory = "ror-auto-load-components" config.auto_load_bundle = true - # Don't explicitly set generated_component_packs_loading_strategy - let it default to :defer - # which ensures generated component packs load and register components before main bundle executes - ################################################################################ # Pro Feature Testing: Server Bundle Security ################################################################################ From 0f91a2030ab43e4944afaa44309907b610979fda Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sun, 16 Nov 2025 16:25:54 -1000 Subject: [PATCH 04/12] Replace Ruby precompile hooks with shared bash script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert precompile hooks from Ruby to bash and DRY them up: **Why Bash Over Ruby:** - Simpler: No Ruby stdlib dependencies - Faster: No Ruby interpreter startup overhead - Standard: Most build hooks are shell scripts - Cross-platform: Works everywhere (sh/bash universal) - Fewer dependencies: Doesn't require Ruby loaded **Changes:** - Create shared bash script in generator templates - Fix ReScript build to run from Rails root (cd into rails_root) - Fix pack generation to run from Rails root - Use proper path resolution with Rails root for all file checks - Replace Ruby scripts in both dummy apps with bash version - All three copies identical (generator template + 2 dummy apps) **Script Features:** - Finds Rails root by walking up directory tree - Detects ReScript config (bsconfig.json or rescript.json) - Runs ReScript builds from correct directory - Detects auto_load_bundle/components_subdirectory config - Generates packs when configured - Cross-platform package manager detection (yarn/npm) - Proper error handling and exit codes - Sets REACT_ON_RAILS_SKIP_VALIDATION for build context **Testing:** - Shellcheck passes with no warnings - Script executes successfully in non-pro dummy app - ReScript builds complete successfully - Pack generation runs successfully - All files have trailing newlines 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../base/base/bin/shakapacker-precompile-hook | 144 ++++++++++--- .../dummy/bin/shakapacker-precompile-hook | 191 +++++++++--------- spec/dummy/bin/shakapacker-precompile-hook | 185 +++++++++-------- 3 files changed, 322 insertions(+), 198 deletions(-) diff --git a/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook b/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook index 9e9632cc7a..93b8a7f2ba 100755 --- a/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook +++ b/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook @@ -1,30 +1,122 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - +#!/bin/sh # Shakapacker precompile hook for React on Rails # -# This script runs before webpack compilation to generate pack files -# for auto-bundled components. It's called automatically by Shakapacker -# when configured in config/shakapacker.yml: +# This script runs before webpack compilation to: +# 1. Build ReScript files (if configured) +# 2. Generate pack files for auto-bundled components +# +# It's called automatically by Shakapacker when configured in config/shakapacker.yml: # precompile_hook: 'bin/shakapacker-precompile-hook' # -# Emoji Scheme: -# 🔄 = Running/in-progress -# ✅ = Success -# ❌ = Error - -# Skip validation during precompile hook execution -# The hook runs early in the build process, potentially before full Rails initialization, -# and doesn't need package version validation since it's part of the build itself -ENV["REACT_ON_RAILS_SKIP_VALIDATION"] = "true" - -require_relative "../config/environment" - -begin - puts Rainbow("🔄 Running React on Rails precompile hook...").cyan - ReactOnRails::PacksGenerator.instance.generate_packs_if_stale -rescue StandardError => e - warn Rainbow("❌ Error in precompile hook: #{e.message}").red - warn e.backtrace.first(5).join("\n") - exit 1 -end +# See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md + +set -e + +# Find Rails root by walking upward looking for config/environment.rb +find_rails_root() { + dir="$PWD" + while [ "$dir" != "/" ]; do + if [ -f "$dir/config/environment.rb" ]; then + echo "$dir" + return 0 + fi + dir=$(dirname "$dir") + done + return 1 +} + +# Build ReScript if needed +build_rescript_if_needed() { + rails_root=$(find_rails_root) + if [ -z "$rails_root" ]; then + echo "⚠️ Warning: Could not find Rails root. Skipping ReScript build." + return 0 + fi + + # Check for both old (bsconfig.json) and new (rescript.json) config files + if [ ! -f "$rails_root/bsconfig.json" ] && [ ! -f "$rails_root/rescript.json" ]; then + return 0 + fi + + echo "🔧 Building ReScript..." + + # Change to Rails root to run build commands + cd "$rails_root" || return 1 + + # Cross-platform package manager detection + if command -v yarn >/dev/null 2>&1; then + if yarn build:rescript; then + echo "✅ ReScript build completed successfully" + return 0 + else + echo "❌ ReScript build failed" >&2 + exit 1 + fi + elif command -v npm >/dev/null 2>&1; then + if npm run build:rescript; then + echo "✅ ReScript build completed successfully" + return 0 + else + echo "❌ ReScript build failed" >&2 + exit 1 + fi + else + echo "⚠️ Warning: Neither yarn nor npm found. Skipping ReScript build." + return 0 + fi +} + +# Generate React on Rails packs if needed +generate_packs_if_needed() { + rails_root=$(find_rails_root) + if [ -z "$rails_root" ]; then + return 0 + fi + + # Check if React on Rails initializer exists + initializer_path="$rails_root/config/initializers/react_on_rails.rb" + if [ ! -f "$initializer_path" ]; then + return 0 + fi + + # Check if auto-pack generation is configured (ignore comments) + # Look for uncommented config.auto_load_bundle or config.components_subdirectory + if ! grep -q "^[[:space:]]*config\.auto_load_bundle[[:space:]]*=" "$initializer_path" && \ + ! grep -q "^[[:space:]]*config\.components_subdirectory[[:space:]]*=" "$initializer_path"; then + return 0 + fi + + echo "📦 Generating React on Rails packs..." + + # Check if bundle is available + if ! command -v bundle >/dev/null 2>&1; then + return 0 + fi + + # Change to Rails root + cd "$rails_root" || return 1 + + # Check if rake task exists + if ! bundle exec rails -T | grep -q "react_on_rails:generate_packs"; then + return 0 + fi + + # Skip validation during precompile hook execution + # The hook runs early in the build process and doesn't need package version validation + export REACT_ON_RAILS_SKIP_VALIDATION=true + + # Run pack generation + if bundle exec rails react_on_rails:generate_packs; then + echo "✅ Pack generation completed successfully" + return 0 + else + echo "❌ Pack generation failed" >&2 + exit 1 + fi +} + +# Main execution +build_rescript_if_needed +generate_packs_if_needed + +exit 0 diff --git a/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook b/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook index f42ee7769a..93b8a7f2ba 100755 --- a/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook +++ b/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook @@ -1,111 +1,122 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# Shakapacker precompile hook -# This script runs before Shakapacker compilation in both development and production. +#!/bin/sh +# Shakapacker precompile hook for React on Rails +# +# This script runs before webpack compilation to: +# 1. Build ReScript files (if configured) +# 2. Generate pack files for auto-bundled components +# +# It's called automatically by Shakapacker when configured in config/shakapacker.yml: +# precompile_hook: 'bin/shakapacker-precompile-hook' +# # See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md -require "fileutils" +set -e # Find Rails root by walking upward looking for config/environment.rb -def find_rails_root - dir = Dir.pwd - loop do - return dir if File.exist?(File.join(dir, "config", "environment.rb")) - - parent = File.dirname(dir) - return nil if parent == dir # Reached filesystem root - - dir = parent - end -end +find_rails_root() { + dir="$PWD" + while [ "$dir" != "/" ]; do + if [ -f "$dir/config/environment.rb" ]; then + echo "$dir" + return 0 + fi + dir=$(dirname "$dir") + done + return 1 +} # Build ReScript if needed -def build_rescript_if_needed - # Find Rails root directory - rails_root = find_rails_root - unless rails_root - warn "⚠️ Warning: Could not find Rails root. Skipping ReScript build." - return - end +build_rescript_if_needed() { + rails_root=$(find_rails_root) + if [ -z "$rails_root" ]; then + echo "⚠️ Warning: Could not find Rails root. Skipping ReScript build." + return 0 + fi # Check for both old (bsconfig.json) and new (rescript.json) config files - bsconfig_path = File.join(rails_root, "bsconfig.json") - rescript_config_path = File.join(rails_root, "rescript.json") - return unless File.exist?(bsconfig_path) || File.exist?(rescript_config_path) + if [ ! -f "$rails_root/bsconfig.json" ] && [ ! -f "$rails_root/rescript.json" ]; then + return 0 + fi + + echo "🔧 Building ReScript..." - puts "🔧 Building ReScript..." + # Change to Rails root to run build commands + cd "$rails_root" || return 1 # Cross-platform package manager detection - yarn_available = system("yarn", "--version", out: File::NULL, err: File::NULL) - npm_available = system("npm", "--version", out: File::NULL, err: File::NULL) - - # Run build command from Rails root directory - success = Dir.chdir(rails_root) do - if yarn_available - system("yarn", "build:rescript") - elsif npm_available - system("npm", "run", "build:rescript") + if command -v yarn >/dev/null 2>&1; then + if yarn build:rescript; then + echo "✅ ReScript build completed successfully" + return 0 else - warn "⚠️ Warning: Neither yarn nor npm found. Skipping ReScript build." - return false - end - end - - if success - puts "✅ ReScript build completed successfully" + echo "❌ ReScript build failed" >&2 + exit 1 + fi + elif command -v npm >/dev/null 2>&1; then + if npm run build:rescript; then + echo "✅ ReScript build completed successfully" + return 0 + else + echo "❌ ReScript build failed" >&2 + exit 1 + fi else - warn "❌ ReScript build failed" - exit 1 - end -end + echo "⚠️ Warning: Neither yarn nor npm found. Skipping ReScript build." + return 0 + fi +} # Generate React on Rails packs if needed -def generate_packs_if_needed - # Find Rails root directory - rails_root = find_rails_root - return unless rails_root +generate_packs_if_needed() { + rails_root=$(find_rails_root) + if [ -z "$rails_root" ]; then + return 0 + fi # Check if React on Rails initializer exists - initializer_path = File.join(rails_root, "config", "initializers", "react_on_rails.rb") - return unless File.exist?(initializer_path) - - # Check if auto-pack generation is configured (match actual config assignments, not comments) - config_file = File.read(initializer_path) - # Match uncommented configuration lines only (lines not starting with #) - has_auto_load = config_file =~ /^\s*(?!#).*config\.auto_load_bundle\s*=/ - has_components_subdir = config_file =~ /^\s*(?!#).*config\.components_subdirectory\s*=/ - return unless has_auto_load || has_components_subdir - - puts "📦 Generating React on Rails packs..." - - # Cross-platform bundle availability check - bundle_available = system("bundle", "--version", out: File::NULL, err: File::NULL) - return unless bundle_available - - # Check if rake task exists (use array form for security) - task_list = IO.popen(["bundle", "exec", "rails", "-T"], err: %i[child out], &:read) - return unless task_list.include?("react_on_rails:generate_packs") - - # Use array form for better cross-platform support - success = system("bundle", "exec", "rails", "react_on_rails:generate_packs") - - if success - puts "✅ Pack generation completed successfully" + initializer_path="$rails_root/config/initializers/react_on_rails.rb" + if [ ! -f "$initializer_path" ]; then + return 0 + fi + + # Check if auto-pack generation is configured (ignore comments) + # Look for uncommented config.auto_load_bundle or config.components_subdirectory + if ! grep -q "^[[:space:]]*config\.auto_load_bundle[[:space:]]*=" "$initializer_path" && \ + ! grep -q "^[[:space:]]*config\.components_subdirectory[[:space:]]*=" "$initializer_path"; then + return 0 + fi + + echo "📦 Generating React on Rails packs..." + + # Check if bundle is available + if ! command -v bundle >/dev/null 2>&1; then + return 0 + fi + + # Change to Rails root + cd "$rails_root" || return 1 + + # Check if rake task exists + if ! bundle exec rails -T | grep -q "react_on_rails:generate_packs"; then + return 0 + fi + + # Skip validation during precompile hook execution + # The hook runs early in the build process and doesn't need package version validation + export REACT_ON_RAILS_SKIP_VALIDATION=true + + # Run pack generation + if bundle exec rails react_on_rails:generate_packs; then + echo "✅ Pack generation completed successfully" + return 0 else - warn "❌ Pack generation failed" + echo "❌ Pack generation failed" >&2 exit 1 - end -end + fi +} # Main execution -begin - build_rescript_if_needed - generate_packs_if_needed - - exit 0 -rescue StandardError => e - warn "❌ Precompile hook failed: #{e.message}" - warn e.backtrace.join("\n") - exit 1 -end +build_rescript_if_needed +generate_packs_if_needed + +exit 0 diff --git a/spec/dummy/bin/shakapacker-precompile-hook b/spec/dummy/bin/shakapacker-precompile-hook index b650b02db5..93b8a7f2ba 100755 --- a/spec/dummy/bin/shakapacker-precompile-hook +++ b/spec/dummy/bin/shakapacker-precompile-hook @@ -1,101 +1,122 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# Shakapacker precompile hook -# This script runs before Shakapacker compilation in both development and production. +#!/bin/sh +# Shakapacker precompile hook for React on Rails +# +# This script runs before webpack compilation to: +# 1. Build ReScript files (if configured) +# 2. Generate pack files for auto-bundled components +# +# It's called automatically by Shakapacker when configured in config/shakapacker.yml: +# precompile_hook: 'bin/shakapacker-precompile-hook' +# # See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md -require "fileutils" +set -e # Find Rails root by walking upward looking for config/environment.rb -def find_rails_root - dir = Dir.pwd - loop do - return dir if File.exist?(File.join(dir, "config", "environment.rb")) - - parent = File.dirname(dir) - return nil if parent == dir # Reached filesystem root - - dir = parent - end -end +find_rails_root() { + dir="$PWD" + while [ "$dir" != "/" ]; do + if [ -f "$dir/config/environment.rb" ]; then + echo "$dir" + return 0 + fi + dir=$(dirname "$dir") + done + return 1 +} # Build ReScript if needed -def build_rescript_if_needed +build_rescript_if_needed() { + rails_root=$(find_rails_root) + if [ -z "$rails_root" ]; then + echo "⚠️ Warning: Could not find Rails root. Skipping ReScript build." + return 0 + fi + # Check for both old (bsconfig.json) and new (rescript.json) config files - return unless File.exist?("bsconfig.json") || File.exist?("rescript.json") + if [ ! -f "$rails_root/bsconfig.json" ] && [ ! -f "$rails_root/rescript.json" ]; then + return 0 + fi - puts "🔧 Building ReScript..." + echo "🔧 Building ReScript..." + + # Change to Rails root to run build commands + cd "$rails_root" || return 1 # Cross-platform package manager detection - yarn_available = system("yarn", "--version", out: File::NULL, err: File::NULL) - npm_available = system("npm", "--version", out: File::NULL, err: File::NULL) - - success = if yarn_available - system("yarn", "build:rescript") - elsif npm_available - system("npm", "run", "build:rescript") - else - warn "⚠️ Warning: Neither yarn nor npm found. Skipping ReScript build." - return - end - - if success - puts "✅ ReScript build completed successfully" + if command -v yarn >/dev/null 2>&1; then + if yarn build:rescript; then + echo "✅ ReScript build completed successfully" + return 0 + else + echo "❌ ReScript build failed" >&2 + exit 1 + fi + elif command -v npm >/dev/null 2>&1; then + if npm run build:rescript; then + echo "✅ ReScript build completed successfully" + return 0 + else + echo "❌ ReScript build failed" >&2 + exit 1 + fi else - warn "❌ ReScript build failed" - exit 1 - end -end + echo "⚠️ Warning: Neither yarn nor npm found. Skipping ReScript build." + return 0 + fi +} # Generate React on Rails packs if needed -# rubocop:disable Metrics/CyclomaticComplexity -def generate_packs_if_needed - # Find Rails root directory - rails_root = find_rails_root - return unless rails_root +generate_packs_if_needed() { + rails_root=$(find_rails_root) + if [ -z "$rails_root" ]; then + return 0 + fi # Check if React on Rails initializer exists - initializer_path = File.join(rails_root, "config", "initializers", "react_on_rails.rb") - return unless File.exist?(initializer_path) - - # Check if auto-pack generation is configured (match actual config assignments, not comments) - config_file = File.read(initializer_path) - # Match uncommented configuration lines only (lines not starting with #) - has_auto_load = config_file =~ /^\s*(?!#).*config\.auto_load_bundle\s*=/ - has_components_subdir = config_file =~ /^\s*(?!#).*config\.components_subdirectory\s*=/ - return unless has_auto_load || has_components_subdir - - puts "📦 Generating React on Rails packs..." - - # Cross-platform bundle availability check - bundle_available = system("bundle", "--version", out: File::NULL, err: File::NULL) - return unless bundle_available - - # Check if rake task exists (use array form for security) - task_list = IO.popen(["bundle", "exec", "rails", "-T"], err: [:child, :out], &:read) - return unless task_list.include?("react_on_rails:generate_packs") - - # Use array form for better cross-platform support - success = system("bundle", "exec", "rails", "react_on_rails:generate_packs") - - if success - puts "✅ Pack generation completed successfully" + initializer_path="$rails_root/config/initializers/react_on_rails.rb" + if [ ! -f "$initializer_path" ]; then + return 0 + fi + + # Check if auto-pack generation is configured (ignore comments) + # Look for uncommented config.auto_load_bundle or config.components_subdirectory + if ! grep -q "^[[:space:]]*config\.auto_load_bundle[[:space:]]*=" "$initializer_path" && \ + ! grep -q "^[[:space:]]*config\.components_subdirectory[[:space:]]*=" "$initializer_path"; then + return 0 + fi + + echo "📦 Generating React on Rails packs..." + + # Check if bundle is available + if ! command -v bundle >/dev/null 2>&1; then + return 0 + fi + + # Change to Rails root + cd "$rails_root" || return 1 + + # Check if rake task exists + if ! bundle exec rails -T | grep -q "react_on_rails:generate_packs"; then + return 0 + fi + + # Skip validation during precompile hook execution + # The hook runs early in the build process and doesn't need package version validation + export REACT_ON_RAILS_SKIP_VALIDATION=true + + # Run pack generation + if bundle exec rails react_on_rails:generate_packs; then + echo "✅ Pack generation completed successfully" + return 0 else - warn "❌ Pack generation failed" + echo "❌ Pack generation failed" >&2 exit 1 - end -end -# rubocop:enable Metrics/CyclomaticComplexity + fi +} # Main execution -begin - build_rescript_if_needed - generate_packs_if_needed - - exit 0 -rescue StandardError => e - warn "❌ Precompile hook failed: #{e.message}" - warn e.backtrace.join("\n") - exit 1 -end +build_rescript_if_needed +generate_packs_if_needed + +exit 0 From 641132ca717bf5a8deafff91130ee7799059efa4 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sun, 16 Nov 2025 20:51:46 -1000 Subject: [PATCH 05/12] Refactor shakapacker-precompile-hook to use shared implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidate duplicate precompile hook logic across OSS dummy app, Pro dummy app, and generator template into a single shared Ruby implementation. Changes: - Create lib/tasks/precompile/shakapacker_precompile_hook_shared.rb with common logic - Update spec/dummy/bin/shakapacker-precompile-hook to load shared implementation - Update react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook to load shared implementation - Simplify generator template to use Ruby instead of shell script (removes ReScript build logic) Benefits: - Eliminates 342 lines of duplicate code across 3 files - Easier to maintain and update precompile hook logic in one place - Consistent behavior across all environments - Better error handling with Ruby exceptions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../base/base/bin/shakapacker-precompile-hook | 148 +++++------------- .../shakapacker_precompile_hook_shared.rb | 112 +++++++++++++ .../dummy/bin/shakapacker-precompile-hook | 133 ++-------------- spec/dummy/bin/shakapacker-precompile-hook | 133 ++-------------- 4 files changed, 184 insertions(+), 342 deletions(-) create mode 100755 lib/tasks/precompile/shakapacker_precompile_hook_shared.rb diff --git a/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook b/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook index 93b8a7f2ba..ba4158b2b0 100755 --- a/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook +++ b/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook @@ -1,122 +1,58 @@ -#!/bin/sh +#!/usr/bin/env ruby +# frozen_string_literal: true + # Shakapacker precompile hook for React on Rails # -# This script runs before webpack compilation to: -# 1. Build ReScript files (if configured) -# 2. Generate pack files for auto-bundled components +# This script runs before webpack compilation to generate pack files +# for auto-bundled components. # # It's called automatically by Shakapacker when configured in config/shakapacker.yml: # precompile_hook: 'bin/shakapacker-precompile-hook' # # See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md -set -e +require "fileutils" # Find Rails root by walking upward looking for config/environment.rb -find_rails_root() { - dir="$PWD" - while [ "$dir" != "/" ]; do - if [ -f "$dir/config/environment.rb" ]; then - echo "$dir" - return 0 - fi - dir=$(dirname "$dir") - done - return 1 -} - -# Build ReScript if needed -build_rescript_if_needed() { - rails_root=$(find_rails_root) - if [ -z "$rails_root" ]; then - echo "⚠️ Warning: Could not find Rails root. Skipping ReScript build." - return 0 - fi - - # Check for both old (bsconfig.json) and new (rescript.json) config files - if [ ! -f "$rails_root/bsconfig.json" ] && [ ! -f "$rails_root/rescript.json" ]; then - return 0 - fi - - echo "🔧 Building ReScript..." +def find_rails_root + dir = Dir.pwd + while dir != "/" + return dir if File.exist?(File.join(dir, "config", "environment.rb")) - # Change to Rails root to run build commands - cd "$rails_root" || return 1 - - # Cross-platform package manager detection - if command -v yarn >/dev/null 2>&1; then - if yarn build:rescript; then - echo "✅ ReScript build completed successfully" - return 0 - else - echo "❌ ReScript build failed" >&2 - exit 1 - fi - elif command -v npm >/dev/null 2>&1; then - if npm run build:rescript; then - echo "✅ ReScript build completed successfully" - return 0 - else - echo "❌ ReScript build failed" >&2 - exit 1 - fi - else - echo "⚠️ Warning: Neither yarn nor npm found. Skipping ReScript build." - return 0 - fi -} + dir = File.dirname(dir) + end + nil +end # Generate React on Rails packs if needed -generate_packs_if_needed() { - rails_root=$(find_rails_root) - if [ -z "$rails_root" ]; then - return 0 - fi - - # Check if React on Rails initializer exists - initializer_path="$rails_root/config/initializers/react_on_rails.rb" - if [ ! -f "$initializer_path" ]; then - return 0 - fi - - # Check if auto-pack generation is configured (ignore comments) - # Look for uncommented config.auto_load_bundle or config.components_subdirectory - if ! grep -q "^[[:space:]]*config\.auto_load_bundle[[:space:]]*=" "$initializer_path" && \ - ! grep -q "^[[:space:]]*config\.components_subdirectory[[:space:]]*=" "$initializer_path"; then - return 0 - fi - - echo "📦 Generating React on Rails packs..." - - # Check if bundle is available - if ! command -v bundle >/dev/null 2>&1; then - return 0 - fi - - # Change to Rails root - cd "$rails_root" || return 1 - - # Check if rake task exists - if ! bundle exec rails -T | grep -q "react_on_rails:generate_packs"; then - return 0 - fi - - # Skip validation during precompile hook execution - # The hook runs early in the build process and doesn't need package version validation - export REACT_ON_RAILS_SKIP_VALIDATION=true - - # Run pack generation - if bundle exec rails react_on_rails:generate_packs; then - echo "✅ Pack generation completed successfully" - return 0 - else - echo "❌ Pack generation failed" >&2 - exit 1 - fi -} +def generate_packs_if_needed + rails_root = find_rails_root + return unless rails_root + + initializer_path = File.join(rails_root, "config", "initializers", "react_on_rails.rb") + return unless File.exist?(initializer_path) + + # Check if auto-pack generation is configured + initializer_content = File.read(initializer_path) + return unless initializer_content.match?(/^\s*config\.auto_load_bundle\s*=/) || + initializer_content.match?(/^\s*config\.components_subdirectory\s*=/) + + puts "📦 Generating React on Rails packs..." + + Dir.chdir(rails_root) do + # Skip validation during precompile hook execution + ENV["REACT_ON_RAILS_SKIP_VALIDATION"] = "true" + + # Run pack generation + system("bundle", "exec", "rails", "react_on_rails:generate_packs", exception: true) + puts "✅ Pack generation completed successfully" + end +rescue Errno::ENOENT => e + warn "⚠️ Warning: #{e.message}" +rescue StandardError => e + warn "❌ Pack generation failed: #{e.message}" + exit 1 +end # Main execution -build_rescript_if_needed generate_packs_if_needed - -exit 0 diff --git a/lib/tasks/precompile/shakapacker_precompile_hook_shared.rb b/lib/tasks/precompile/shakapacker_precompile_hook_shared.rb new file mode 100755 index 0000000000..684e13a3ee --- /dev/null +++ b/lib/tasks/precompile/shakapacker_precompile_hook_shared.rb @@ -0,0 +1,112 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Shakapacker precompile hook for React on Rails - Shared Implementation +# +# This is the shared implementation used by both test dummy apps: +# - spec/dummy/bin/shakapacker-precompile-hook +# - react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook +# +# This script runs before webpack compilation to: +# 1. Build ReScript files (if configured) +# 2. Generate pack files for auto-bundled components +# +# See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md + +require "fileutils" +require "json" + +# Find Rails root by walking upward looking for config/environment.rb +def find_rails_root + dir = Dir.pwd + while dir != "/" + return dir if File.exist?(File.join(dir, "config", "environment.rb")) + + dir = File.dirname(dir) + end + nil +end + +# Build ReScript if needed +# rubocop:disable Metrics/CyclomaticComplexity +def build_rescript_if_needed + rails_root = find_rails_root + unless rails_root + warn "⚠️ Warning: Could not find Rails root. Skipping ReScript build." + return + end + + # Check for both old (bsconfig.json) and new (rescript.json) config files + return unless File.exist?(File.join(rails_root, "bsconfig.json")) || + File.exist?(File.join(rails_root, "rescript.json")) + + puts "🔧 Building ReScript..." + + # Validate that build:rescript script exists in package.json + package_json_path = File.join(rails_root, "package.json") + unless File.exist?(package_json_path) + warn "⚠️ Warning: package.json not found. Skipping ReScript build." + return + end + + package_json = JSON.parse(File.read(package_json_path)) + unless package_json.dig("scripts", "build:rescript") + warn "⚠️ Warning: ReScript config found but no build:rescript script in package.json" + warn " Add a build:rescript script to your package.json to enable ReScript builds" + return + end + + Dir.chdir(rails_root) do + # Cross-platform package manager detection + if system("which yarn > /dev/null 2>&1") + system("yarn", "build:rescript", exception: true) + elsif system("which npm > /dev/null 2>&1") + system("npm", "run", "build:rescript", exception: true) + else + warn "⚠️ Warning: Neither yarn nor npm found. Skipping ReScript build." + return + end + + puts "✅ ReScript build completed successfully" + end +rescue StandardError => e + warn "❌ ReScript build failed: #{e.message}" + exit 1 +end +# rubocop:enable Metrics/CyclomaticComplexity + +# Generate React on Rails packs if needed +def generate_packs_if_needed + rails_root = find_rails_root + return unless rails_root + + initializer_path = File.join(rails_root, "config", "initializers", "react_on_rails.rb") + return unless File.exist?(initializer_path) + + # Check if auto-pack generation is configured + initializer_content = File.read(initializer_path) + return unless initializer_content.match?(/^\s*config\.auto_load_bundle\s*=/) || + initializer_content.match?(/^\s*config\.components_subdirectory\s*=/) + + puts "📦 Generating React on Rails packs..." + + Dir.chdir(rails_root) do + # Skip validation during precompile hook execution + ENV["REACT_ON_RAILS_SKIP_VALIDATION"] = "true" + + # Run pack generation + system("bundle", "exec", "rails", "react_on_rails:generate_packs", exception: true) + puts "✅ Pack generation completed successfully" + end +rescue Errno::ENOENT => e + warn "⚠️ Warning: #{e.message}" +rescue StandardError => e + warn "❌ Pack generation failed: #{e.message}" + exit 1 +end + +# Main execution (only if run directly, not when required) +if __FILE__ == $PROGRAM_NAME + build_rescript_if_needed + generate_packs_if_needed +end diff --git a/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook b/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook index 93b8a7f2ba..dd5e3eff59 100755 --- a/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook +++ b/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook @@ -1,122 +1,19 @@ -#!/bin/sh -# Shakapacker precompile hook for React on Rails -# -# This script runs before webpack compilation to: -# 1. Build ReScript files (if configured) -# 2. Generate pack files for auto-bundled components -# -# It's called automatically by Shakapacker when configured in config/shakapacker.yml: -# precompile_hook: 'bin/shakapacker-precompile-hook' -# -# See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md - -set -e - -# Find Rails root by walking upward looking for config/environment.rb -find_rails_root() { - dir="$PWD" - while [ "$dir" != "/" ]; do - if [ -f "$dir/config/environment.rb" ]; then - echo "$dir" - return 0 - fi - dir=$(dirname "$dir") - done - return 1 -} - -# Build ReScript if needed -build_rescript_if_needed() { - rails_root=$(find_rails_root) - if [ -z "$rails_root" ]; then - echo "⚠️ Warning: Could not find Rails root. Skipping ReScript build." - return 0 - fi - - # Check for both old (bsconfig.json) and new (rescript.json) config files - if [ ! -f "$rails_root/bsconfig.json" ] && [ ! -f "$rails_root/rescript.json" ]; then - return 0 - fi - - echo "🔧 Building ReScript..." - - # Change to Rails root to run build commands - cd "$rails_root" || return 1 +#!/usr/bin/env ruby +# frozen_string_literal: true - # Cross-platform package manager detection - if command -v yarn >/dev/null 2>&1; then - if yarn build:rescript; then - echo "✅ ReScript build completed successfully" - return 0 - else - echo "❌ ReScript build failed" >&2 - exit 1 - fi - elif command -v npm >/dev/null 2>&1; then - if npm run build:rescript; then - echo "✅ ReScript build completed successfully" - return 0 - else - echo "❌ ReScript build failed" >&2 - exit 1 - fi - else - echo "⚠️ Warning: Neither yarn nor npm found. Skipping ReScript build." - return 0 - fi -} - -# Generate React on Rails packs if needed -generate_packs_if_needed() { - rails_root=$(find_rails_root) - if [ -z "$rails_root" ]; then - return 0 - fi - - # Check if React on Rails initializer exists - initializer_path="$rails_root/config/initializers/react_on_rails.rb" - if [ ! -f "$initializer_path" ]; then - return 0 - fi - - # Check if auto-pack generation is configured (ignore comments) - # Look for uncommented config.auto_load_bundle or config.components_subdirectory - if ! grep -q "^[[:space:]]*config\.auto_load_bundle[[:space:]]*=" "$initializer_path" && \ - ! grep -q "^[[:space:]]*config\.components_subdirectory[[:space:]]*=" "$initializer_path"; then - return 0 - fi - - echo "📦 Generating React on Rails packs..." - - # Check if bundle is available - if ! command -v bundle >/dev/null 2>&1; then - return 0 - fi - - # Change to Rails root - cd "$rails_root" || return 1 - - # Check if rake task exists - if ! bundle exec rails -T | grep -q "react_on_rails:generate_packs"; then - return 0 - fi - - # Skip validation during precompile hook execution - # The hook runs early in the build process and doesn't need package version validation - export REACT_ON_RAILS_SKIP_VALIDATION=true +# Shakapacker precompile hook for React on Rails Pro test dummy app +# +# This script loads the shared precompile hook implementation to avoid duplication. +# The shared implementation is maintained in lib/tasks/precompile/shakapacker_precompile_hook_shared.rb - # Run pack generation - if bundle exec rails react_on_rails:generate_packs; then - echo "✅ Pack generation completed successfully" - return 0 - else - echo "❌ Pack generation failed" >&2 - exit 1 - fi -} +# Find the gem root directory (four levels up from react_on_rails_pro/spec/dummy/bin) +gem_root = File.expand_path("../../../..", __dir__) +shared_hook = File.join(gem_root, "lib", "tasks", "precompile", "shakapacker_precompile_hook_shared.rb") -# Main execution -build_rescript_if_needed -generate_packs_if_needed +unless File.exist?(shared_hook) + warn "❌ Error: Shared precompile hook not found at #{shared_hook}" + exit 1 +end -exit 0 +# Load and execute the shared hook +load shared_hook diff --git a/spec/dummy/bin/shakapacker-precompile-hook b/spec/dummy/bin/shakapacker-precompile-hook index 93b8a7f2ba..9dd2e3f562 100755 --- a/spec/dummy/bin/shakapacker-precompile-hook +++ b/spec/dummy/bin/shakapacker-precompile-hook @@ -1,122 +1,19 @@ -#!/bin/sh -# Shakapacker precompile hook for React on Rails -# -# This script runs before webpack compilation to: -# 1. Build ReScript files (if configured) -# 2. Generate pack files for auto-bundled components -# -# It's called automatically by Shakapacker when configured in config/shakapacker.yml: -# precompile_hook: 'bin/shakapacker-precompile-hook' -# -# See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md - -set -e - -# Find Rails root by walking upward looking for config/environment.rb -find_rails_root() { - dir="$PWD" - while [ "$dir" != "/" ]; do - if [ -f "$dir/config/environment.rb" ]; then - echo "$dir" - return 0 - fi - dir=$(dirname "$dir") - done - return 1 -} - -# Build ReScript if needed -build_rescript_if_needed() { - rails_root=$(find_rails_root) - if [ -z "$rails_root" ]; then - echo "⚠️ Warning: Could not find Rails root. Skipping ReScript build." - return 0 - fi - - # Check for both old (bsconfig.json) and new (rescript.json) config files - if [ ! -f "$rails_root/bsconfig.json" ] && [ ! -f "$rails_root/rescript.json" ]; then - return 0 - fi - - echo "🔧 Building ReScript..." - - # Change to Rails root to run build commands - cd "$rails_root" || return 1 +#!/usr/bin/env ruby +# frozen_string_literal: true - # Cross-platform package manager detection - if command -v yarn >/dev/null 2>&1; then - if yarn build:rescript; then - echo "✅ ReScript build completed successfully" - return 0 - else - echo "❌ ReScript build failed" >&2 - exit 1 - fi - elif command -v npm >/dev/null 2>&1; then - if npm run build:rescript; then - echo "✅ ReScript build completed successfully" - return 0 - else - echo "❌ ReScript build failed" >&2 - exit 1 - fi - else - echo "⚠️ Warning: Neither yarn nor npm found. Skipping ReScript build." - return 0 - fi -} - -# Generate React on Rails packs if needed -generate_packs_if_needed() { - rails_root=$(find_rails_root) - if [ -z "$rails_root" ]; then - return 0 - fi - - # Check if React on Rails initializer exists - initializer_path="$rails_root/config/initializers/react_on_rails.rb" - if [ ! -f "$initializer_path" ]; then - return 0 - fi - - # Check if auto-pack generation is configured (ignore comments) - # Look for uncommented config.auto_load_bundle or config.components_subdirectory - if ! grep -q "^[[:space:]]*config\.auto_load_bundle[[:space:]]*=" "$initializer_path" && \ - ! grep -q "^[[:space:]]*config\.components_subdirectory[[:space:]]*=" "$initializer_path"; then - return 0 - fi - - echo "📦 Generating React on Rails packs..." - - # Check if bundle is available - if ! command -v bundle >/dev/null 2>&1; then - return 0 - fi - - # Change to Rails root - cd "$rails_root" || return 1 - - # Check if rake task exists - if ! bundle exec rails -T | grep -q "react_on_rails:generate_packs"; then - return 0 - fi - - # Skip validation during precompile hook execution - # The hook runs early in the build process and doesn't need package version validation - export REACT_ON_RAILS_SKIP_VALIDATION=true +# Shakapacker precompile hook for React on Rails test dummy app +# +# This script loads the shared precompile hook implementation to avoid duplication. +# The shared implementation is maintained in lib/tasks/precompile/shakapacker_precompile_hook_shared.rb - # Run pack generation - if bundle exec rails react_on_rails:generate_packs; then - echo "✅ Pack generation completed successfully" - return 0 - else - echo "❌ Pack generation failed" >&2 - exit 1 - fi -} +# Find the gem root directory (three levels up from spec/dummy/bin) +gem_root = File.expand_path("../../..", __dir__) +shared_hook = File.join(gem_root, "lib", "tasks", "precompile", "shakapacker_precompile_hook_shared.rb") -# Main execution -build_rescript_if_needed -generate_packs_if_needed +unless File.exist?(shared_hook) + warn "❌ Error: Shared precompile hook not found at #{shared_hook}" + exit 1 +end -exit 0 +# Load and execute the shared hook +load shared_hook From 64d14e8e46edd7738988e663a1145bb2b6d84851 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sun, 16 Nov 2025 22:08:10 -1000 Subject: [PATCH 06/12] Make generator template precompile hook use shared implementation when available MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the generator template to intelligently use the shared precompile hook implementation when running in the gem's development/test context, while maintaining a self-contained fallback for generated user applications. This provides the best of both worlds: - Test dummy apps and generator template all use the same shared code - Generated user apps have a standalone copy that works independently - Easier to maintain and update the precompile hook logic The hook now tries to load the shared implementation from the gem first, and falls back to its inline implementation if the gem isn't available or accessible. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../base/base/bin/shakapacker-precompile-hook | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook b/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook index ba4158b2b0..e1e47f319a 100755 --- a/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook +++ b/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook @@ -11,6 +11,22 @@ # # See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md +# Try to use shared implementation from gem if available (during development/testing) +# This allows the gem's test apps and generated apps to share the same implementation +begin + require "react_on_rails" + gem_root = Gem.loaded_specs["react_on_rails"]&.gem_dir + shared_hook = File.join(gem_root, "lib", "tasks", "precompile", "shakapacker_precompile_hook_shared.rb") if gem_root + + if shared_hook && File.exist?(shared_hook) + load shared_hook + exit 0 + end +rescue LoadError, StandardError + # If we can't load from gem, fall through to inline implementation +end + +# Inline implementation for generated apps (when gem source isn't available) require "fileutils" # Find Rails root by walking upward looking for config/environment.rb From fc024b644158b2fb79edb3c0e79c2d431a5401ad Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 17 Nov 2025 16:39:01 -1000 Subject: [PATCH 07/12] Move shared precompile hook to spec/support and simplify generator template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address feedback to properly organize the precompile hook code: 1. Move shared implementation to spec/support/ (test-only location) - Renamed: lib/tasks/precompile/shakapacker_precompile_hook_shared.rb - To: spec/support/shakapacker_precompile_hook_shared.rb - This makes it clear the shared code is only for test dummy apps 2. Simplify generator template to be standalone - Remove logic to load shared implementation from gem - Generator template is for newly created production apps - Should be simple, self-contained, and production-ready - No references to development/testing concerns 3. Update test dummy apps to reference new location - spec/dummy/bin/shakapacker-precompile-hook - react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook - Both now load from spec/support/ The generator template is now clean and focused on production use cases, while test dummy apps share common implementation from the test support directory. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../base/base/bin/shakapacker-precompile-hook | 16 ---------------- .../spec/dummy/bin/shakapacker-precompile-hook | 6 +++--- spec/dummy/bin/shakapacker-precompile-hook | 6 +++--- .../shakapacker_precompile_hook_shared.rb | 0 4 files changed, 6 insertions(+), 22 deletions(-) rename {lib/tasks/precompile => spec/support}/shakapacker_precompile_hook_shared.rb (100%) diff --git a/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook b/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook index e1e47f319a..ba4158b2b0 100755 --- a/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook +++ b/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook @@ -11,22 +11,6 @@ # # See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md -# Try to use shared implementation from gem if available (during development/testing) -# This allows the gem's test apps and generated apps to share the same implementation -begin - require "react_on_rails" - gem_root = Gem.loaded_specs["react_on_rails"]&.gem_dir - shared_hook = File.join(gem_root, "lib", "tasks", "precompile", "shakapacker_precompile_hook_shared.rb") if gem_root - - if shared_hook && File.exist?(shared_hook) - load shared_hook - exit 0 - end -rescue LoadError, StandardError - # If we can't load from gem, fall through to inline implementation -end - -# Inline implementation for generated apps (when gem source isn't available) require "fileutils" # Find Rails root by walking upward looking for config/environment.rb diff --git a/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook b/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook index dd5e3eff59..c94e8681d1 100755 --- a/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook +++ b/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook @@ -3,12 +3,12 @@ # Shakapacker precompile hook for React on Rails Pro test dummy app # -# This script loads the shared precompile hook implementation to avoid duplication. -# The shared implementation is maintained in lib/tasks/precompile/shakapacker_precompile_hook_shared.rb +# This script loads the shared test helper implementation. +# For production apps, use the generator template which includes a standalone implementation. # Find the gem root directory (four levels up from react_on_rails_pro/spec/dummy/bin) gem_root = File.expand_path("../../../..", __dir__) -shared_hook = File.join(gem_root, "lib", "tasks", "precompile", "shakapacker_precompile_hook_shared.rb") +shared_hook = File.join(gem_root, "spec", "support", "shakapacker_precompile_hook_shared.rb") unless File.exist?(shared_hook) warn "❌ Error: Shared precompile hook not found at #{shared_hook}" diff --git a/spec/dummy/bin/shakapacker-precompile-hook b/spec/dummy/bin/shakapacker-precompile-hook index 9dd2e3f562..a7a4abf408 100755 --- a/spec/dummy/bin/shakapacker-precompile-hook +++ b/spec/dummy/bin/shakapacker-precompile-hook @@ -3,12 +3,12 @@ # Shakapacker precompile hook for React on Rails test dummy app # -# This script loads the shared precompile hook implementation to avoid duplication. -# The shared implementation is maintained in lib/tasks/precompile/shakapacker_precompile_hook_shared.rb +# This script loads the shared test helper implementation. +# For production apps, use the generator template which includes a standalone implementation. # Find the gem root directory (three levels up from spec/dummy/bin) gem_root = File.expand_path("../../..", __dir__) -shared_hook = File.join(gem_root, "lib", "tasks", "precompile", "shakapacker_precompile_hook_shared.rb") +shared_hook = File.join(gem_root, "spec", "support", "shakapacker_precompile_hook_shared.rb") unless File.exist?(shared_hook) warn "❌ Error: Shared precompile hook not found at #{shared_hook}" diff --git a/lib/tasks/precompile/shakapacker_precompile_hook_shared.rb b/spec/support/shakapacker_precompile_hook_shared.rb similarity index 100% rename from lib/tasks/precompile/shakapacker_precompile_hook_shared.rb rename to spec/support/shakapacker_precompile_hook_shared.rb From ab317e2bc5eebb421959ef32a236d545598800e5 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 18 Nov 2025 15:04:04 -1000 Subject: [PATCH 08/12] Simplify generator template precompile hook to minimal bash script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace overly complicated 58-line Ruby implementation with a simple 22-line bash script that just checks for auto_load_bundle configuration and runs the rake task if needed. The bash script is: - Easier to read and understand - Fewer dependencies (no Ruby parsing, no fileutils) - Standard shell script that works everywhere - Does exactly what's needed, nothing more Perfect for newly generated production apps. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../base/base/bin/shakapacker-precompile-hook | 64 ++++--------------- 1 file changed, 14 insertions(+), 50 deletions(-) diff --git a/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook b/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook index ba4158b2b0..82d1ac939d 100755 --- a/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook +++ b/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook @@ -1,58 +1,22 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - +#!/bin/sh # Shakapacker precompile hook for React on Rails # # This script runs before webpack compilation to generate pack files -# for auto-bundled components. -# -# It's called automatically by Shakapacker when configured in config/shakapacker.yml: -# precompile_hook: 'bin/shakapacker-precompile-hook' +# for auto-bundled components (if configured). # # See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md -require "fileutils" - -# Find Rails root by walking upward looking for config/environment.rb -def find_rails_root - dir = Dir.pwd - while dir != "/" - return dir if File.exist?(File.join(dir, "config", "environment.rb")) - - dir = File.dirname(dir) - end - nil -end - -# Generate React on Rails packs if needed -def generate_packs_if_needed - rails_root = find_rails_root - return unless rails_root - - initializer_path = File.join(rails_root, "config", "initializers", "react_on_rails.rb") - return unless File.exist?(initializer_path) - - # Check if auto-pack generation is configured - initializer_content = File.read(initializer_path) - return unless initializer_content.match?(/^\s*config\.auto_load_bundle\s*=/) || - initializer_content.match?(/^\s*config\.components_subdirectory\s*=/) - - puts "📦 Generating React on Rails packs..." - - Dir.chdir(rails_root) do - # Skip validation during precompile hook execution - ENV["REACT_ON_RAILS_SKIP_VALIDATION"] = "true" +set -e - # Run pack generation - system("bundle", "exec", "rails", "react_on_rails:generate_packs", exception: true) - puts "✅ Pack generation completed successfully" - end -rescue Errno::ENOENT => e - warn "⚠️ Warning: #{e.message}" -rescue StandardError => e - warn "❌ Pack generation failed: #{e.message}" - exit 1 -end +# Skip validation during precompile hook execution +export REACT_ON_RAILS_SKIP_VALIDATION=true -# Main execution -generate_packs_if_needed +# Generate packs if React on Rails is configured with auto_load_bundle +if [ -f "config/initializers/react_on_rails.rb" ]; then + if grep -q "config\.auto_load_bundle" config/initializers/react_on_rails.rb || \ + grep -q "config\.components_subdirectory" config/initializers/react_on_rails.rb; then + echo "📦 Generating React on Rails packs..." + bundle exec rails react_on_rails:generate_packs + echo "✅ Pack generation completed successfully" + fi +fi From 278c93210a90d0d0ad8b8efc1721ab5c2f52bdc2 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 18 Nov 2025 15:07:15 -1000 Subject: [PATCH 09/12] Improve precompile hook error handling and config detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Key improvements to shakapacker precompile hook: 1. **Better error messages for ReScript builds** - Fail fast when ReScript config exists but package.json is missing - Fail fast when build:rescript script is not defined - Provide actionable error messages with exact fixes needed - Add JSON parse error handling 2. **More robust config detection** - Improved regex to properly ignore commented configuration lines - Allow flexible spacing in config assignments - Prevent false positives from commented-out config 3. **Consistent error handling** - Changed warnings to errors when builds are required but missing - Exit with status 1 on all failure scenarios - Better distinction between optional vs required failures Addresses code review feedback on error handling and regex robustness. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../base/base/bin/shakapacker-precompile-hook | 5 ++-- .../shakapacker_precompile_hook_shared.rb | 29 ++++++++++++------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook b/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook index 82d1ac939d..ccd8ace38c 100755 --- a/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook +++ b/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook @@ -13,8 +13,9 @@ export REACT_ON_RAILS_SKIP_VALIDATION=true # Generate packs if React on Rails is configured with auto_load_bundle if [ -f "config/initializers/react_on_rails.rb" ]; then - if grep -q "config\.auto_load_bundle" config/initializers/react_on_rails.rb || \ - grep -q "config\.components_subdirectory" config/initializers/react_on_rails.rb; then + # Check for uncommented config lines (ignore lines starting with # after optional whitespace) + if grep -E "^[[:space:]]*[^#]*config\.auto_load_bundle[[:space:]]*=" config/initializers/react_on_rails.rb >/dev/null || \ + grep -E "^[[:space:]]*[^#]*config\.components_subdirectory[[:space:]]*=" config/initializers/react_on_rails.rb >/dev/null; then echo "📦 Generating React on Rails packs..." bundle exec rails react_on_rails:generate_packs echo "✅ Pack generation completed successfully" diff --git a/spec/support/shakapacker_precompile_hook_shared.rb b/spec/support/shakapacker_precompile_hook_shared.rb index 684e13a3ee..af5d324491 100755 --- a/spec/support/shakapacker_precompile_hook_shared.rb +++ b/spec/support/shakapacker_precompile_hook_shared.rb @@ -28,7 +28,7 @@ def find_rails_root end # Build ReScript if needed -# rubocop:disable Metrics/CyclomaticComplexity +# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity def build_rescript_if_needed rails_root = find_rails_root unless rails_root @@ -45,15 +45,17 @@ def build_rescript_if_needed # Validate that build:rescript script exists in package.json package_json_path = File.join(rails_root, "package.json") unless File.exist?(package_json_path) - warn "⚠️ Warning: package.json not found. Skipping ReScript build." - return + warn "❌ Error: ReScript config found but package.json not found" + warn " ReScript requires a package.json with a build:rescript script" + exit 1 end package_json = JSON.parse(File.read(package_json_path)) unless package_json.dig("scripts", "build:rescript") - warn "⚠️ Warning: ReScript config found but no build:rescript script in package.json" - warn " Add a build:rescript script to your package.json to enable ReScript builds" - return + warn "❌ Error: ReScript config found but no build:rescript script in package.json" + warn " Add this to your package.json scripts section:" + warn ' "build:rescript": "rescript build"' + exit 1 end Dir.chdir(rails_root) do @@ -63,17 +65,21 @@ def build_rescript_if_needed elsif system("which npm > /dev/null 2>&1") system("npm", "run", "build:rescript", exception: true) else - warn "⚠️ Warning: Neither yarn nor npm found. Skipping ReScript build." - return + warn "❌ Error: Neither yarn nor npm found but ReScript build required" + warn " Install yarn or npm to build ReScript files" + exit 1 end puts "✅ ReScript build completed successfully" end +rescue JSON::ParserError => e + warn "❌ Error: Invalid package.json: #{e.message}" + exit 1 rescue StandardError => e warn "❌ ReScript build failed: #{e.message}" exit 1 end -# rubocop:enable Metrics/CyclomaticComplexity +# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity # Generate React on Rails packs if needed def generate_packs_if_needed @@ -84,9 +90,10 @@ def generate_packs_if_needed return unless File.exist?(initializer_path) # Check if auto-pack generation is configured + # Match config lines that aren't commented out and allow flexible spacing initializer_content = File.read(initializer_path) - return unless initializer_content.match?(/^\s*config\.auto_load_bundle\s*=/) || - initializer_content.match?(/^\s*config\.components_subdirectory\s*=/) + return unless initializer_content.match?(/^\s*(?!#).*config\.auto_load_bundle\s*=/) || + initializer_content.match?(/^\s*(?!#).*config\.components_subdirectory\s*=/) puts "📦 Generating React on Rails packs..." From d584749fc7931531afd03bf131179b6ec37fb6ff Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 18 Nov 2025 15:57:22 -1000 Subject: [PATCH 10/12] Remove unnecessary REACT_ON_RAILS_SKIP_VALIDATION from generator template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SKIP_VALIDATION environment variable is not needed in the generator template because: 1. By the time apps use this hook, packages are already installed 2. The engine's skip_version_validation? already handles edge cases: - package_json_missing? check - running_generator? check 3. This was cargo-culted from the Ruby version which needed it for different reasons (loading Rails environment directly) The shared implementation in spec/support still sets this variable because test apps have different requirements, but user-generated apps should rely on the engine's built-in validation logic. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../templates/base/base/bin/shakapacker-precompile-hook | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook b/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook index ccd8ace38c..688c8501be 100755 --- a/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook +++ b/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook @@ -8,9 +8,6 @@ set -e -# Skip validation during precompile hook execution -export REACT_ON_RAILS_SKIP_VALIDATION=true - # Generate packs if React on Rails is configured with auto_load_bundle if [ -f "config/initializers/react_on_rails.rb" ]; then # Check for uncommented config lines (ignore lines starting with # after optional whitespace) From 5682b3dde30684400a1727ee3808cd0b3756e2c9 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 18 Nov 2025 16:13:53 -1000 Subject: [PATCH 11/12] Simplify generator template precompile hook to minimal bash script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverted from the hacky bash grep approach back to a clean Ruby script. The bash version had several problems: 1. Hacky grep parsing to detect config - fragile and error-prone 2. Duplicates logic that already exists in PacksGenerator 3. More complex (20+ lines vs 21 lines) 4. Harder to maintain The Ruby version is superior because: 1. Leverages PacksGenerator.generate_packs_if_stale which already knows when to generate vs skip 2. Simple and clean - just load Rails and call the method 3. Proper error handling with backtrace 4. Let the engine's built-in validation logic handle skipping The engine already skips validation appropriately via: - package_json_missing? check - running_generator? check So we don't need REACT_ON_RAILS_SKIP_VALIDATION here either. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../base/base/bin/shakapacker-precompile-hook | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook b/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook index 688c8501be..59a7b20781 100755 --- a/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook +++ b/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook @@ -1,20 +1,21 @@ -#!/bin/sh +#!/usr/bin/env ruby +# frozen_string_literal: true + # Shakapacker precompile hook for React on Rails # # This script runs before webpack compilation to generate pack files -# for auto-bundled components (if configured). +# for auto-bundled components. It's called automatically by Shakapacker +# when configured in config/shakapacker.yml: +# precompile_hook: 'bin/shakapacker-precompile-hook' # # See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md -set -e +require_relative "../config/environment" -# Generate packs if React on Rails is configured with auto_load_bundle -if [ -f "config/initializers/react_on_rails.rb" ]; then - # Check for uncommented config lines (ignore lines starting with # after optional whitespace) - if grep -E "^[[:space:]]*[^#]*config\.auto_load_bundle[[:space:]]*=" config/initializers/react_on_rails.rb >/dev/null || \ - grep -E "^[[:space:]]*[^#]*config\.components_subdirectory[[:space:]]*=" config/initializers/react_on_rails.rb >/dev/null; then - echo "📦 Generating React on Rails packs..." - bundle exec rails react_on_rails:generate_packs - echo "✅ Pack generation completed successfully" - fi -fi +begin + ReactOnRails::PacksGenerator.instance.generate_packs_if_stale +rescue StandardError => e + warn "❌ Error in precompile hook: #{e.message}" + warn e.backtrace.first(5).join("\n") + exit 1 +end From 6da13518aeb5f6cc4f5b8c3109da9963adb6b7cb Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 18 Nov 2025 16:18:17 -1000 Subject: [PATCH 12/12] Restore Rainbow colors and SKIP_VALIDATION to generator template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restored the original implementation which had: 1. Rainbow colored output for better UX during builds 2. ENV["REACT_ON_RAILS_SKIP_VALIDATION"] = "true" because this hook loads Rails environment and validation would fail during builds The SKIP_VALIDATION is needed here because: - The hook loads Rails via require_relative "../config/environment" - This triggers the engine's validation initializer - During precompile, packages may not be fully available yet - Setting this env var prevents spurious validation failures 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../base/base/bin/shakapacker-precompile-hook | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook b/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook index 59a7b20781..9e9632cc7a 100755 --- a/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook +++ b/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook @@ -8,14 +8,23 @@ # when configured in config/shakapacker.yml: # precompile_hook: 'bin/shakapacker-precompile-hook' # -# See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md +# Emoji Scheme: +# 🔄 = Running/in-progress +# ✅ = Success +# ❌ = Error + +# Skip validation during precompile hook execution +# The hook runs early in the build process, potentially before full Rails initialization, +# and doesn't need package version validation since it's part of the build itself +ENV["REACT_ON_RAILS_SKIP_VALIDATION"] = "true" require_relative "../config/environment" begin + puts Rainbow("🔄 Running React on Rails precompile hook...").cyan ReactOnRails::PacksGenerator.instance.generate_packs_if_stale rescue StandardError => e - warn "❌ Error in precompile hook: #{e.message}" + warn Rainbow("❌ Error in precompile hook: #{e.message}").red warn e.backtrace.first(5).join("\n") exit 1 end