# frozen_string_literal: true require "rails/generators/app_base" module Rails module ActionMethods # :nodoc: attr_reader :options def initialize(generator) @generator = generator @options = generator.options end private %w(template copy_file directory empty_directory inside empty_directory_with_keep_file create_file chmod shebang).each do |method| class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{method}(*args, &block) @generator.send(:#{method}, *args, &block) end RUBY end def method_missing(...) @generator.send(...) end end # The application builder allows you to override elements of the application # generator without being forced to reverse the operations of the default # generator. # # This allows you to override entire operations, like the creation of the # Gemfile, README, or JavaScript files, without needing to know exactly # what those operations do so you can create another template action. # # class CustomAppBuilder < Rails::AppBuilder # def test # @generator.gem "rspec-rails", group: [:development, :test] # run "bundle install" # generate "rspec:install" # end # end class AppBuilder def rakefile template "Rakefile" end def readme copy_file "README.md", "README.md" end def ruby_version template "ruby-version", ".ruby-version" end def node_version template "node-version", ".node-version" end def gemfile template "Gemfile" end def configru template "config.ru" end def gitignore template "gitignore", ".gitignore" end def gitattributes template "gitattributes", ".gitattributes" end def dockerfiles template "Dockerfile" template "dockerignore", ".dockerignore" template "docker-entrypoint", "bin/docker-entrypoint" chmod "bin/docker-entrypoint", 0755 & ~File.umask, verbose: false end def version_control if !options[:skip_git] && !options[:pretend] run git_init_command, capture: options[:quiet], abort_on_failure: false end end def app directory "app" empty_directory_with_keep_file "app/assets/images" keep_file "app/controllers/concerns" keep_file "app/models/concerns" end def bin directory "bin" do |content| "#{shebang}\n" + content end chmod "bin", 0755 & ~File.umask, verbose: false end def bin_when_updating bin end def config empty_directory "config" inside "config" do template "routes.rb" unless options[:update] template "application.rb" template "environment.rb" template "cable.yml" unless options[:update] || options[:skip_action_cable] template "puma.rb" unless options[:update] template "storage.yml" unless options[:update] || skip_active_storage? directory "environments" directory "initializers" directory "locales" unless options[:update] end end def config_when_updating action_cable_config_exist = File.exist?("config/cable.yml") active_storage_config_exist = File.exist?("config/storage.yml") rack_cors_config_exist = File.exist?("config/initializers/cors.rb") assets_config_exist = File.exist?("config/initializers/assets.rb") asset_manifest_exist = File.exist?("app/assets/config/manifest.js") asset_app_stylesheet_exist = File.exist?("app/assets/stylesheets/application.css") csp_config_exist = File.exist?("config/initializers/content_security_policy.rb") permissions_policy_config_exist = File.exist?("config/initializers/permissions_policy.rb") @config_target_version = Rails.application.config.loaded_config_version || "5.0" config if !options[:skip_action_cable] && !action_cable_config_exist template "config/cable.yml" end if !skip_active_storage? && !active_storage_config_exist template "config/storage.yml" end if skip_sprockets? && skip_propshaft? && !assets_config_exist remove_file "config/initializers/assets.rb" end if skip_sprockets? && !asset_manifest_exist remove_file "app/assets/config/manifest.js" end if skip_sprockets? && !asset_app_stylesheet_exist remove_file "app/assets/stylesheets/application.css" end unless rack_cors_config_exist remove_file "config/initializers/cors.rb" end if options[:api] unless csp_config_exist remove_file "config/initializers/content_security_policy.rb" end unless permissions_policy_config_exist remove_file "config/initializers/permissions_policy.rb" end end end def master_key return if options[:pretend] || options[:dummy_app] require "rails/generators/rails/master_key/master_key_generator" master_key_generator = Rails::Generators::MasterKeyGenerator.new([], quiet: options[:quiet], force: options[:force]) master_key_generator.add_master_key_file_silently master_key_generator.ignore_master_key_file_silently end def credentials return if options[:pretend] || options[:dummy_app] require "rails/generators/rails/credentials/credentials_generator" Rails::Generators::CredentialsGenerator.new([], quiet: true).add_credentials_file end def credentials_diff_enroll return if options[:skip_decrypted_diffs] || options[:dummy_app] || options[:pretend] @generator.shell.mute do rails_command "credentials:diff --enroll", inline: true, shell: @generator.shell end end def database_yml template "config/databases/#{options[:database]}.yml", "config/database.yml" end def db directory "db" end def lib empty_directory "lib" empty_directory_with_keep_file "lib/tasks" empty_directory_with_keep_file "lib/assets" end def log empty_directory_with_keep_file "log" end def public_directory directory "public", "public", recursive: false end def storage empty_directory_with_keep_file "storage" empty_directory_with_keep_file "tmp/storage" end def test empty_directory_with_keep_file "test/fixtures/files" empty_directory_with_keep_file "test/controllers" empty_directory_with_keep_file "test/mailers" empty_directory_with_keep_file "test/models" empty_directory_with_keep_file "test/helpers" empty_directory_with_keep_file "test/integration" template "test/channels/application_cable/connection_test.rb" template "test/test_helper.rb" end def system_test empty_directory_with_keep_file "test/system" template "test/application_system_test_case.rb" end def tmp empty_directory_with_keep_file "tmp" empty_directory_with_keep_file "tmp/pids" empty_directory "tmp/cache" empty_directory "tmp/cache/assets" end def vendor empty_directory_with_keep_file "vendor" end def config_target_version defined?(@config_target_version) ? @config_target_version : Rails::VERSION::STRING.to_f end end module Generators # We need to store the RAILS_DEV_PATH in a constant, otherwise the path # can change in Ruby 1.8.7 when we FileUtils.cd. RAILS_DEV_PATH = File.expand_path("../../../../../..", __dir__) class AppGenerator < AppBase # :stopdoc: add_shared_options_for "application" # Add rails command options class_option :version, type: :boolean, aliases: "-v", group: :rails, desc: "Show Rails version number and quit" class_option :api, type: :boolean, desc: "Preconfigure smaller stack for API only apps" class_option :minimal, type: :boolean, desc: "Preconfigure a minimal rails app" class_option :javascript, type: :string, aliases: ["-j", "--js"], default: "importmap", desc: "Choose JavaScript approach [options: importmap (default), bun, webpack, esbuild, rollup]" class_option :css, type: :string, aliases: "-c", desc: "Choose CSS processor [options: tailwind, bootstrap, bulma, postcss, sass] check https://github.com/rails/cssbundling-rails for more options" class_option :skip_bundle, type: :boolean, aliases: "-B", default: nil, desc: "Don't run bundle install" class_option :skip_decrypted_diffs, type: :boolean, default: nil, desc: "Don't configure git to show decrypted diffs of encrypted credentials" OPTION_IMPLICATIONS = # :nodoc: AppBase::OPTION_IMPLICATIONS.merge( skip_git: [:skip_decrypted_diffs], minimal: [ :skip_action_cable, :skip_action_mailbox, :skip_action_mailer, :skip_action_text, :skip_active_job, :skip_active_storage, :skip_bootsnap, :skip_dev_gems, :skip_hotwire, :skip_javascript, :skip_jbuilder, :skip_system_test, ], api: [ :skip_asset_pipeline, :skip_javascript, ], ) do |option, implications, more_implications| implications + more_implications end META_OPTIONS = [:minimal] # :nodoc: def self.apply_rails_template(template, destination) # :nodoc: generator = new([destination], { template: template }, { destination_root: destination }) generator.set_default_accessors! generator.apply_rails_template generator.run_bundle generator.run_after_bundle_callbacks end def initialize(*args) super imply_options(OPTION_IMPLICATIONS, meta_options: META_OPTIONS) if !options[:skip_active_record] && !DATABASES.include?(options[:database]) raise Error, "Invalid value for --database option. Supported preconfigurations are: #{DATABASES.join(", ")}." end @after_bundle_callbacks = [] end public_task :report_implied_options public_task :set_default_accessors! public_task :create_root public_task :target_rails_prerelease def create_root_files build(:readme) build(:rakefile) build(:node_version) if using_node? build(:ruby_version) build(:configru) unless options[:skip_git] build(:gitignore) build(:gitattributes) end build(:gemfile) build(:version_control) end def create_app_files build(:app) end def create_bin_files build(:bin) end def update_bin_files build(:bin_when_updating) end remove_task :update_bin_files def update_active_storage unless skip_active_storage? rails_command "active_storage:update", inline: true end end remove_task :update_active_storage def create_dockerfiles return if options[:skip_docker] || options[:dummy_app] build(:dockerfiles) end def create_config_files build(:config) end def update_config_files build(:config_when_updating) end remove_task :update_config_files def create_master_key build(:master_key) end def create_credentials build(:credentials) build(:credentials_diff_enroll) end def display_upgrade_guide_info say "\nAfter this, check Rails upgrade guide at https://guides.rubyonrails.org/upgrading_ruby_on_rails.html for more details about upgrading your app." end remove_task :display_upgrade_guide_info def create_boot_file template "config/boot.rb" end def create_active_record_files return if options[:skip_active_record] build(:database_yml) end def create_db_files return if options[:skip_active_record] build(:db) end def create_lib_files build(:lib) end def create_log_files build(:log) end def create_public_files build(:public_directory) end def create_tmp_files build(:tmp) end def create_vendor_files build(:vendor) end def create_test_files build(:test) unless options[:skip_test] end def create_system_test_files build(:system_test) if depends_on_system_test? end def create_storage_files build(:storage) end def delete_app_assets_if_api_option if options[:api] remove_dir "app/assets" remove_dir "lib/assets" remove_dir "tmp/cache/assets" end end def delete_app_helpers_if_api_option if options[:api] remove_dir "app/helpers" remove_dir "test/helpers" end end def delete_app_views_if_api_option if options[:api] if options[:skip_action_mailer] remove_dir "app/views" else remove_file "app/views/layouts/application.html.erb" end end end def delete_public_files_if_api_option if options[:api] remove_file "public/404.html" remove_file "public/422.html" remove_file "public/500.html" remove_file "public/apple-touch-icon-precomposed.png" remove_file "public/apple-touch-icon.png" remove_file "public/favicon.ico" end end def delete_assets_initializer_skipping_sprockets_and_propshaft if skip_sprockets? && skip_propshaft? remove_file "config/initializers/assets.rb" end if skip_sprockets? remove_file "app/assets/config/manifest.js" remove_dir "app/assets/config" remove_file "app/assets/stylesheets/application.css" create_file "app/assets/stylesheets/application.css", "/* Application styles */\n" unless options[:api] end end def delete_application_record_skipping_active_record if options[:skip_active_record] remove_file "app/models/application_record.rb" end end def delete_active_job_folder_if_skipping_active_job if options[:skip_active_job] remove_dir "app/jobs" end end def delete_action_mailer_files_skipping_action_mailer if options[:skip_action_mailer] remove_file "app/views/layouts/mailer.html.erb" remove_file "app/views/layouts/mailer.text.erb" remove_dir "app/mailers" remove_dir "test/mailers" end end def delete_action_cable_files_skipping_action_cable if options[:skip_action_cable] remove_dir "app/javascript/channels" remove_dir "app/channels" remove_dir "test/channels" end end def delete_non_api_initializers_if_api_option if options[:api] remove_file "config/initializers/content_security_policy.rb" remove_file "config/initializers/permissions_policy.rb" end end def delete_api_initializers unless options[:api] remove_file "config/initializers/cors.rb" end end def delete_new_framework_defaults unless options[:update] remove_file "config/initializers/new_framework_defaults_#{Rails::VERSION::MAJOR}_#{Rails::VERSION::MINOR}.rb" end end def finish_template build(:leftovers) end public_task :apply_rails_template public_task :run_bundle public_task :add_bundler_platforms public_task :generate_bundler_binstub public_task :run_javascript public_task :run_hotwire public_task :run_css def run_after_bundle_callbacks @after_bundle_callbacks.each(&:call) end def self.banner "rails new #{arguments.map(&:usage).join(' ')} [options]" end # :startdoc: private # Define file as an alias to create_file for backwards compatibility. def file(*args, &block) create_file(*args, &block) end # Registers a callback to be executed after bundle binstubs # have run. # # after_bundle do # git add: '.' # end def after_bundle(&block) # :doc: @after_bundle_callbacks << block end def get_builder_class defined?(::AppBuilder) ? ::AppBuilder : Rails::AppBuilder end end # This class handles preparation of the arguments before the AppGenerator is # called. The class provides version or help information if they were # requested, and also constructs the railsrc file (used for extra configuration # options). # # This class should be called before the AppGenerator is required and started # since it configures and mutates ARGV correctly. class ARGVScrubber # :nodoc: def initialize(argv = ARGV) @argv = argv end def prepare! handle_version_request!(@argv.first) handle_invalid_command!(@argv.first, @argv) do handle_rails_rc!(@argv.drop(1)) end end def self.default_rc_file xdg_config_home = ENV["XDG_CONFIG_HOME"].presence || "~/.config" xdg_railsrc = File.expand_path("rails/railsrc", xdg_config_home) if File.exist?(xdg_railsrc) xdg_railsrc else File.expand_path("~/.railsrc") end end private def handle_version_request!(argument) if ["--version", "-v"].include?(argument) require "rails/version" puts "Rails #{Rails::VERSION::STRING}" exit(0) end end def handle_invalid_command!(argument, argv) if argument == "new" yield else ["--help"] + argv.drop(1) end end def handle_rails_rc!(argv) if argv.find { |arg| arg == "--no-rc" } argv.reject { |arg| arg == "--no-rc" } else railsrc(argv) { |rc_argv, rc| insert_railsrc_into_argv!(rc_argv, rc) } end end def railsrc(argv) if (customrc = argv.index { |x| x.include?("--rc=") }) fname = File.expand_path(argv[customrc].gsub(/--rc=/, "")) yield(argv.take(customrc) + argv.drop(customrc + 1), fname) else yield argv, self.class.default_rc_file end end def read_rc_file(railsrc) extra_args = File.readlines(railsrc).flat_map(&:split) puts "Using #{extra_args.join(" ")} from #{railsrc}" extra_args end def insert_railsrc_into_argv!(argv, railsrc) return argv unless File.exist?(railsrc) extra_args = read_rc_file railsrc argv.take(1) + extra_args + argv.drop(1) end end end end