diff --git a/docs/contributor-info/coding-agents-guide.md b/docs/contributor-info/coding-agents-guide.md index 8fbe8ca5e7..71c35f1266 100644 --- a/docs/contributor-info/coding-agents-guide.md +++ b/docs/contributor-info/coding-agents-guide.md @@ -16,7 +16,7 @@ This guide provides structured instructions for AI coding agents working with Re ### Version Compatibility Matrix | react_on_rails | Shakapacker | Webpack | Node.js | Ruby | -|----------------|-------------|---------|---------|------| +| -------------- | ----------- | ------- | ------- | ---- | | v16.x | >= 6.0 | v5 | 20-22 | 3.2+ | | v14.x | >= 6.0 | v5 | 18-20 | 2.7+ | | v13.x | >= 6.0 | v5 | 16-18 | 2.7+ | @@ -206,12 +206,14 @@ fix_webpack_cache() { #### 1. Missing Routes File (js-routes gem) **Detection:** + ```regex /Cannot read properties of undefined.*reading 'module'/ /ProvidedDependencyTemplate\.apply/ ``` **Auto-fix:** + ```bash bundle exec rails js:export ``` @@ -219,12 +221,14 @@ bundle exec rails js:export #### 2. ProvidePlugin Module Missing **Detection:** + ```regex /Error: Can't resolve.*\$app/ /Module not found.*utils\/routes/ ``` **Auto-fix:** + ```bash # Check if file exists, generate if missing [ -f "app/javascript/utils/routes.js" ] || bundle exec rails js:export @@ -236,12 +240,14 @@ grep -q "\$app" config/webpack/*.js || echo "⚠️ Missing webpack alias" #### 3. Version Incompatibility **Detection:** + ```regex /webpack.*incompatible/ /peer dep.*react-on-rails/ ``` **Auto-fix:** + ```bash # Update to compatible versions npm install react-on-rails@^16.0.0 @@ -370,6 +376,7 @@ After successful upgrades, suggest: ### If Build Completely Breaks 1. **Rollback immediately:** + ```bash git checkout HEAD~1 -- Gemfile package.json Gemfile.lock package-lock.json bundle install @@ -377,11 +384,13 @@ After successful upgrades, suggest: ``` 2. **Identify the issue:** + ```bash npm run build 2>&1 | tee build-error.log ``` 3. **Apply targeted fixes:** + - Missing routes: `rails js:export` - Cache issues: `rm -rf node_modules/.cache tmp/cache` - Dependencies: `bundle update && npm install` @@ -391,6 +400,7 @@ After successful upgrades, suggest: ### If Rails Environment Unavailable Use minimal commands: + ```bash # Skip database operations DATABASE_URL=sqlite3:tmp/minimal.db rails js:export @@ -401,4 +411,4 @@ RAILS_ENV=test rails js:export --- -This guide ensures consistent, reliable React on Rails operations for coding agents while providing clear error recovery paths. \ No newline at end of file +This guide ensures consistent, reliable React on Rails operations for coding agents while providing clear error recovery paths. diff --git a/docs/guides/upgrading-react-on-rails.md b/docs/guides/upgrading-react-on-rails.md index 00cc9eba1f..9e07b59ac5 100644 --- a/docs/guides/upgrading-react-on-rails.md +++ b/docs/guides/upgrading-react-on-rails.md @@ -88,6 +88,7 @@ rails generate react_on_rails:install **Symptoms:** Webpack cannot find modules referenced in your configuration **Solutions:** + 1. Clear webpack cache: `rm -rf node_modules/.cache` 2. Verify all ProvidePlugin modules exist 3. Check webpack alias configuration diff --git a/docs/javascript/troubleshooting-build-errors.md b/docs/javascript/troubleshooting-build-errors.md index 400fac213d..79dde2c12f 100644 --- a/docs/javascript/troubleshooting-build-errors.md +++ b/docs/javascript/troubleshooting-build-errors.md @@ -15,6 +15,7 @@ This guide covers common webpack build errors encountered when using react_on_ra **Note:** This error only occurs if you're using the optional `js-routes` gem to access Rails routes in JavaScript. ### Error Message + ``` Cannot read properties of undefined (reading 'module') TypeError: Cannot read properties of undefined (reading 'module') @@ -22,31 +23,40 @@ TypeError: Cannot read properties of undefined (reading 'module') ``` ### Root Cause + This error occurs when: + 1. Your webpack config references Rails routes via ProvidePlugin 2. The `js-routes` gem hasn't generated the JavaScript routes file 3. You're using `js-routes` integration but missing the generated file ### When You Need js-routes + `js-routes` is **optional** and typically used when: + - Rails-heavy apps with React components that need to navigate to Rails routes - Server-side rendered apps mixing Rails and React routing - Legacy Rails apps migrating ERB views to React - Apps using Rails routing patterns for RESTful APIs ### When You DON'T Need js-routes + Most modern React apps use: + - Client-side routing (React Router) instead of Rails routes - Hardcoded API endpoints or environment variables - SPA (Single Page App) architecture with API-only Rails backend ### Solution (if using js-routes) + 1. **Generate JavaScript routes file:** + ```bash bundle exec rails js:export ``` 2. **Verify the routes file was created:** + ```bash ls app/javascript/utils/routes.js ``` @@ -54,22 +64,25 @@ Most modern React apps use: 3. **Check webpack configuration includes ProvidePlugin:** ```javascript new webpack.ProvidePlugin({ - Routes: "$app/utils/routes" - }) + Routes: '$app/utils/routes', + }); ``` ### Alternative Solution (if NOT using js-routes) + Remove the Routes ProvidePlugin from your webpack configuration: + ```javascript // Remove this line if you don't use js-routes new webpack.ProvidePlugin({ - Routes: "$app/utils/routes" // ← Remove this -}) + Routes: '$app/utils/routes', // ← Remove this +}); ``` ## ProvidePlugin Module Resolution Errors ### Common Error Patterns + - `Cannot read properties of undefined (reading 'module')` - `Module not found: Error: Can't resolve 'module_name'` - `ERROR in ./path/to/file.js: Cannot find name 'GlobalVariable'` @@ -77,18 +90,21 @@ new webpack.ProvidePlugin({ ### Debugging Steps 1. **Check file existence:** + ```bash find app/javascript -name "routes.*" -type f find app/javascript -name "*global*" -type f ``` 2. **Verify webpack aliases:** + ```javascript // In your webpack config console.log('Webpack aliases:', config.resolve.alias); ``` 3. **Test module resolution:** + ```bash # Run webpack with debug output bin/shakapacker --debug-shakapacker @@ -109,6 +125,7 @@ new webpack.ProvidePlugin({ ## Environment Setup Dependencies ### Rails Environment Required + Some operations require a working Rails environment: - `rails js:export` (generates routes - **only needed if using js-routes gem**) @@ -118,17 +135,20 @@ Some operations require a working Rails environment: ### Common Issues 1. **Database Connection Errors:** + ``` MONGODB | Error checking localhost:27017: Connection refused ``` **Solution:** These are usually warnings and don't prevent operation. To silence: + ```bash # Run with minimal environment RAILS_ENV=development bundle exec rails js:export ``` 2. **Missing Dependencies:** + ``` sidekiq-pro is not installed ``` @@ -138,6 +158,7 @@ Some operations require a working Rails environment: ### Workarounds 1. **Skip database initialization:** + ```bash DATABASE_URL=sqlite3:tmp/db.sqlite3 rails js:export ``` @@ -152,7 +173,7 @@ Some operations require a working Rails environment: ### Version Compatibility Matrix | react_on_rails | Shakapacker | Webpack | Node.js | -|----------------|-------------|---------|---------| +| -------------- | ----------- | ------- | ------- | | v16.x | >= 6.0 | v5 | 20-22 | | v14.x | >= 6.0 | v5 | 18-20 | | v13.x | >= 6.0 | v5 | 16-18 | @@ -160,6 +181,7 @@ Some operations require a working Rails environment: ### Common Upgrade Issues 1. **Webpacker to Shakapacker migration incomplete:** + ```bash # Remove webpacker references grep -r "webpacker" config/ @@ -172,6 +194,7 @@ Some operations require a working Rails environment: ``` ### Migration Steps + 1. Follow the [Shakapacker upgrade guide](https://github.com/shakacode/shakapacker/blob/main/docs/v6_upgrade.md) 2. Update webpack configurations 3. Regenerate configurations with `rails generate react_on_rails:install` @@ -254,4 +277,4 @@ fi - Check the [general troubleshooting guide](./troubleshooting-when-using-shakapacker.md) - Review [webpack configuration docs](./webpack.md) -- Contact [justin@shakacode.com](mailto:justin@shakacode.com) for professional support \ No newline at end of file +- Contact [justin@shakacode.com](mailto:justin@shakacode.com) for professional support diff --git a/lib/generators/react_on_rails/bin/dev b/lib/generators/react_on_rails/bin/dev deleted file mode 100755 index 7e2259d6c7..0000000000 --- a/lib/generators/react_on_rails/bin/dev +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# ReactOnRails Development Server -# -# This script provides a simple interface to the ReactOnRails development -# server management. The core logic is implemented in ReactOnRails::Dev -# classes for better maintainability and testing. -# -# Each command uses a specific Procfile for process management: -# - bin/dev (default/hmr): Uses Procfile.dev -# - bin/dev static: Uses Procfile.dev-static-assets -# - bin/dev prod: Uses Procfile.dev-prod-assets -# -# To customize development environment: -# 1. Edit the appropriate Procfile to modify which processes run -# 2. Modify this script for project-specific command-line behavior -# 3. Extend ReactOnRails::Dev classes in your Rails app for advanced customization -# 4. Use classes directly: ReactOnRails::Dev::ServerManager.start(:development, "Custom.procfile") - -begin - require "bundler/setup" - require "react_on_rails/dev" -rescue LoadError - # Fallback for when gem is not yet installed - puts "Loading ReactOnRails development tools..." - require_relative "../../lib/react_on_rails/dev" -end - -# Main execution -case ARGV[0] -when "production-assets", "prod" - ReactOnRails::Dev::ServerManager.start(:production_like) -when "static" - ReactOnRails::Dev::ServerManager.start(:static, "Procfile.dev-static-assets") -when "kill" - ReactOnRails::Dev::ServerManager.kill_processes -when "help", "--help", "-h" - ReactOnRails::Dev::ServerManager.show_help -when "hmr", nil - ReactOnRails::Dev::ServerManager.start(:development, "Procfile.dev") -else - puts "Unknown argument: #{ARGV[0]}" - puts "Run 'bin/dev help' for usage information" - exit 1 -end diff --git a/lib/generators/react_on_rails/generator_messages.rb b/lib/generators/react_on_rails/generator_messages.rb index 68656489ee..b0bdf31bf9 100644 --- a/lib/generators/react_on_rails/generator_messages.rb +++ b/lib/generators/react_on_rails/generator_messages.rb @@ -38,7 +38,7 @@ def clear @output = [] end - def helpful_message_after_installation(component_name: "HelloWorld") + def helpful_message_after_installation(component_name: "HelloWorld", route: "hello_world") process_manager_section = build_process_manager_section testing_section = build_testing_section package_manager = detect_package_manager @@ -62,7 +62,7 @@ def helpful_message_after_installation(component_name: "HelloWorld") ./bin/dev prod # Production-like mode for testing ./bin/dev help # See all available options - 3. Visit: #{Rainbow('http://localhost:3000/hello_world').cyan.underline} + 3. Visit: #{Rainbow(route ? "http://localhost:3000/#{route}" : 'http://localhost:3000').cyan.underline} ✨ KEY FEATURES: ───────────────────────────────────────────────────────────────────────── • Auto-registration enabled - Your layout only needs: diff --git a/lib/generators/react_on_rails/install_generator.rb b/lib/generators/react_on_rails/install_generator.rb index 5a5042b4ce..ba78b8e94b 100644 --- a/lib/generators/react_on_rails/install_generator.rb +++ b/lib/generators/react_on_rails/install_generator.rb @@ -136,11 +136,13 @@ def shakapacker_in_gemfile? end def add_bin_scripts - directory "#{__dir__}/bin", "bin" + # Copy bin scripts from templates + template_bin_path = "#{__dir__}/templates/base/base/bin" + directory template_bin_path, "bin" # Make these and only these files executable files_to_copy = [] - Dir.chdir("#{__dir__}/bin") do + Dir.chdir(template_bin_path) do files_to_copy.concat(Dir.glob("*")) end files_to_become_executable = files_to_copy.map { |filename| "bin/#{filename}" } @@ -149,7 +151,14 @@ def add_bin_scripts end def add_post_install_message - GeneratorMessages.add_info(GeneratorMessages.helpful_message_after_installation) + # Determine what route will be created by the generator + route = "hello_world" # This is the hardcoded route from base_generator.rb + component_name = options.redux? ? "HelloWorldApp" : "HelloWorld" + + GeneratorMessages.add_info(GeneratorMessages.helpful_message_after_installation( + component_name: component_name, + route: route + )) end def shakapacker_loaded_in_process?(gem_name) diff --git a/lib/generators/react_on_rails/react_with_redux_generator.rb b/lib/generators/react_on_rails/react_with_redux_generator.rb index 30849585ef..8e0e96d575 100644 --- a/lib/generators/react_on_rails/react_with_redux_generator.rb +++ b/lib/generators/react_on_rails/react_with_redux_generator.rb @@ -68,7 +68,7 @@ def add_redux_specific_messages require_relative "generator_messages" GeneratorMessages.output.clear GeneratorMessages.add_info( - GeneratorMessages.helpful_message_after_installation(component_name: "HelloWorldApp") + GeneratorMessages.helpful_message_after_installation(component_name: "HelloWorldApp", route: "hello_world") ) end end diff --git a/lib/generators/react_on_rails/templates/base/base/bin/dev b/lib/generators/react_on_rails/templates/base/base/bin/dev index 7e2259d6c7..80ce846b08 100755 --- a/lib/generators/react_on_rails/templates/base/base/bin/dev +++ b/lib/generators/react_on_rails/templates/base/base/bin/dev @@ -28,19 +28,4 @@ rescue LoadError end # Main execution -case ARGV[0] -when "production-assets", "prod" - ReactOnRails::Dev::ServerManager.start(:production_like) -when "static" - ReactOnRails::Dev::ServerManager.start(:static, "Procfile.dev-static-assets") -when "kill" - ReactOnRails::Dev::ServerManager.kill_processes -when "help", "--help", "-h" - ReactOnRails::Dev::ServerManager.show_help -when "hmr", nil - ReactOnRails::Dev::ServerManager.start(:development, "Procfile.dev") -else - puts "Unknown argument: #{ARGV[0]}" - puts "Run 'bin/dev help' for usage information" - exit 1 -end +ReactOnRails::Dev::ServerManager.run_from_command_line(ARGV) diff --git a/lib/react_on_rails/configuration.rb b/lib/react_on_rails/configuration.rb index 23cb741a9e..b2a67b451f 100644 --- a/lib/react_on_rails/configuration.rb +++ b/lib/react_on_rails/configuration.rb @@ -175,7 +175,7 @@ def validate_generated_component_packs_loading_strategy end msg = <<~MSG - ReactOnRails: Your current version of #{ReactOnRails::PackerUtils.packer_type.upcase_first} \ + ReactOnRails: Your current version of shakapacker \ does not support async script loading, which may cause performance issues. Please either: 1. Use :sync or :defer loading strategy instead of :async 2. Upgrade to Shakapacker v8.2.0 or above to enable async script loading @@ -284,8 +284,10 @@ def configure_generated_assets_dirs_deprecation if ReactOnRails::PackerUtils.using_packer? packer_public_output_path = ReactOnRails::PackerUtils.packer_public_output_path # rubocop:disable Layout/LineLength + packer_name = ReactOnRails::PackerUtils.packer_type&.upcase_first + Rails.logger.warn "Error configuring config/initializers/react_on_rails. Define neither the generated_assets_dirs nor " \ - "the generated_assets_dir when using #{ReactOnRails::PackerUtils.packer_type.upcase_first}. This is defined by " \ + "the generated_assets_dir when using #{packer_name}. This is defined by " \ "public_output_path specified in #{ReactOnRails::PackerUtils.packer_type}.yml = #{packer_public_output_path}." # rubocop:enable Layout/LineLength return @@ -331,15 +333,17 @@ def raise_missing_components_subdirectory end def compile_command_conflict_message + packer_name = ReactOnRails::PackerUtils.packer_type.upcase_first + packer_type = ReactOnRails::PackerUtils.packer_type <<~MSG - React on Rails and #{ReactOnRails::PackerUtils.packer_type.upcase_first} error in configuration! + React on Rails and #{packer_name} error in configuration! In order to use config/react_on_rails.rb config.build_production_command, - you must edit config/#{ReactOnRails::PackerUtils.packer_type}.yml to include this value in the default configuration: - '#{ReactOnRails::PackerUtils.packer_type}_precompile: false' + you must edit config/#{packer_type}.yml to include this value in the default configuration: + '#{packer_type}_precompile: false' Alternatively, remove the config/react_on_rails.rb config.build_production_command and the - default bin/#{ReactOnRails::PackerUtils.packer_type} script will be used for assets:precompile. + default bin/#{packer_type} script will be used for assets:precompile. MSG end diff --git a/lib/react_on_rails/dev/server_manager.rb b/lib/react_on_rails/dev/server_manager.rb index e4a20ab458..832a416ac6 100644 --- a/lib/react_on_rails/dev/server_manager.rb +++ b/lib/react_on_rails/dev/server_manager.rb @@ -8,16 +8,16 @@ module ReactOnRails module Dev class ServerManager class << self - def start(mode = :development, procfile = nil, verbose: false) + def start(mode = :development, procfile = nil, verbose: false, route: nil, rails_env: nil) case mode when :production_like - run_production_like(_verbose: verbose) + run_production_like(_verbose: verbose, route: route, rails_env: rails_env) when :static procfile ||= "Procfile.dev-static-assets" - run_static_development(procfile, verbose: verbose) + run_static_development(procfile, verbose: verbose, route: route) when :development, :hmr procfile ||= "Procfile.dev" - run_development(procfile, verbose: verbose) + run_development(procfile, verbose: verbose, route: route) else raise ArgumentError, "Unknown mode: #{mode}" end @@ -116,6 +116,50 @@ def show_help puts help_troubleshooting end + def run_from_command_line(args = ARGV) + require "optparse" + + options = { route: nil, rails_env: nil } + + OptionParser.new do |opts| + opts.banner = "Usage: dev [command] [options]" + + opts.on("--route ROUTE", "Specify the route to display in URLs (default: root)") do |route| + options[:route] = route + end + + opts.on("--rails-env ENV", "Override RAILS_ENV for assets:precompile step only (prod mode only)") do |env| + options[:rails_env] = env + end + + opts.on("-h", "--help", "Prints this help") do + show_help + exit + end + end.parse!(args) + + # Get the command (anything that's not parsed as an option) + command = args[0] + + # Main execution + case command + when "production-assets", "prod" + start(:production_like, nil, verbose: false, route: options[:route], rails_env: options[:rails_env]) + when "static" + start(:static, "Procfile.dev-static-assets", verbose: false, route: options[:route]) + when "kill" + kill_processes + when "help", "--help", "-h" + show_help + when "hmr", nil + start(:development, "Procfile.dev", verbose: false, route: options[:route]) + else + puts "Unknown argument: #{command}" + puts "Run 'dev help' for usage information" + exit 1 + end + end + private def help_usage @@ -142,12 +186,21 @@ def help_commands end # rubocop:enable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize def help_options <<~OPTIONS #{Rainbow('⚙️ OPTIONS:').cyan.bold} - #{Rainbow('--verbose, -v').green.bold} #{Rainbow('Enable verbose output for pack generation').white} + #{Rainbow('--route ROUTE').green.bold} #{Rainbow('Specify route to display in URLs (default: root)').white} + #{Rainbow('--rails-env ENV').green.bold} #{Rainbow('Override RAILS_ENV for assets:precompile step only (prod mode only)').white} + #{Rainbow('--verbose, -v').green.bold} #{Rainbow('Enable verbose output for pack generation').white} + + #{Rainbow('📝 EXAMPLES:').cyan.bold} + #{Rainbow('bin/dev prod').green.bold} #{Rainbow('# NODE_ENV=production, RAILS_ENV=development').white} + #{Rainbow('bin/dev prod --rails-env=production').green.bold} #{Rainbow('# NODE_ENV=production, RAILS_ENV=production').white} + #{Rainbow('bin/dev prod --route=dashboard').green.bold} #{Rainbow('# Custom route in URLs').white} OPTIONS end + # rubocop:enable Metrics/AbcSize def help_customization <<~CUSTOMIZATION @@ -172,7 +225,7 @@ def help_mode_details #{Rainbow('•').yellow} #{Rainbow('Source maps for debugging').white} #{Rainbow('•').yellow} #{Rainbow('May have Flash of Unstyled Content (FOUC)').white} #{Rainbow('•').yellow} #{Rainbow('Fast recompilation').white} - #{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow('http://localhost:3000/hello_world').cyan.underline} + #{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow('http://localhost:3000/').cyan.underline} #{Rainbow('📦 Static development mode').cyan.bold} - #{Rainbow('Procfile.dev-static-assets').green}: #{Rainbow('•').yellow} #{Rainbow('No HMR (static assets with auto-recompilation)').white} @@ -181,24 +234,26 @@ def help_mode_details #{Rainbow('•').yellow} #{Rainbow('CSS extracted to separate files (no FOUC)').white} #{Rainbow('•').yellow} #{Rainbow('Development environment (faster builds than production)').white} #{Rainbow('•').yellow} #{Rainbow('Source maps for debugging').white} - #{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow('http://localhost:3000/hello_world').cyan.underline} + #{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('Asset precompilation with production optimizations').white} - #{Rainbow('•').yellow} #{Rainbow('Optimized, minified bundles').white} - #{Rainbow('•').yellow} #{Rainbow('Extracted CSS files (no FOUC)').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} + #{Rainbow('•').yellow} #{Rainbow('Server processes controlled by Procfile.dev-prod-assets environment').white} + #{Rainbow('•').yellow} #{Rainbow('Optimized, minified bundles with CSS extraction').white} #{Rainbow('•').yellow} #{Rainbow('No HMR (static assets)').white} - #{Rainbow('•').yellow} #{Rainbow('Slower recompilation').white} - #{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow('http://localhost:3001/hello_world').cyan.underline} + #{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow('http://localhost:3001/').cyan.underline} MODES end # rubocop:enable Metrics/AbcSize - def run_production_like(_verbose: false) + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + def run_production_like(_verbose: false, route: nil, rails_env: nil) procfile = "Procfile.dev-prod-assets" - print_procfile_info(procfile) + print_procfile_info(procfile, route: route) print_server_info( "🏭 Starting production-like development server...", [ @@ -208,25 +263,124 @@ def run_production_like(_verbose: false) "No HMR (Hot Module Replacement)", "CSS extracted to separate files (no FOUC)" ], - 3001 + 3001, + route: route ) - # Precompile assets in production mode (includes pack generation automatically) - puts "🔨 Precompiling assets..." - success = system "RAILS_ENV=production NODE_ENV=production bundle exec rails assets:precompile" + # Precompile assets with production webpack optimizations (includes pack generation automatically) + env = { "NODE_ENV" => "production" } + + # Validate and sanitize rails_env to prevent shell injection + if rails_env + unless rails_env.match?(/\A[a-z0-9_]+\z/i) + puts "❌ Invalid rails_env: '#{rails_env}'. Must contain only letters, numbers, and underscores." + exit 1 + end + env["RAILS_ENV"] = rails_env + end + + argv = ["bundle", "exec", "rails", "assets:precompile"] + + puts "🔨 Precompiling assets with production webpack optimizations..." + puts "" + + puts Rainbow("ℹ️ Asset Precompilation Environment:").blue + puts " • NODE_ENV=production → Webpack optimizations (minification, compression)" + if rails_env + puts " • RAILS_ENV=#{rails_env} → Custom Rails environment for assets:precompile only" + puts " • Note: RAILS_ENV=production requires credentials, database setup, etc." + puts " • Server processes will use environment from Procfile.dev-prod-assets" + else + puts " • RAILS_ENV=development → Simpler Rails setup (no credentials needed)" + puts " • Use --rails-env=production for assets:precompile step only" + puts " • Server processes will use environment from Procfile.dev-prod-assets" + puts " • Gets production webpack bundles without production Rails complexity" + end + puts "" + + env_display = env.map { |k, v| "#{k}=#{v}" }.join(" ") + puts "#{Rainbow('💻 Running:').blue} #{env_display} #{argv.join(' ')}" + puts "" - if success + # Capture both stdout and stderr + require "open3" + stdout, stderr, status = Open3.capture3(env, *argv) + + if status.success? puts "✅ Assets precompiled successfully" ProcessManager.ensure_procfile(procfile) ProcessManager.run_with_process_manager(procfile) else puts "❌ Asset precompilation failed" + puts "" + + # Combine and display all output + all_output = [] + all_output << stdout unless stdout.empty? + all_output << stderr unless stderr.empty? + + unless all_output.empty? + puts Rainbow("📋 Full Command Output:").red.bold + puts Rainbow("─" * 60).red + all_output.each { |output| puts output } + puts Rainbow("─" * 60).red + puts "" + end + + puts Rainbow("🛠️ To debug this issue:").yellow.bold + command_display = "#{env_display} #{argv.join(' ')}" + puts "#{Rainbow('1.').cyan} #{Rainbow('Run the command separately to see detailed output:').white}" + puts " #{Rainbow(command_display).cyan}" + puts "" + puts "#{Rainbow('2.').cyan} #{Rainbow('Add --trace for full stack trace:').white}" + puts " #{Rainbow("#{command_display} --trace").cyan}" + puts "" + puts "#{Rainbow('3.').cyan} #{Rainbow('Or try with development webpack (faster, less optimized):').white}" + puts " #{Rainbow('NODE_ENV=development bundle exec rails assets:precompile').cyan}" + puts "" + + puts Rainbow("💡 Common fixes:").yellow.bold + + # Provide specific guidance based on error content + error_content = "#{stderr} #{stdout}".downcase + + if error_content.include?("secret_key_base") + puts "#{Rainbow('•').yellow} #{Rainbow('Missing secret_key_base:').white.bold} " \ + "Run #{Rainbow('bin/rails credentials:edit').cyan}" + end + + if error_content.include?("database") || error_content.include?("relation") || + error_content.include?("table") + puts "#{Rainbow('•').yellow} #{Rainbow('Database issues:').white.bold} " \ + "Run #{Rainbow('bin/rails db:create db:migrate').cyan}" + end + + if error_content.include?("gem") || error_content.include?("bundle") || error_content.include?("load error") + puts "#{Rainbow('•').yellow} #{Rainbow('Missing dependencies:').white.bold} " \ + "Run #{Rainbow('bundle install && npm install').cyan}" + end + + if error_content.include?("webpack") || error_content.include?("module") || + error_content.include?("compilation") + puts "#{Rainbow('•').yellow} #{Rainbow('Webpack compilation:').white.bold} " \ + "Check JavaScript/webpack errors above" + end + + # Always show these general options + puts "#{Rainbow('•').yellow} #{Rainbow('Environment config:').white} " \ + "Check #{Rainbow('config/environments/production.rb').cyan}" + + puts "" + puts Rainbow("ℹ️ Alternative for development:").blue + puts " #{Rainbow('bin/dev static').green} # Static assets without production optimizations" + puts "" exit 1 end end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity - def run_static_development(procfile, verbose: false) - print_procfile_info(procfile) + def run_static_development(procfile, verbose: false, route: nil) + print_procfile_info(procfile, route: route) print_server_info( "⚡ Starting development server with static assets...", [ @@ -235,7 +389,8 @@ def run_static_development(procfile, verbose: false) "CSS extracted to separate files (no FOUC)", "Development environment (source maps, faster builds)", "Auto-recompiles on file changes" - ] + ], + route: route ) PackGenerator.generate(verbose: verbose) @@ -243,25 +398,27 @@ def run_static_development(procfile, verbose: false) ProcessManager.run_with_process_manager(procfile) end - def run_development(procfile, verbose: false) - print_procfile_info(procfile) + def run_development(procfile, verbose: false, route: nil) + print_procfile_info(procfile, route: route) PackGenerator.generate(verbose: verbose) ProcessManager.ensure_procfile(procfile) ProcessManager.run_with_process_manager(procfile) end - def print_server_info(title, features, port = 3000) + def print_server_info(title, features, port = 3000, route: nil) puts title features.each { |feature| puts " - #{feature}" } puts "" puts "" - puts "💡 Access at: #{Rainbow("http://localhost:#{port}/hello_world").cyan.underline}" + url = route ? "http://localhost:#{port}/#{route}" : "http://localhost:#{port}" + puts "💡 Access at: #{Rainbow(url).cyan.underline}" puts "" end - def print_procfile_info(procfile) + def print_procfile_info(procfile, route: nil) port = procfile_port(procfile) box_width = 60 + url = route ? "http://localhost:#{port}/#{route}" : "http://localhost:#{port}" puts "" puts box_border(box_width) @@ -269,7 +426,7 @@ def print_procfile_info(procfile) puts format_box_line("📋 Using Procfile: #{procfile}", box_width) puts format_box_line("🔧 Customize this file for your app's needs", box_width) puts box_empty_line(box_width) - puts format_box_line("💡 Access at: #{Rainbow("http://localhost:#{port}/hello_world").cyan.underline}", + puts format_box_line("💡 Access at: #{Rainbow(url).cyan.underline}", box_width) puts box_empty_line(box_width) puts box_bottom(box_width) diff --git a/spec/dummy/bin/dev b/spec/dummy/bin/dev index d5f833752a..a253a84c49 120000 --- a/spec/dummy/bin/dev +++ b/spec/dummy/bin/dev @@ -1 +1 @@ -../../../lib/generators/react_on_rails/bin/dev \ No newline at end of file +../../../lib/generators/react_on_rails/templates/base/base/bin/dev \ No newline at end of file diff --git a/spec/react_on_rails/binstubs/dev_spec.rb b/spec/react_on_rails/binstubs/dev_spec.rb index a5e4965f4b..52f8b02aef 100644 --- a/spec/react_on_rails/binstubs/dev_spec.rb +++ b/spec/react_on_rails/binstubs/dev_spec.rb @@ -3,7 +3,7 @@ require "react_on_rails/dev" RSpec.describe "bin/dev script" do - let(:script_path) { "lib/generators/react_on_rails/bin/dev" } + let(:script_path) { "lib/generators/react_on_rails/templates/base/base/bin/dev" } # To suppress stdout during tests original_stderr = $stderr @@ -40,36 +40,20 @@ def setup_script_execution_for_tool_tests expect(script_content).to include("require \"react_on_rails/dev\"") end - it "supports static development mode" do + it "delegates to ServerManager command line interface" do script_content = File.read(script_path) - expect(script_content).to include("ReactOnRails::Dev::ServerManager.start(:static") - end - - it "supports production-like mode" do - script_content = File.read(script_path) - expect(script_content).to include("ReactOnRails::Dev::ServerManager.start(:production_like") - end - - it "supports help command" do - script_content = File.read(script_path) - expect(script_content).to include('when "help", "--help", "-h"') - expect(script_content).to include("ReactOnRails::Dev::ServerManager.show_help") - end - - it "supports kill command" do - script_content = File.read(script_path) - expect(script_content).to include("ReactOnRails::Dev::ServerManager.kill_processes") + expect(script_content).to include("ReactOnRails::Dev::ServerManager.run_from_command_line") end it "with ReactOnRails::Dev loaded, delegates to ServerManager" do setup_script_execution_for_tool_tests - allow(ReactOnRails::Dev::ServerManager).to receive(:start) + allow(ReactOnRails::Dev::ServerManager).to receive(:run_from_command_line) # Mock the require to succeed allow_any_instance_of(Kernel).to receive(:require).with("bundler/setup").and_return(true) allow_any_instance_of(Kernel).to receive(:require).with("react_on_rails/dev").and_return(true) - expect(ReactOnRails::Dev::ServerManager).to receive(:start).with(:development, "Procfile.dev") + expect(ReactOnRails::Dev::ServerManager).to receive(:run_from_command_line).with(ARGV) load script_path end diff --git a/spec/react_on_rails/dev/server_manager_spec.rb b/spec/react_on_rails/dev/server_manager_spec.rb index 4ee05cf3c6..69ecfddfd4 100644 --- a/spec/react_on_rails/dev/server_manager_spec.rb +++ b/spec/react_on_rails/dev/server_manager_spec.rb @@ -54,14 +54,36 @@ def mock_system_calls end it "starts production-like mode" do - command = "RAILS_ENV=production NODE_ENV=production bundle exec rails assets:precompile" - expect_any_instance_of(Kernel).to receive(:system).with(command).and_return(true) + env = { "NODE_ENV" => "production" } + argv = ["bundle", "exec", "rails", "assets:precompile"] + status_double = instance_double(Process::Status, success?: true) + expect(Open3).to receive(:capture3).with(env, *argv).and_return(["output", "", status_double]) expect(ReactOnRails::Dev::ProcessManager).to receive(:ensure_procfile).with("Procfile.dev-prod-assets") expect(ReactOnRails::Dev::ProcessManager).to receive(:run_with_process_manager).with("Procfile.dev-prod-assets") described_class.start(:production_like) end + it "starts production-like mode with custom rails_env" do + env = { "NODE_ENV" => "production", "RAILS_ENV" => "staging" } + argv = ["bundle", "exec", "rails", "assets:precompile"] + status_double = instance_double(Process::Status, success?: true) + expect(Open3).to receive(:capture3).with(env, *argv).and_return(["output", "", status_double]) + expect(ReactOnRails::Dev::ProcessManager).to receive(:ensure_procfile).with("Procfile.dev-prod-assets") + expect(ReactOnRails::Dev::ProcessManager).to receive(:run_with_process_manager).with("Procfile.dev-prod-assets") + + described_class.start(:production_like, nil, verbose: false, rails_env: "staging") + end + + it "rejects invalid rails_env with shell injection characters" do + expect_any_instance_of(Kernel).to receive(:exit).with(1) + allow_any_instance_of(Kernel).to receive(:puts) # Allow other puts calls + error_pattern = /Invalid rails_env.*Must contain only letters, numbers, and underscores/ + expect_any_instance_of(Kernel).to receive(:puts).with(error_pattern) + + described_class.start(:production_like, nil, verbose: false, rails_env: "production; rm -rf /") + end + it "raises error for unknown mode" do expect { described_class.start(:unknown) }.to raise_error(ArgumentError, "Unknown mode: unknown") end diff --git a/spec/react_on_rails/utils_spec.rb b/spec/react_on_rails/utils_spec.rb index 44769f2ecf..11bf2bca05 100644 --- a/spec/react_on_rails/utils_spec.rb +++ b/spec/react_on_rails/utils_spec.rb @@ -30,7 +30,6 @@ module ReactOnRails # We don't need to mock anything here because the shakapacker gem is already installed and will be used by default it "uses shakapacker" do - expect(ReactOnRails::PackerUtils.using_shakapacker_const?).to be(true) expect(ReactOnRails::PackerUtils.packer_type).to eq("shakapacker") expect(ReactOnRails::PackerUtils.packer).to eq(::Shakapacker) end