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..c94e8681d1 --- /dev/null +++ b/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook @@ -0,0 +1,19 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Shakapacker precompile hook for React on Rails Pro test dummy app +# +# 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, "spec", "support", "shakapacker_precompile_hook_shared.rb") + +unless File.exist?(shared_hook) + warn "❌ Error: Shared precompile hook not found at #{shared_hook}" + exit 1 +end + +# Load and execute the shared hook +load shared_hook 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 diff --git a/spec/dummy/bin/shakapacker-precompile-hook b/spec/dummy/bin/shakapacker-precompile-hook index b650b02db5..a7a4abf408 100755 --- a/spec/dummy/bin/shakapacker-precompile-hook +++ b/spec/dummy/bin/shakapacker-precompile-hook @@ -1,101 +1,19 @@ #!/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 +# Shakapacker precompile hook for React on Rails test dummy app +# +# This script loads the shared test helper implementation. +# For production apps, use the generator template which includes a standalone implementation. -require "fileutils" +# Find the gem root directory (three levels up from spec/dummy/bin) +gem_root = File.expand_path("../../..", __dir__) +shared_hook = File.join(gem_root, "spec", "support", "shakapacker_precompile_hook_shared.rb") -# 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") +unless File.exist?(shared_hook) + warn "❌ Error: Shared precompile hook not found at #{shared_hook}" exit 1 end + +# Load and execute the shared hook +load shared_hook diff --git a/spec/support/shakapacker_precompile_hook_shared.rb b/spec/support/shakapacker_precompile_hook_shared.rb new file mode 100755 index 0000000000..af5d324491 --- /dev/null +++ b/spec/support/shakapacker_precompile_hook_shared.rb @@ -0,0 +1,119 @@ +#!/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/AbcSize, 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 "❌ 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 "❌ 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 + # 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 "❌ 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/AbcSize, 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 + # 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*=/) + + 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