# 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(meth, *args, &block)
@generator.send(meth, *args, &block)
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 gemfile
template "Gemfile"
end
def configru
template "config.ru"
end
def gitignore
template "gitignore", ".gitignore"
end
def gitattributes
template "gitattributes", ".gitattributes"
end
def version_control
if !options[:skip_git] && !options[:pretend]
run "git init", capture: options[:quiet], abort_on_failure: false
if user_default_branch.strip.empty?
`git symbolic-ref HEAD refs/heads/main`
end
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[:updating]
template "application.rb"
template "environment.rb"
template "cable.yml" unless options[:updating] || options[:skip_action_cable]
template "puma.rb" unless options[:updating]
template "storage.yml" unless options[:updating] || skip_active_storage?
directory "environments"
directory "initializers"
directory "locales" unless options[:updating]
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
if !skip_sprockets?
insert_into_file "config/application.rb", %(require "sprockets/railtie"), after: /require\(["']rails\/all["']\)\n/
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: options[:quiet]).add_credentials_file_silently
end
def database_yml
template "config/databases/#{options[:database]}.yml", "config/database.yml"
end
def db
directory "db"
end
def db_when_updating
path = File.expand_path("db/schema.rb", destination_root)
if File.exist?(path)
gsub_file("db/schema.rb", /ActiveRecord::Schema\.define/, "ActiveRecord::Schema[6.1].define")
end
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
private
def user_default_branch
@user_default_branch ||= `git config init.defaultbranch`
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", default: "importmap", desc: "Choose JavaScript approach [options: importmap (default), 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"
class_option :skip_bundle, type: :boolean, aliases: "-B", default: false, desc: "Don't run bundle install"
def initialize(*args)
super
if !options[:skip_active_record] && !DATABASES.include?(options[:database])
raise Error, "Invalid value for --database option. Supported preconfigurations are: #{DATABASES.join(", ")}."
end
# Force sprockets and JavaScript to be skipped when generating API only apps.
# Can't modify options hash as it's frozen by default.
if options[:api]
self.options = options.merge(skip_asset_pipeline: true, skip_javascript: true).freeze
end
if options[:minimal]
self.options = options.merge(
skip_action_cable: true,
skip_action_mailer: true,
skip_action_mailbox: true,
skip_action_text: true,
skip_active_job: true,
skip_active_storage: true,
skip_bootsnap: true,
skip_dev_gems: true,
skip_javascript: true,
skip_jbuilder: true,
skip_system_test: true,
skip_hotwire: true).freeze
end
@after_bundle_callbacks = []
end
public_task :set_default_accessors!
public_task :create_root
public_task :target_rails_prerelease
def create_root_files
build(:readme)
build(:rakefile)
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_db_schema
build(:db_when_updating)
end
remove_task :update_db_schema
def update_active_storage
unless skip_active_storage?
rails_command "active_storage:update", inline: true
end
end
remove_task :update_active_storage
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)
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) unless skip_active_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 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 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_7_0.rb"
end
end
def finish_template
build(:leftovers)
end
public_task :apply_rails_template, :run_bundle
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