class ReactOnRails::Dev::PackGenerator
-
Handles hook detection, error handling, and output formatting
- Provides a clean API for bin/dev without exposing Rake internals
- It’s a semantic wrapper around pack generation for the dev workflow
3. Why is the class named “PackGenerator” when it delegates?
- This optimization avoids subprocess overhead in the common case
- Bundle exec fallback: Required when called outside Rails context
- Direct Rake execution: Faster when already in Bundler/Rails context (bin/dev)
2. Why two execution strategies (direct vs bundle exec)?
- Users can also run the task directly: ‘rake react_on_rails:generate_packs`
- This allows the implementation to evolve without breaking bin/dev
- The rake task (lib/tasks/generate_packs.rake) provides a stable, documented interface
- The actual pack generation logic lives in lib/react_on_rails/packs_generator.rb
1. Why trigger via Rake task instead of direct Ruby code?
Design decisions:
PackGenerator triggers the generation of React on Rails packs
def capture_output(silent)
def capture_output(silent) return yield unless silent original_stdout = $stdout original_stderr = $stderr output_buffer = StringIO.new $stdout = output_buffer $stderr = output_buffer begin yield ensure $stdout = original_stdout $stderr = original_stderr end end
def generate(verbose: false)
def generate(verbose: false) # Skip if shakapacker has a precompile hook configured if ReactOnRails::PackerUtils.shakapacker_precompile_hook_configured? if verbose hook_value = ReactOnRails::PackerUtils.shakapacker_precompile_hook_value puts "⏭️ Skipping pack generation (handled by shakapacker precompile hook: #{hook_value})" end return end if verbose puts "📦 Generating React on Rails packs..." success = run_pack_generation else print "📦 Generating packs... " success = run_pack_generation(silent: true) puts success ? "✅" : "❌" end return if success puts "❌ Pack generation failed" exit 1 end
def handle_rake_error(error, _silent)
def handle_rake_error(error, _silent) error_msg = "Error generating packs: #{error.message}" error_msg += "\n#{error.backtrace.join("\n")}" if ENV["DEBUG"] # Always write to stderr, even in silent mode # Use STDERR constant instead of warn/$stderr to bypass capture_output redirection # rubocop:disable Style/StderrPuts, Style/GlobalStdStream STDERR.puts error_msg # rubocop:enable Style/StderrPuts, Style/GlobalStdStream end
def load_rake_tasks
def load_rake_tasks return if Rake::Task.task_defined?("react_on_rails:generate_packs") Rails.application.load_tasks end
def prepare_rake_task
def prepare_rake_task task = Rake::Task["react_on_rails:generate_packs"] task.reenable # Allow re-execution if called multiple times task end
def rails_available?
def rails_available? return false unless defined?(Rails) return false unless Rails.respond_to?(:application) return false if Rails.application.nil? # Verify Rails app can actually load tasks begin Rails.application.respond_to?(:load_tasks) rescue StandardError false end end
def run_pack_generation(silent: false)
def run_pack_generation(silent: false) # If we're already inside a Bundler context AND Rails is available (e.g., called from bin/dev), # we can directly require and run the task. Otherwise, use bundle exec. if should_run_directly? run_rake_task_directly(silent: silent) else run_via_bundle_exec(silent: silent) end end
def run_rake_task_directly(silent: false)
def run_rake_task_directly(silent: false) require "rake" load_rake_tasks task = prepare_rake_task capture_output(silent) do task.invoke true end rescue StandardError => e handle_rake_error(e, silent) false end
def run_via_bundle_exec(silent: false)
def run_via_bundle_exec(silent: false) if silent system( "bundle", "exec", "rake", "react_on_rails:generate_packs", out: File::NULL, err: File::NULL ) else system("bundle", "exec", "rake", "react_on_rails:generate_packs") end end
def should_run_directly?
def should_run_directly? # Check if we're in a meaningful Bundler context with BUNDLE_GEMFILE return false unless defined?(Bundler) return false unless ENV["BUNDLE_GEMFILE"] return false unless rails_available? true end