diff --git a/lib/generators/react_on_rails/base_generator.rb b/lib/generators/react_on_rails/base_generator.rb index 6f8746a08e..88998b946e 100644 --- a/lib/generators/react_on_rails/base_generator.rb +++ b/lib/generators/react_on_rails/base_generator.rb @@ -37,12 +37,16 @@ def copy_base_files app/views/layouts/hello_world.html.erb Procfile.dev Procfile.dev-static-assets - Procfile.dev-prod-assets] + Procfile.dev-prod-assets + bin/shakapacker-precompile-hook] base_templates = %w[config/initializers/react_on_rails.rb] base_files.each { |file| copy_file("#{base_path}#{file}", file) } base_templates.each do |file| template("#{base_path}/#{file}.tt", file) end + + # Make the hook script executable + File.chmod(0o755, "bin/shakapacker-precompile-hook") if File.exist?("bin/shakapacker-precompile-hook") end def copy_js_bundle_files 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 new file mode 100644 index 0000000000..9e755e09aa --- /dev/null +++ b/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook @@ -0,0 +1,20 @@ +#!/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. It's called automatically by Shakapacker +# when configured in config/shakapacker.yml: +# precompile_hook: 'bin/shakapacker-precompile-hook' + +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 diff --git a/lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml b/lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml index 8f76d350b2..2a1994fbdd 100644 --- a/lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml +++ b/lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml @@ -42,6 +42,11 @@ default: &default # Raises an error if there is a mismatch in the shakapacker gem and npm package being used ensure_consistent_versioning: true + # Hook to run before webpack compilation (e.g., for generating dynamic entry points) + # SECURITY: Only reference trusted scripts within your project. The hook command will be + # validated to ensure it points to a file within the project root. + precompile_hook: 'bin/shakapacker-precompile-hook' + # Select whether the compiler will use SHA digest ('digest' option) or most recent modified timestamp ('mtime') to determine freshness compiler_strategy: digest diff --git a/lib/react_on_rails/configuration.rb b/lib/react_on_rails/configuration.rb index 20be673d42..efac595d42 100644 --- a/lib/react_on_rails/configuration.rb +++ b/lib/react_on_rails/configuration.rb @@ -238,7 +238,11 @@ def adjust_precompile_task raise(ReactOnRails::Error, compile_command_conflict_message) if ReactOnRails::PackerUtils.precompile? precompile_tasks = lambda { - Rake::Task["react_on_rails:generate_packs"].invoke + # Skip generate_packs if shakapacker has a precompile hook configured + unless ReactOnRails::PackerUtils.shakapacker_precompile_hook_configured? + Rake::Task["react_on_rails:generate_packs"].invoke + end + Rake::Task["react_on_rails:assets:webpack"].invoke # VERSIONS is per the shakacode/shakapacker clean method definition. diff --git a/lib/react_on_rails/dev/pack_generator.rb b/lib/react_on_rails/dev/pack_generator.rb index 9e74bf0631..aec873fe28 100644 --- a/lib/react_on_rails/dev/pack_generator.rb +++ b/lib/react_on_rails/dev/pack_generator.rb @@ -7,6 +7,12 @@ module Dev class PackGenerator class << self def generate(verbose: false) + # Skip if shakapacker has a precompile hook configured + if ReactOnRails::PackerUtils.shakapacker_precompile_hook_configured? + puts "⏭️ Skipping pack generation (handled by shakapacker precompile hook)" if verbose + return + end + if verbose puts "📦 Generating React on Rails packs..." success = system "bundle exec rake react_on_rails:generate_packs" diff --git a/lib/react_on_rails/dev/server_manager.rb b/lib/react_on_rails/dev/server_manager.rb index 08587dbb2c..cf133ec448 100644 --- a/lib/react_on_rails/dev/server_manager.rb +++ b/lib/react_on_rails/dev/server_manager.rb @@ -243,7 +243,7 @@ def help_mode_details <<~MODES #{Rainbow('🔥 HMR Development mode (default)').cyan.bold} - #{Rainbow('Procfile.dev').green}: #{Rainbow('•').yellow} #{Rainbow('Hot Module Replacement (HMR) enabled').white} - #{Rainbow('•').yellow} #{Rainbow('React on Rails pack generation before Procfile start').white} + #{Rainbow('•').yellow} #{Rainbow('React on Rails pack generation (via precompile hook or bin/dev)').white} #{Rainbow('•').yellow} #{Rainbow('Webpack dev server for fast recompilation').white} #{Rainbow('•').yellow} #{Rainbow('Source maps for debugging').white} #{Rainbow('•').yellow} #{Rainbow('May have Flash of Unstyled Content (FOUC)').white} @@ -252,7 +252,7 @@ def help_mode_details #{Rainbow('📦 Static development mode').cyan.bold} - #{Rainbow('Procfile.dev-static-assets').green}: #{Rainbow('•').yellow} #{Rainbow('No HMR (static assets with auto-recompilation)').white} - #{Rainbow('•').yellow} #{Rainbow('React on Rails pack generation before Procfile start').white} + #{Rainbow('•').yellow} #{Rainbow('React on Rails pack generation (via precompile hook or bin/dev)').white} #{Rainbow('•').yellow} #{Rainbow('Webpack watch mode for auto-recompilation').white} #{Rainbow('•').yellow} #{Rainbow('CSS extracted to separate files (no FOUC)').white} #{Rainbow('•').yellow} #{Rainbow('Development environment (faster builds than production)').white} @@ -260,7 +260,7 @@ def help_mode_details #{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow('http://localhost:3000/').cyan.underline} #{Rainbow('🏭 Production-assets mode').cyan.bold} - #{Rainbow('Procfile.dev-prod-assets').green}: - #{Rainbow('•').yellow} #{Rainbow('React on Rails pack generation before Procfile start').white} + #{Rainbow('•').yellow} #{Rainbow('React on Rails pack generation (via precompile hook or assets:precompile)').white} #{Rainbow('•').yellow} #{Rainbow('Asset precompilation with NODE_ENV=production (webpack optimizations)').white} #{Rainbow('•').yellow} #{Rainbow('RAILS_ENV=development by default for assets:precompile (avoids credentials)').white} #{Rainbow('•').yellow} #{Rainbow('Use --rails-env=production for assets:precompile only (not server processes)').white} @@ -276,16 +276,20 @@ def help_mode_details def run_production_like(_verbose: false, route: nil, rails_env: nil) procfile = "Procfile.dev-prod-assets" + features = [ + "Precompiling assets with production optimizations", + "Running Rails server on port 3001", + "No HMR (Hot Module Replacement)", + "CSS extracted to separate files (no FOUC)" + ] + + # NOTE: Pack generation happens automatically during assets:precompile + # either via precompile hook or via the configuration.rb adjust_precompile_task + print_procfile_info(procfile, route: route) print_server_info( "🏭 Starting production-like development server...", - [ - "Generating React on Rails packs", - "Precompiling assets with production optimizations", - "Running Rails server on port 3001", - "No HMR (Hot Module Replacement)", - "CSS extracted to separate files (no FOUC)" - ], + features, 3001, route: route ) @@ -404,15 +408,22 @@ def run_production_like(_verbose: false, route: nil, rails_env: nil) def run_static_development(procfile, verbose: false, route: nil) print_procfile_info(procfile, route: route) + + features = [ + "Using shakapacker --watch (no HMR)", + "CSS extracted to separate files (no FOUC)", + "Development environment (source maps, faster builds)", + "Auto-recompiles on file changes" + ] + + # Add pack generation info if not using precompile hook + unless ReactOnRails::PackerUtils.shakapacker_precompile_hook_configured? + features.unshift("Generating React on Rails packs") + end + print_server_info( "⚡ Starting development server with static assets...", - [ - "Generating React on Rails packs", - "Using shakapacker --watch (no HMR)", - "CSS extracted to separate files (no FOUC)", - "Development environment (source maps, faster builds)", - "Auto-recompiles on file changes" - ], + features, route: route ) diff --git a/lib/react_on_rails/packer_utils.rb b/lib/react_on_rails/packer_utils.rb index d6e6511000..4cdbaf3a00 100644 --- a/lib/react_on_rails/packer_utils.rb +++ b/lib/react_on_rails/packer_utils.rb @@ -166,5 +166,18 @@ def self.raise_shakapacker_version_incompatible_for_basic_pack_generation raise ReactOnRails::Error, msg end + + # Check if shakapacker.yml has a precompile_hook configured + # This prevents react_on_rails from running generate_packs redundantly + def self.shakapacker_precompile_hook_configured? + return false unless defined?(::Shakapacker) + + config_data = ::Shakapacker.config.send(:data) + hook = config_data[:precompile_hook] + + hook.present? + rescue StandardError + false + end end end diff --git a/spec/dummy/bin/shakapacker-precompile-hook b/spec/dummy/bin/shakapacker-precompile-hook new file mode 100755 index 0000000000..9e755e09aa --- /dev/null +++ b/spec/dummy/bin/shakapacker-precompile-hook @@ -0,0 +1,20 @@ +#!/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. It's called automatically by Shakapacker +# when configured in config/shakapacker.yml: +# precompile_hook: 'bin/shakapacker-precompile-hook' + +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 diff --git a/spec/dummy/config/shakapacker.yml b/spec/dummy/config/shakapacker.yml index c58b284594..d4a9871336 100644 --- a/spec/dummy/config/shakapacker.yml +++ b/spec/dummy/config/shakapacker.yml @@ -17,6 +17,11 @@ default: &default cache_manifest: false nested_entries: true + # Hook to run before webpack compilation (e.g., for generating dynamic entry points) + # SECURITY: Only reference trusted scripts within your project. The hook command will be + # validated to ensure it points to a file within the project root. + precompile_hook: 'bin/shakapacker-precompile-hook' + development: <<: *default # Turn this to true if you want to use the rails/shakapacker check that the test