class ReactOnRails::Generators::InstallGenerator
rubocop:disable Metrics/ClassLength
def add_bin_scripts
def add_bin_scripts # 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(template_bin_path) do files_to_copy.concat(Dir.glob("*")) end files_to_become_executable = files_to_copy.map { |filename| "bin/#{filename}" } File.chmod(0o755, *files_to_become_executable) end
def add_post_install_message
def add_post_install_message # 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 check_node_version
def check_node_version node_version = `node --version 2>/dev/null`.strip return if node_version.blank? # Extract major version number (e.g., "v18.17.0" -> 18) major_version = node_version[/v(\d+)/, 1]&.to_i return unless major_version return unless major_version < 18 warning = <<~MSG.strip ⚠️ Node.js version #{node_version} detected. React on Rails recommends Node.js 18+ for best compatibility. You may experience issues with older versions. Consider upgrading: https://nodejs.org/en/ MSG GeneratorMessages.add_warning(warning) end
def cli_exists?(command)
def cli_exists?(command) system("which #{command} > /dev/null 2>&1") end
def create_css_module_types
def create_css_module_types puts Rainbow("📝 Creating CSS module type definitions...").yellow # Ensure the types directory exists FileUtils.mkdir_p("app/javascript/types") css_module_types_content = <<~TS.strip // TypeScript definitions for CSS modules declare module "*.module.css" { const classes: { [key: string]: string }; export default classes; } declare module "*.module.scss" { const classes: { [key: string]: string }; export default classes; } declare module "*.module.sass" { const classes: { [key: string]: string }; export default classes; } TS File.write("app/javascript/types/css-modules.d.ts", css_module_types_content) puts Rainbow("✅ Created CSS module type definitions").green end
def create_typescript_config
def create_typescript_config if File.exist?("tsconfig.json") puts Rainbow("⚠️ tsconfig.json already exists, skipping creation").yellow return end tsconfig_content = { "compilerOptions" => { "target" => "es2018", "allowJs" => true, "skipLibCheck" => true, "strict" => true, "noUncheckedIndexedAccess" => true, "forceConsistentCasingInFileNames" => true, "noFallthroughCasesInSwitch" => true, "module" => "esnext", "moduleResolution" => "bundler", "resolveJsonModule" => true, "isolatedModules" => true, "noEmit" => true, "jsx" => "react-jsx" }, "include" => [ "app/javascript/**/*" ] } File.write("tsconfig.json", JSON.pretty_generate(tsconfig_content)) puts Rainbow("✅ Created tsconfig.json").green end
def ensure_shakapacker_in_gemfile
def ensure_shakapacker_in_gemfile return if shakapacker_in_gemfile? puts Rainbow("📝 Adding Shakapacker to Gemfile...").yellow success = system("bundle add shakapacker --strict") return if success handle_shakapacker_gemfile_error end
def ensure_shakapacker_installed
def ensure_shakapacker_installed return if shakapacker_configured? print_shakapacker_setup_banner ensure_shakapacker_in_gemfile install_shakapacker finalize_shakapacker_setup end
def finalize_shakapacker_setup
def finalize_shakapacker_setup puts Rainbow("✅ Shakapacker installed successfully!").green puts Rainbow("=" * 80).cyan puts Rainbow("🚀 CONTINUING WITH REACT ON RAILS SETUP").cyan.bold puts "#{Rainbow('=' * 80).cyan}\n" # Create marker file so base generator can avoid copying shakapacker.yml File.write(".shakapacker_just_installed", "") end
def handle_shakapacker_gemfile_error
def handle_shakapacker_gemfile_error error = <<~MSG.strip 🚫 Failed to add Shakapacker to your Gemfile. This could be due to: • Bundle installation issues • Network connectivity problems • Gemfile permissions Please try manually: bundle add shakapacker --strict Then re-run: rails generate react_on_rails:install MSG GeneratorMessages.add_error(error) raise Thor::Error, error unless options.ignore_warnings? end
def handle_shakapacker_install_error
def handle_shakapacker_install_error error = <<~MSG.strip 🚫 Failed to install Shakapacker automatically. This could be due to: • Missing Node.js or npm/yarn • Network connectivity issues • Incomplete bundle installation • Missing write permissions Troubleshooting steps: 1. Ensure Node.js is installed: node --version 2. Run: bundle install 3. Try manually: bundle exec rails shakapacker:install 4. Check for error output above 5. Re-run: rails generate react_on_rails:install Need help? Visit: https://github.com/shakacode/shakapacker/blob/main/docs/installation.md MSG GeneratorMessages.add_error(error) raise Thor::Error, error unless options.ignore_warnings? end
def install_shakapacker
def install_shakapacker puts Rainbow("⚙️ Installing Shakapacker (required for webpack integration)...").yellow # First run bundle install to make shakapacker available puts Rainbow("📦 Running bundle install...").yellow bundle_success = system("bundle install") unless bundle_success handle_shakapacker_install_error return end # Then run the shakapacker installer success = system("bundle exec rails shakapacker:install") return if success handle_shakapacker_install_error end
def install_typescript_dependencies
def install_typescript_dependencies puts Rainbow("📝 Installing TypeScript dependencies...").yellow # Delegate to shared module for consistent dependency management add_typescript_dependencies end
def installation_prerequisites_met?
js(.coffee) are not checked by this method, but instead produce warning messages
NOTE: other requirements for existing files such as .gitignore or application.
def installation_prerequisites_met? !(missing_node? || missing_package_manager? || ReactOnRails::GitUtils.uncommitted_changes?(GeneratorMessages)) end
def invoke_generators
def invoke_generators ensure_shakapacker_installed if options.typescript? install_typescript_dependencies create_css_module_types create_typescript_config end invoke "react_on_rails:base", [], { typescript: options.typescript?, redux: options.redux?, rspack: options.rspack? } if options.redux? invoke "react_on_rails:react_with_redux", [], { typescript: options.typescript? } else invoke "react_on_rails:react_no_redux", [], { typescript: options.typescript? } end setup_react_dependencies end
def missing_node?
def missing_node? node_missing = ReactOnRails::Utils.running_on_windows? ? `where node`.blank? : `which node`.blank? if node_missing error = <<~MSG.strip 🚫 Node.js is required but not found on your system. Please install Node.js before continuing: • Download from: https://nodejs.org/en/ • Recommended: Use a version manager like nvm, fnm, or volta • Minimum required version: Node.js 18+ After installation, restart your terminal and try again. MSG GeneratorMessages.add_error(error) return true end # Check Node.js version if available check_node_version false end
def missing_package_manager?
def missing_package_manager? package_managers = %w[npm pnpm yarn bun] missing = package_managers.none? { |pm| cli_exists?(pm) } if missing error = <<~MSG.strip 🚫 No JavaScript package manager found on your system. React on Rails requires a JavaScript package manager to install dependencies. Please install one of the following: • npm: Usually comes with Node.js (https://nodejs.org/en/) • yarn: npm install -g yarn (https://yarnpkg.com/) • pnpm: npm install -g pnpm (https://pnpm.io/) • bun: Install from https://bun.sh/ After installation, restart your terminal and try again. MSG GeneratorMessages.add_error(error) return true end false end
def print_generator_messages
def print_generator_messages GeneratorMessages.messages.each { |message| puts message } end
def print_shakapacker_setup_banner
def print_shakapacker_setup_banner puts Rainbow("\n#{'=' * 80}").cyan puts Rainbow("🔧 SHAKAPACKER SETUP").cyan.bold puts Rainbow("=" * 80).cyan end
def run_generators
- Note: - Validation Skipping: Sets ENV["REACT_ON_RAILS_SKIP_VALIDATION"] to prevent
def run_generators # Set environment variable to skip validation during generator run # This is inherited by all invoked generators and persists through Rails initialization # See lib/react_on_rails/engine.rb for the validation skip logic ENV["REACT_ON_RAILS_SKIP_VALIDATION"] = "true" if installation_prerequisites_met? || options.ignore_warnings? invoke_generators add_bin_scripts # Only add the post install message if not using Redux # Redux generator handles its own messages add_post_install_message unless options.redux? else error = <<~MSG.strip 🚫 React on Rails generator prerequisites not met! Please resolve the issues listed above before continuing. All prerequisites must be satisfied for a successful installation. Use --ignore-warnings to bypass checks (not recommended). MSG GeneratorMessages.add_error(error) end ensure # Always clean up ENV variable, even if generator fails # CRITICAL: ENV cleanup must come first to ensure it executes even if # print_generator_messages raises an exception. This prevents ENV pollution # that could affect subsequent processes. ENV.delete("REACT_ON_RAILS_SKIP_VALIDATION") print_generator_messages end
def setup_react_dependencies
def setup_react_dependencies setup_js_dependencies end
def shakapacker_binaries_exist?
def shakapacker_binaries_exist? File.exist?("bin/shakapacker") && File.exist?("bin/shakapacker-dev-server") end
def shakapacker_configured?
def shakapacker_configured? # Check for essential shakapacker configuration files and binaries shakapacker_binaries_exist? && File.exist?("config/shakapacker.yml") && File.exist?("config/webpack/webpack.config.js") end
def shakapacker_in_bundler_specs?(gem_name)
def shakapacker_in_bundler_specs?(gem_name) require "bundler" Bundler.load.specs.any? { |s| s.name == gem_name } rescue StandardError false end
def shakapacker_in_gemfile?
shakapacker might be present as a dependency of react_on_rails but not
We only check the Gemfile text, not lockfile or dependencies, because
Checks whether "shakapacker" is explicitly declared in this project's Gemfile.
def shakapacker_in_gemfile? gem_name = "shakapacker" shakapacker_in_gemfile_text?(gem_name) end
def shakapacker_in_gemfile_text?(gem_name)
def shakapacker_in_gemfile_text?(gem_name) gemfile = ENV["BUNDLE_GEMFILE"] || "Gemfile" File.file?(gemfile) && File.foreach(gemfile).any? { |l| l.match?(/^\s*gem\s+['"]#{Regexp.escape(gem_name)}['"]/) } end
def shakapacker_in_lockfile?(gem_name)
def shakapacker_in_lockfile?(gem_name) gemfile = ENV["BUNDLE_GEMFILE"] || "Gemfile" lockfile = File.join(File.dirname(gemfile), "Gemfile.lock") File.file?(lockfile) && File.foreach(lockfile).any? { |l| l.match?(/^\s{4}#{Regexp.escape(gem_name)}\s\(/) } end
def shakapacker_loaded_in_process?(gem_name)
def shakapacker_loaded_in_process?(gem_name) Gem.loaded_specs.key?(gem_name) end