From 86b506e6a6ede38508f6c0cd6539e36c283941cc Mon Sep 17 00:00:00 2001 From: zhandao Date: Tue, 12 Mar 2024 15:40:48 +0800 Subject: [PATCH 1/7] Underline the keywords in the questions --- .gitignore | 1 + lib/nextgen/commands/create.rb | 56 ++++++++++++++++++---------------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 4e0f123..ccdde23 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ /spec/reports/ /tmp/ /Gemfile.lock +/.idea/ diff --git a/lib/nextgen/commands/create.rb b/lib/nextgen/commands/create.rb index 2fce5d9..665d454 100644 --- a/lib/nextgen/commands/create.rb +++ b/lib/nextgen/commands/create.rb @@ -5,6 +5,7 @@ require "open3" require "tmpdir" require "tty-prompt" +require "rainbow" require "nextgen/ext/prompt/list" require "nextgen/ext/prompt/multilist" @@ -26,9 +27,9 @@ def run # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity say <<~BANNER Welcome to nextgen, the interactive Rails app generator! - You are about to create a Rails app named "#{app_name}" in the following directory: + You are about to create a Rails app named "#{cyan(app_name)}" in the following directory: - #{app_path} + #{cyan(app_path)} You'll be asked ~10 questions about database, test framework, and other options. The standard Rails "omakase" experience will be selected by default. @@ -83,9 +84,9 @@ def run # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity say <<~DONE.gsub(/^/, " ") - #{set_color("Done!", :green)} + #{green("Done!")} - A Rails #{rails_version} app was generated in #{set_color(app_path, :cyan)}. + A Rails #{rails_version} app was generated in #{cyan(app_path)}. Run #{set_color("bin/setup", :yellow)} in that directory to get started. @@ -99,7 +100,7 @@ def run # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity def_delegators :shell, :say, :set_color def continue_if(question) - if prompt.yes?(question) + if prompt.yes?("#{question} ↵") say else say "Canceled", :red @@ -121,7 +122,7 @@ def rails_version def ask_rails_version selected = prompt.select( - "What version of Rails will you use?", + "What #{underline("version")} of Rails will you use?", Rails.version => :current, "edge (#{Rails.edge_branch} branch)" => :edge ) @@ -129,24 +130,21 @@ def ask_rails_version end def ask_database - common_databases = { + databases = { "SQLite3 (default)" => "sqlite3", "PostgreSQL (recommended)" => "postgresql", - "MySQL" => "mysql" - } - all_databases = common_databases.merge( - %w[MySQL Trilogy Oracle SQLServer JDBCMySQL JDBCSQLite3 JDBCPostgreSQL JDBC].to_h do |name| + **%w[MySQL Trilogy Oracle SQLServer JDBCMySQL JDBCSQLite3 JDBCPostgreSQL JDBC].to_h do |name| [name, name.downcase] end, "None (disable Active Record)" => nil + } + rails_opts.database = prompt_select( + "Which #{underline("database")}?", databases ) - rails_opts.database = - prompt.select("Which database?", common_databases.merge("More options..." => false)) || - prompt.select("Which database?", all_databases) end def ask_full_stack_or_api - api = prompt.select( + api = prompt_select( "What style of Rails app do you need?", "Standard, full-stack Rails (default)" => false, "API only" => true @@ -155,12 +153,13 @@ def ask_full_stack_or_api end def ask_frontend_management - frontend = prompt.select( - "How will you manage frontend assets?", + frontend = prompt_select( + "How will you manage frontend #{underline("assets")}?", "Sprockets (default)" => "sprockets", "Propshaft" => "propshaft", "Vite" => :vite ) + if frontend == :vite rails_opts.asset_pipeline = nil rails_opts.javascript = "vite" @@ -170,8 +169,8 @@ def ask_frontend_management end def ask_css - rails_opts.css = prompt.select( - "Which CSS framework will you use with the asset pipeline?", + rails_opts.css = prompt_select( + "Which #{underline("CSS")} framework will you use with the asset pipeline?", "None (default)" => nil, "Bootstrap" => "bootstrap", "Bulma" => "bulma", @@ -182,8 +181,8 @@ def ask_css end def ask_javascript - rails_opts.javascript = prompt.select( - "Which JavaScript bundler will you use with the asset pipeline?", + rails_opts.javascript = prompt_select( + "Which #{underline("JavaScript")} bundler will you use with the asset pipeline?", "Importmap (default)" => "importmap", "Bun" => "bun", "ESBuild" => "esbuild", @@ -214,7 +213,7 @@ def ask_rails_frameworks end answers = prompt.multi_select( - "Which optional Rails frameworks do you need?", + "Which optional Rails #{underline("frameworks")} do you need?", frameworks, default: frameworks.keys.reverse ) @@ -223,8 +222,8 @@ def ask_rails_frameworks end def ask_test_framework - rails_opts.test_framework = prompt.select( - "Which test framework will you use?", + rails_opts.test_framework = prompt_select( + "Which #{underline("test")} framework will you use?", "Minitest (default)" => "minitest", "RSpec" => "rspec", "None" => nil @@ -232,8 +231,8 @@ def ask_test_framework end def ask_system_testing - system_testing = prompt.select( - "Include system testing (capybara)?", + system_testing = prompt_select( + "Include #{underline("system testing")} (capybara)?", "Yes (default)" => true, "No" => false ) @@ -309,5 +308,10 @@ def prompt def shell @shell ||= Thor::Base.shell.new end + + def green(string) = set_color(string, :green) + def cyan(string) = set_color(string, :cyan) + def underline(string) = Rainbow(string).underline + def prompt_select(question, choices) = prompt.select(question, choices, enum: ".", cycle: true) end end From 55b9d7a299c83e4fba97f6d8c4947ba2935af2d8 Mon Sep 17 00:00:00 2001 From: zhandao Date: Tue, 12 Mar 2024 17:58:35 +0800 Subject: [PATCH 2/7] Job Backend generator --- README.md | 8 +- config/job.yml | 10 +++ lib/nextgen/commands/create.rb | 158 +++++---------------------------- lib/nextgen/generators.rb | 17 ++-- lib/nextgen/helpers.rb | 143 +++++++++++++++++++++++++++++ 5 files changed, 194 insertions(+), 142 deletions(-) create mode 100644 config/job.yml create mode 100644 lib/nextgen/helpers.rb diff --git a/README.md b/README.md index 9aedf61..fb53478 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,13 @@ Prefer RSpec? Nextgen can set you up with RSpec, plus the gems and configuration Nextgen can install and configure your choice of these recommended gems: +#### Job Backends + +- [sidekiq](https://github.com/sidekiq/sidekiq) +- [solid_queue](https://github.com/basecamp/solid_queue) + +#### Other + - [annotate](https://github.com/ctran/annotate_models) - [brakeman](https://github.com/presidentbeef/brakeman) - [bundler-audit](https://github.com/rubysec/bundler-audit) @@ -90,7 +97,6 @@ Nextgen can install and configure your choice of these recommended gems: - [rack-mini-profiler](https://github.com/MiniProfiler/rack-mini-profiler) - [rubocop](https://github.com/rubocop/rubocop) - [shoulda-matchers](https://github.com/thoughtbot/shoulda-matchers) -- [sidekiq](https://github.com/sidekiq/sidekiq) - [thor](https://github.com/rails/thor) - [tomo](https://github.com/mattbrictson/tomo) - [vcr](https://github.com/vcr/vcr) diff --git a/config/job.yml b/config/job.yml new file mode 100644 index 0000000..c5083a5 --- /dev/null +++ b/config/job.yml @@ -0,0 +1,10 @@ + +sidekiq: + prompt: "Sidekiq (Redis-backed)" + description: "Install sidekiq gem to use in production" + requires: active_job + +solid_queue: + prompt: "SolidQueue (Database-backed)" + description: "Install solid_queue as ActiveJob's backend" + requires: active_job diff --git a/lib/nextgen/commands/create.rb b/lib/nextgen/commands/create.rb index 665d454..4f89a04 100644 --- a/lib/nextgen/commands/create.rb +++ b/lib/nextgen/commands/create.rb @@ -10,8 +10,9 @@ require "nextgen/ext/prompt/multilist" module Nextgen - class Commands::Create # rubocop:disable Metrics/ClassLength + class Commands::Create extend Forwardable + include Helpers def self.run(app_path, options) new(app_path, options).run @@ -23,19 +24,8 @@ def initialize(app_path, _options) @rails_opts = RailsOptions.new end - def run # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity - say <<~BANNER - Welcome to nextgen, the interactive Rails app generator! - - You are about to create a Rails app named "#{cyan(app_name)}" in the following directory: - - #{cyan(app_path)} - - You'll be asked ~10 questions about database, test framework, and other options. - The standard Rails "omakase" experience will be selected by default. - - BANNER - + def run # rubocop:disable Metrics/PerceivedComplexity + say_banner continue_if "Ready to start?" ask_rails_version @@ -47,79 +37,32 @@ def run # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity ask_rails_frameworks ask_test_framework ask_system_testing if rails_opts.frontend? && rails_opts.test_framework? - ask_optional_enhancements - - say <<~SUMMARY - - OK! Your Rails app is ready to be created. - The following options will be passed to `rails new`: - - #{rails_new_args.join("\n ")} - The following nextgen enhancements will also be applied in individual git commits via `rails app:template`: - - #{selected_generators.join(", ").scan(/\S.{0,75}(?:,|$)/).join("\n ")} - - SUMMARY - - if node? - say <<~NODE - Based on the options you selected, your app will require Node and Yarn. For reference, you are using these versions: - - Node: #{capture_version("node")} - Yarn: #{capture_version("yarn")} - - NODE + if prompt.yes?("More detailed configuration? [ cache, job and gems ] ↵") + ask_job_backend if rails_opts.active_job? + ask_optional_enhancements end + say_summary + say_node if node? continue_if "Continue?" create_initial_commit_message copy_package_json if node? Nextgen::Rails.run "new", *rails_new_args Dir.chdir(app_path) do - Nextgen::Rails.run "app:template", "LOCATION=#{write_generators_script}" + Nextgen::Rails.run "app:template", "LOCATION=#{write_generators_script(generators)}" + Nextgen::Rails.run "app:template", "LOCATION=#{write_generators_script(job_backend)}" end - - say <<~DONE.gsub(/^/, " ") - - - #{green("Done!")} - - A Rails #{rails_version} app was generated in #{cyan(app_path)}. - Run #{set_color("bin/setup", :yellow)} in that directory to get started. - - - DONE + say_done end private - attr_accessor :app_path, :app_name, :rails_opts, :generators + attr_accessor :app_path, :app_name, :rails_opts, :generators, :job_backend def_delegators :shell, :say, :set_color - def continue_if(question) - if prompt.yes?("#{question} ↵") - say - else - say "Canceled", :red - exit - end - end - - def copy_package_json - FileUtils.mkdir_p(app_path) - FileUtils.cp( - Nextgen.template_path.join("package.json"), - File.join(app_path, "package.json") - ) - end - - def rails_version - rails_opts.edge? ? "edge (#{Rails.edge_branch} branch)" : Rails.version - end - def ask_rails_version selected = prompt.select( "What #{underline("version")} of Rails will you use?", @@ -239,6 +182,16 @@ def ask_system_testing rails_opts.skip_system_test! unless system_testing end + def ask_job_backend + @job_backend = Generators.compatible_with(rails_opts: rails_opts, scope: "job") + + answer = prompt_select( + "Which #{underline("job backend")} would you like to use?", + job_backend.optional + ) + job_backend.activate(answer) + end + def ask_optional_enhancements @generators = Generators.compatible_with(rails_opts: rails_opts) @@ -248,70 +201,5 @@ def ask_optional_enhancements ) generators.activate(*answers) end - - def create_initial_commit_message - path = File.join(app_path, "tmp", "initial_nextgen_commit") - FileUtils.mkdir_p(File.dirname(path)) - File.write(path, <<~COMMIT) - Init project with `rails new` (#{Nextgen::Rails.version}) - - Nextgen generated this project with the following `rails new` options: - - ``` - #{rails_opts.to_args.join("\n")} - ``` - COMMIT - end - - def rails_new_args - [app_path, "--no-rc", *rails_opts.to_args].tap do |args| - # Work around a Rails bug where --edge causes --no-rc to get ignored. - # Specifying --rc= with a non-existent file has the same effect as --no-rc. - @rc_token ||= SecureRandom.hex(8) - args << "--rc=#{@rc_token}" if rails_opts.edge? - end - end - - def node? - generators.node_active? - end - - def capture_version(command) - out, _err, status = Open3.capture3(command, "--version") - version = status.success? && out[/\d[.\d]+\d/] - - version || "" - end - - def selected_generators - optional = generators.optional.invert - selected = generators.all_active.filter_map { |name| optional[name] } - - selected.any? ? selected.sort_by(&:downcase) : [""] - end - - def write_generators_script - new_tempfile_path.tap do |location| - File.write(location, generators.to_ruby_script) - end - end - - def new_tempfile_path - token = SecureRandom.hex(8) - File.join(Dir.tmpdir, "nextgen_create_#{token}.rb") - end - - def prompt - @prompt ||= TTY::Prompt.new - end - - def shell - @shell ||= Thor::Base.shell.new - end - - def green(string) = set_color(string, :green) - def cyan(string) = set_color(string, :cyan) - def underline(string) = Rainbow(string).underline - def prompt_select(question, choices) = prompt.select(question, choices, enum: ".", cycle: true) end end diff --git a/lib/nextgen/generators.rb b/lib/nextgen/generators.rb index 946f193..5bd6dae 100644 --- a/lib/nextgen/generators.rb +++ b/lib/nextgen/generators.rb @@ -2,15 +2,15 @@ module Nextgen class Generators - def self.compatible_with(rails_opts:) - yaml_path = File.expand_path("../../config/generators.yml", __dir__) - new.tap do |g| + def self.compatible_with(rails_opts:, scope: "generators") + yaml_path = File.expand_path("../../config/#{scope}.yml", __dir__) + new.tap do |itself| YAML.load_file(yaml_path).each do |name, options| options ||= {} requirements = Array(options["requires"]) next unless requirements.all? { |req| rails_opts.public_send(:"#{req}?") } - g.add( + itself.add( name.to_sym, prompt: options["prompt"], description: options["description"], @@ -18,7 +18,7 @@ def self.compatible_with(rails_opts:) ) end - g.deactivate_node unless rails_opts.requires_node? + itself.deactivate_node unless rails_opts.requires_node? end end @@ -36,6 +36,11 @@ def all_active end end + def all_active_names + opts = optional.invert + all_active.filter_map { |name| opts[name] } + end + def add(name, node: false, prompt: nil, description: nil) name = name.to_sym raise ArgumentError, "Generator #{name.inspect} was already added" if generators.key?(name) @@ -60,7 +65,7 @@ def activate(*optional_generators) end def deactivate_node - generators.fetch(:node)[:active] = false + generators[:node][:active] = false if generators.key?(:node) end def to_ruby_script diff --git a/lib/nextgen/helpers.rb b/lib/nextgen/helpers.rb new file mode 100644 index 0000000..d9d664f --- /dev/null +++ b/lib/nextgen/helpers.rb @@ -0,0 +1,143 @@ +module Nextgen + module Helpers + private + + def say_banner + say <<~BANNER + Welcome to nextgen, the interactive Rails app generator! + + You are about to create a Rails app named "#{cyan(app_name)}" in the following directory: + + #{cyan(app_path)} + + You'll be asked ~10 questions about database, test framework, and other options. + The standard Rails "omakase" experience will be selected by default. + + BANNER + end + + def say_summary + say <<~SUMMARY + + OK! Your Rails app is ready to be created. + The following options will be passed to `rails new`: + + #{rails_new_args.join("\n ")} + + The following nextgen enhancements will also be applied in individual git commits via `rails app:template`: + + #{activated_generators.join(", ").scan(/\S.{0,75}(?:,|$)/).join("\n ")} + + SUMMARY + end + + def say_node + say <<~NODE + Based on the options you selected, your app will require Node and Yarn. For reference, you are using these versions: + + Node: #{capture_version("node")} + Yarn: #{capture_version("yarn")} + + NODE + end + + def say_done + say <<~DONE.gsub(/^/, " ") + + + #{green("Done!")} + + A Rails #{rails_version} app was generated in #{cyan(app_path)}. + Run #{set_color("bin/setup", :yellow)} in that directory to get started. + + + DONE + end + + def continue_if(question) + if prompt.yes?("#{question} ↵") + say + else + say "Canceled", :red + exit + end + end + + def copy_package_json + FileUtils.mkdir_p(app_path) + FileUtils.cp( + Nextgen.template_path.join("package.json"), + File.join(app_path, "package.json") + ) + end + + def rails_version + rails_opts.edge? ? "edge (#{Rails.edge_branch} branch)" : Rails.version + end + + def create_initial_commit_message + path = File.join(app_path, "tmp", "initial_nextgen_commit") + FileUtils.mkdir_p(File.dirname(path)) + File.write(path, <<~COMMIT) + Init project with `rails new` (#{Nextgen::Rails.version}) + + Nextgen generated this project with the following `rails new` options: + + ``` + #{rails_opts.to_args.join("\n")} + ``` + COMMIT + end + + def rails_new_args + [app_path, "--no-rc", *rails_opts.to_args].tap do |args| + # Work around a Rails bug where --edge causes --no-rc to get ignored. + # Specifying --rc= with a non-existent file has the same effect as --no-rc. + @rc_token ||= SecureRandom.hex(8) + args << "--rc=#{@rc_token}" if rails_opts.edge? + end + end + + def node? + generators.node_active? + end + + def capture_version(command) + out, _err, status = Open3.capture3(command, "--version") + version = status.success? && out[/\d[.\d]+\d/] + + version || "" + end + + def activated_generators + activated = generators.all_active_names + activated.prepend(job_backend.all_active_names.first) unless job_backend.nil? + + activated.any? ? activated.sort_by(&:downcase) : [""] + end + + def write_generators_script(g) + new_tempfile_path.tap do |location| + File.write(location, g.to_ruby_script) + end + end + + def new_tempfile_path + token = SecureRandom.hex(8) + File.join(Dir.tmpdir, "nextgen_create_#{token}.rb") + end + + def prompt + @prompt ||= TTY::Prompt.new + end + + def shell + @shell ||= Thor::Base.shell.new + end + + def green(string) = set_color(string, :green) + def cyan(string) = set_color(string, :cyan) + def underline(string) = Rainbow(string).underline + def prompt_select(question, choices) = prompt.select(question, choices, enum: ".", cycle: true) + end +end From 4a537e2cba0e9aafafa966cf1afc1755cdf1ff57 Mon Sep 17 00:00:00 2001 From: zhandao Date: Tue, 12 Mar 2024 21:37:48 +0800 Subject: [PATCH 3/7] Impl solid_queue generator --- lib/nextgen/commands/create.rb | 7 +++++- lib/nextgen/{ => commands}/helpers.rb | 2 +- lib/nextgen/generators.rb | 5 +++++ lib/nextgen/generators/solid_queue.rb | 31 +++++++++++++++++++++++++++ template/config/solid_queue.yml | 18 ++++++++++++++++ 5 files changed, 61 insertions(+), 2 deletions(-) rename lib/nextgen/{ => commands}/helpers.rb (99%) create mode 100644 lib/nextgen/generators/solid_queue.rb create mode 100644 template/config/solid_queue.yml diff --git a/lib/nextgen/commands/create.rb b/lib/nextgen/commands/create.rb index 4f89a04..2c5180c 100644 --- a/lib/nextgen/commands/create.rb +++ b/lib/nextgen/commands/create.rb @@ -12,7 +12,7 @@ module Nextgen class Commands::Create extend Forwardable - include Helpers + include Commands::Helpers def self.run(app_path, options) new(app_path, options).run @@ -189,6 +189,11 @@ def ask_job_backend "Which #{underline("job backend")} would you like to use?", job_backend.optional ) + + if answer == :solid_queue && prompt.no?(" ↪ Run the SolidQueue supervisor together with Puma (as plugin)?") + job_backend.variables[:solid_queue_puma] = true + end + job_backend.activate(answer) end diff --git a/lib/nextgen/helpers.rb b/lib/nextgen/commands/helpers.rb similarity index 99% rename from lib/nextgen/helpers.rb rename to lib/nextgen/commands/helpers.rb index d9d664f..8c49648 100644 --- a/lib/nextgen/helpers.rb +++ b/lib/nextgen/commands/helpers.rb @@ -1,4 +1,4 @@ -module Nextgen +module Nextgen::Commands module Helpers private diff --git a/lib/nextgen/generators.rb b/lib/nextgen/generators.rb index 5bd6dae..38705b4 100644 --- a/lib/nextgen/generators.rb +++ b/lib/nextgen/generators.rb @@ -18,12 +18,16 @@ def self.compatible_with(rails_opts:, scope: "generators") ) end + itself.variables[:api] = rails_opts.api? itself.deactivate_node unless rails_opts.requires_node? end end + attr_accessor :variables + def initialize @generators = {} + @variables = {} end def node_active? @@ -78,6 +82,7 @@ def to_ruby_script <<~SCRIPT require #{File.expand_path("../nextgen", __dir__).inspect} extend Nextgen::Actions + @variables = #{@variables} with_nextgen_source_path do #{apply_statements.join("\n ")} diff --git a/lib/nextgen/generators/solid_queue.rb b/lib/nextgen/generators/solid_queue.rb new file mode 100644 index 0000000..22ba403 --- /dev/null +++ b/lib/nextgen/generators/solid_queue.rb @@ -0,0 +1,31 @@ +say_git "Install solid_queue as ActiveJob's backend" +install_gem "solid_queue", version: "~> 0.2" + +say_git "Add a solid_queue entry to the Procfile" +if @variables[:solid_queue_puma] + append_to_file "config/puma.rb", "plugin :solid_queue\n" +else + append_to_file "Procfile", "worker: bundle exec rake solid_queue:start\n" +end + +say_git "Configure Active Job to use the solid_queue adapter" +uncomment_lines "config/environments/production.rb", /config\.active_job/ +gsub_file "config/environments/production.rb", + /active_job\.queue_adapter\s*=\s*:.+/, + "active_job.queue_adapter = :solid_queue" +uncomment_lines "config/environments/development.rb", /config\.active_job/ +gsub_file "config/environments/production.rb", + /active_job\.queue_adapter\s*=\s*:.+/, + "active_job.queue_adapter = :solid_queue" +copy_file "config/solid_queue.yml" + +say_git "Add the SolidQueue migrations" +system "rails", "solid_queue:install:migrations", exception: true + +unless @variables[:api] + say_git "Mount the SolidQueue web console at /jobs" + install_gem "mission_control-jobs" + route "# See [https://github.com/basecamp/mission_control-jobs#authentication-and-base-controller-class]" + route '# MissionControl::Jobs.base_controller_class = "AdminController"' + route 'mount MissionControl::Jobs::Engine, at: "/jobs"' +end diff --git a/template/config/solid_queue.yml b/template/config/solid_queue.yml new file mode 100644 index 0000000..7a33b9c --- /dev/null +++ b/template/config/solid_queue.yml @@ -0,0 +1,18 @@ + default: &default + dispatchers: + - polling_interval: 1 + batch_size: 500 + workers: + - queues: "*" + threads: 5 + processes: <%= Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count }) %> + polling_interval: 0.1 + + development: + <<: *default + + test: + <<: *default + + production: + <<: *default From 66e5285fa7b2f039d68d71f6f534b6fdc65c2976 Mon Sep 17 00:00:00 2001 From: zhandao Date: Tue, 12 Mar 2024 23:23:12 +0800 Subject: [PATCH 4/7] Second level questions support --- config/generators.yml | 4 ++ config/job.yml | 4 ++ lib/nextgen.rb | 4 +- lib/nextgen/commands/create.rb | 36 ++++++----------- lib/nextgen/commands/helpers.rb | 3 +- lib/nextgen/generators.rb | 39 ++++++++++++++----- lib/nextgen/generators/basic/tailwind.rb | 4 ++ lib/nextgen/generators/{ => job}/sidekiq.rb | 0 .../generators/{ => job}/solid_queue.rb | 13 ++++--- lib/nextgen/generators/node.rb | 12 +++--- 10 files changed, 70 insertions(+), 49 deletions(-) create mode 100644 lib/nextgen/generators/basic/tailwind.rb rename lib/nextgen/generators/{ => job}/sidekiq.rb (100%) rename lib/nextgen/generators/{ => job}/solid_queue.rb (83%) diff --git a/config/generators.yml b/config/generators.yml index 74e5c4f..988f14b 100644 --- a/config/generators.yml +++ b/config/generators.yml @@ -137,6 +137,10 @@ vcr: prompt: "VCR" description: "Install and configure vcr and webmock gems" requires: test_framework + questions: + - variable: solid_queue_puma__no + method: no? + question: "Run the SolidQueue supervisor together with Puma (as plugin)?" rubocop: prompt: "RuboCop" diff --git a/config/job.yml b/config/job.yml index c5083a5..c549437 100644 --- a/config/job.yml +++ b/config/job.yml @@ -8,3 +8,7 @@ solid_queue: prompt: "SolidQueue (Database-backed)" description: "Install solid_queue as ActiveJob's backend" requires: active_job + questions: + - variable: solid_queue_puma__no + method: no? + question: "Run the SolidQueue supervisor together with Puma (as plugin)?" diff --git a/lib/nextgen.rb b/lib/nextgen.rb index df2e244..7f8c2a2 100644 --- a/lib/nextgen.rb +++ b/lib/nextgen.rb @@ -7,8 +7,8 @@ loader.setup module Nextgen - def self.generators_path - Pathname.new(__dir__).join("nextgen/generators") + def self.generators_path(scope = "") + Pathname.new(__dir__).join("nextgen/generators", scope) end def self.template_path diff --git a/lib/nextgen/commands/create.rb b/lib/nextgen/commands/create.rb index 2c5180c..2239fba 100644 --- a/lib/nextgen/commands/create.rb +++ b/lib/nextgen/commands/create.rb @@ -37,6 +37,7 @@ def run # rubocop:disable Metrics/PerceivedComplexity ask_rails_frameworks ask_test_framework ask_system_testing if rails_opts.frontend? && rails_opts.test_framework? + say if prompt.yes?("More detailed configuration? [ cache, job and gems ] ↵") ask_job_backend if rails_opts.active_job? @@ -81,13 +82,13 @@ def ask_database end, "None (disable Active Record)" => nil } - rails_opts.database = prompt_select( + rails_opts.database = select( "Which #{underline("database")}?", databases ) end def ask_full_stack_or_api - api = prompt_select( + api = select( "What style of Rails app do you need?", "Standard, full-stack Rails (default)" => false, "API only" => true @@ -96,7 +97,7 @@ def ask_full_stack_or_api end def ask_frontend_management - frontend = prompt_select( + frontend = select( "How will you manage frontend #{underline("assets")}?", "Sprockets (default)" => "sprockets", "Propshaft" => "propshaft", @@ -112,7 +113,7 @@ def ask_frontend_management end def ask_css - rails_opts.css = prompt_select( + rails_opts.css = select( "Which #{underline("CSS")} framework will you use with the asset pipeline?", "None (default)" => nil, "Bootstrap" => "bootstrap", @@ -124,7 +125,7 @@ def ask_css end def ask_javascript - rails_opts.javascript = prompt_select( + rails_opts.javascript = select( "Which #{underline("JavaScript")} bundler will you use with the asset pipeline?", "Importmap (default)" => "importmap", "Bun" => "bun", @@ -155,7 +156,7 @@ def ask_rails_frameworks ) end - answers = prompt.multi_select( + answers = multi_select( "Which optional Rails #{underline("frameworks")} do you need?", frameworks, default: frameworks.keys.reverse @@ -165,7 +166,7 @@ def ask_rails_frameworks end def ask_test_framework - rails_opts.test_framework = prompt_select( + rails_opts.test_framework = select( "Which #{underline("test")} framework will you use?", "Minitest (default)" => "minitest", "RSpec" => "rspec", @@ -174,7 +175,7 @@ def ask_test_framework end def ask_system_testing - system_testing = prompt_select( + system_testing = select( "Include #{underline("system testing")} (capybara)?", "Yes (default)" => true, "No" => false @@ -184,27 +185,12 @@ def ask_system_testing def ask_job_backend @job_backend = Generators.compatible_with(rails_opts: rails_opts, scope: "job") - - answer = prompt_select( - "Which #{underline("job backend")} would you like to use?", - job_backend.optional - ) - - if answer == :solid_queue && prompt.no?(" ↪ Run the SolidQueue supervisor together with Puma (as plugin)?") - job_backend.variables[:solid_queue_puma] = true - end - - job_backend.activate(answer) + job_backend.ask_select("Which #{underline("job backend")} would you like to use?") end def ask_optional_enhancements @generators = Generators.compatible_with(rails_opts: rails_opts) - - answers = prompt.multi_select( - "Which optional enhancements would you like to add?", - generators.optional.sort_by { |label, _| label.downcase }.to_h - ) - generators.activate(*answers) + generators.ask_select("Which optional enhancements would you like to add?", multi: true, sort: true) end end end diff --git a/lib/nextgen/commands/helpers.rb b/lib/nextgen/commands/helpers.rb index 8c49648..8341b2e 100644 --- a/lib/nextgen/commands/helpers.rb +++ b/lib/nextgen/commands/helpers.rb @@ -138,6 +138,7 @@ def shell def green(string) = set_color(string, :green) def cyan(string) = set_color(string, :cyan) def underline(string) = Rainbow(string).underline - def prompt_select(question, choices) = prompt.select(question, choices, enum: ".", cycle: true) + def select(question, choices) = prompt.select(question, choices, enum: ".", cycle: true) + def multi_select(question, choices, **opts) = prompt.multi_select(question, choices, filter: true, cycle: true, **opts) end end diff --git a/lib/nextgen/generators.rb b/lib/nextgen/generators.rb index 38705b4..5bf47fc 100644 --- a/lib/nextgen/generators.rb +++ b/lib/nextgen/generators.rb @@ -4,30 +4,49 @@ module Nextgen class Generators def self.compatible_with(rails_opts:, scope: "generators") yaml_path = File.expand_path("../../config/#{scope}.yml", __dir__) - new.tap do |itself| + new(scope).tap do |generators| YAML.load_file(yaml_path).each do |name, options| options ||= {} requirements = Array(options["requires"]) next unless requirements.all? { |req| rails_opts.public_send(:"#{req}?") } - itself.add( + generators.add( name.to_sym, prompt: options["prompt"], description: options["description"], - node: !!options["node"] + node: !!options["node"], + questions: options["questions"] ) end - itself.variables[:api] = rails_opts.api? - itself.deactivate_node unless rails_opts.requires_node? + generators.variables[:api] = rails_opts.api? + generators.deactivate_node unless rails_opts.requires_node? end end attr_accessor :variables - def initialize + def initialize(scope) @generators = {} @variables = {} + @scope = scope + end + + def ask_select(question, multi: false, sort: false) + prompt = TTY::Prompt.new + opt = sort ? optional.sort_by { |label, _| label.downcase }.to_h : optional + args = [question, opt, {cycle: true, filter: true}] + answers = multi ? prompt.multi_select(*args) : [prompt.select(*args)] + + answers.each do |answer| + second_level_questions = generators[answer][:questions] || [] + second_level_questions.each do |q| + variables[q.fetch("variable")] = prompt.public_send( + q.fetch("method"), " ↪ #{q.fetch("question")}" + ) + end + end + activate(*answers) end def node_active? @@ -45,11 +64,11 @@ def all_active_names all_active.filter_map { |name| opts[name] } end - def add(name, node: false, prompt: nil, description: nil) + def add(name, node: false, prompt: nil, description: nil, questions: nil) name = name.to_sym raise ArgumentError, "Generator #{name.inspect} was already added" if generators.key?(name) - generators[name] = {node: node, prompt: prompt, description: description} + generators[name] = {node: node, prompt: prompt, description: description, questions: questions} activate(name) unless prompt end @@ -75,7 +94,7 @@ def deactivate_node def to_ruby_script apply_statements = all_active.map do |generator| description = generators.fetch(generator)[:description] - path = Nextgen.generators_path.join("#{generator}.rb") + path = Nextgen.generators_path(scope).join("#{generator}.rb") "apply_as_git_commit #{path.to_s.inspect}, message: #{description.inspect}" end @@ -92,6 +111,6 @@ def to_ruby_script private - attr_reader :generators + attr_reader :generators, :scope end end diff --git a/lib/nextgen/generators/basic/tailwind.rb b/lib/nextgen/generators/basic/tailwind.rb new file mode 100644 index 0000000..90f4aee --- /dev/null +++ b/lib/nextgen/generators/basic/tailwind.rb @@ -0,0 +1,4 @@ +if @variables[:tailwind_puma] + say_git "Add tailwindcss plugin to puma.rb" + append_to_file "config/puma.rb", "plugin :tailwindcss\n" +end diff --git a/lib/nextgen/generators/sidekiq.rb b/lib/nextgen/generators/job/sidekiq.rb similarity index 100% rename from lib/nextgen/generators/sidekiq.rb rename to lib/nextgen/generators/job/sidekiq.rb diff --git a/lib/nextgen/generators/solid_queue.rb b/lib/nextgen/generators/job/solid_queue.rb similarity index 83% rename from lib/nextgen/generators/solid_queue.rb rename to lib/nextgen/generators/job/solid_queue.rb index 22ba403..7bf537c 100644 --- a/lib/nextgen/generators/solid_queue.rb +++ b/lib/nextgen/generators/job/solid_queue.rb @@ -1,11 +1,12 @@ say_git "Install solid_queue as ActiveJob's backend" install_gem "solid_queue", version: "~> 0.2" -say_git "Add a solid_queue entry to the Procfile" -if @variables[:solid_queue_puma] - append_to_file "config/puma.rb", "plugin :solid_queue\n" -else +if @variables[:solid_queue_puma__no] + say_git "Add a solid_queue entry to the Procfile" append_to_file "Procfile", "worker: bundle exec rake solid_queue:start\n" +else + say_git "Add solid_queue plugin to puma.rb" + append_to_file "config/puma.rb", "plugin :solid_queue\n" end say_git "Configure Active Job to use the solid_queue adapter" @@ -19,11 +20,11 @@ "active_job.queue_adapter = :solid_queue" copy_file "config/solid_queue.yml" -say_git "Add the SolidQueue migrations" +say_git "Add the solid_queue migrations" system "rails", "solid_queue:install:migrations", exception: true unless @variables[:api] - say_git "Mount the SolidQueue web console at /jobs" + say_git "Mount the solid_queue web console at /jobs" install_gem "mission_control-jobs" route "# See [https://github.com/basecamp/mission_control-jobs#authentication-and-base-controller-class]" route '# MissionControl::Jobs.base_controller_class = "AdminController"' diff --git a/lib/nextgen/generators/node.rb b/lib/nextgen/generators/node.rb index a771e2e..794ae0e 100644 --- a/lib/nextgen/generators/node.rb +++ b/lib/nextgen/generators/node.rb @@ -1,5 +1,7 @@ -say_git "Add Node and Yarn prerequisites" -copy_file "package.json" unless File.exist?("package.json") -inject_into_file "README.md", "\n- Node 18 (LTS) or newer\n- Yarn 1.x (classic)", after: /^- Ruby.*$/ -inject_into_file "README.md", "\nbrew install node\nbrew install yarn", after: /^brew install rbenv.*$/ -gitignore "node_modules/" +unless File.read(".gitignore").match?("node_modules") + say_git "Add Node and Yarn prerequisites" + copy_file "package.json" unless File.exist?("package.json") + inject_into_file "README.md", "\n- Node 18 (LTS) or newer\n- Yarn 1.x (classic)", after: /^- Ruby.*$/ + inject_into_file "README.md", "\nbrew install node\nbrew install yarn", after: /^brew install rbenv.*$/ + gitignore "node_modules/" +end From 4a8b76dde03151a1970c15e119d7214aed0fd00c Mon Sep 17 00:00:00 2001 From: zhandao Date: Wed, 13 Mar 2024 00:54:29 +0800 Subject: [PATCH 5/7] Grouping generators --- config/basic.yml | 29 ++++ config/checkers.yml | 41 +++++ config/code_snippets.yml | 8 + config/gems.yml | 69 ++++++++ config/generators.yml | 155 ------------------ config/job.yml | 2 +- config/workflows.yml | 8 + lib/nextgen/commands/create.rb | 42 ++++- lib/nextgen/commands/helpers.rb | 8 +- lib/nextgen/generators.rb | 22 +-- .../generators/{ => basic}/action_mailer.rb | 0 lib/nextgen/generators/{ => basic}/base.rb | 0 .../generators/{ => basic}/clean_gemfile.rb | 0 .../generators/{ => basic}/git_safe.rb | 0 .../{ => basic}/initial_git_commit.rb | 0 .../generators/{ => basic}/rspec_rails.rb | 0 .../{ => basic}/rspec_system_testing.rb | 0 lib/nextgen/generators/basic/tailwind.rb | 5 +- .../generators/{ => checkers}/brakeman.rb | 0 .../{ => checkers}/bundler_audit.rb | 0 .../generators/{ => checkers}/erb_lint.rb | 0 .../generators/{ => checkers}/eslint.rb | 0 .../{ => checkers}/good_migrations.rb | 0 lib/nextgen/generators/{ => checkers}/node.rb | 0 .../generators/{ => checkers}/overcommit.rb | 0 .../generators/{ => checkers}/rubocop.rb | 0 .../generators/{ => checkers}/stylelint.rb | 0 .../{ => code_snippets}/basic_auth.rb | 0 .../{ => code_snippets}/home_controller.rb | 0 lib/nextgen/generators/{ => gems}/annotate.rb | 0 .../{ => gems}/capybara_lockstep.rb | 0 lib/nextgen/generators/{ => gems}/dotenv.rb | 0 .../{ => gems}/factory_bot_rails.rb | 0 .../generators/{ => gems}/letter_opener.rb | 0 lib/nextgen/generators/gems/node.rb | 7 + .../{ => gems}/open_browser_on_start.rb | 0 .../generators/{ => gems}/pgcli_rails.rb | 0 .../{ => gems}/rack_canonical_host.rb | 0 .../{ => gems}/rack_mini_profiler.rb | 0 lib/nextgen/generators/{ => gems}/shoulda.rb | 0 lib/nextgen/generators/{ => gems}/thor.rb | 0 lib/nextgen/generators/{ => gems}/tomo.rb | 0 lib/nextgen/generators/{ => gems}/vcr.rb | 0 lib/nextgen/generators/{ => gems}/vite.rb | 0 lib/nextgen/generators/job/solid_queue.rb | 12 +- .../{ => workflows}/github_actions.rb | 0 .../{ => workflows}/github_pr_template.rb | 0 lib/nextgen/rails_options.rb | 4 + 48 files changed, 225 insertions(+), 187 deletions(-) create mode 100644 config/basic.yml create mode 100644 config/checkers.yml create mode 100644 config/code_snippets.yml create mode 100644 config/gems.yml delete mode 100644 config/generators.yml create mode 100644 config/workflows.yml rename lib/nextgen/generators/{ => basic}/action_mailer.rb (100%) rename lib/nextgen/generators/{ => basic}/base.rb (100%) rename lib/nextgen/generators/{ => basic}/clean_gemfile.rb (100%) rename lib/nextgen/generators/{ => basic}/git_safe.rb (100%) rename lib/nextgen/generators/{ => basic}/initial_git_commit.rb (100%) rename lib/nextgen/generators/{ => basic}/rspec_rails.rb (100%) rename lib/nextgen/generators/{ => basic}/rspec_system_testing.rb (100%) rename lib/nextgen/generators/{ => checkers}/brakeman.rb (100%) rename lib/nextgen/generators/{ => checkers}/bundler_audit.rb (100%) rename lib/nextgen/generators/{ => checkers}/erb_lint.rb (100%) rename lib/nextgen/generators/{ => checkers}/eslint.rb (100%) rename lib/nextgen/generators/{ => checkers}/good_migrations.rb (100%) rename lib/nextgen/generators/{ => checkers}/node.rb (100%) rename lib/nextgen/generators/{ => checkers}/overcommit.rb (100%) rename lib/nextgen/generators/{ => checkers}/rubocop.rb (100%) rename lib/nextgen/generators/{ => checkers}/stylelint.rb (100%) rename lib/nextgen/generators/{ => code_snippets}/basic_auth.rb (100%) rename lib/nextgen/generators/{ => code_snippets}/home_controller.rb (100%) rename lib/nextgen/generators/{ => gems}/annotate.rb (100%) rename lib/nextgen/generators/{ => gems}/capybara_lockstep.rb (100%) rename lib/nextgen/generators/{ => gems}/dotenv.rb (100%) rename lib/nextgen/generators/{ => gems}/factory_bot_rails.rb (100%) rename lib/nextgen/generators/{ => gems}/letter_opener.rb (100%) create mode 100644 lib/nextgen/generators/gems/node.rb rename lib/nextgen/generators/{ => gems}/open_browser_on_start.rb (100%) rename lib/nextgen/generators/{ => gems}/pgcli_rails.rb (100%) rename lib/nextgen/generators/{ => gems}/rack_canonical_host.rb (100%) rename lib/nextgen/generators/{ => gems}/rack_mini_profiler.rb (100%) rename lib/nextgen/generators/{ => gems}/shoulda.rb (100%) rename lib/nextgen/generators/{ => gems}/thor.rb (100%) rename lib/nextgen/generators/{ => gems}/tomo.rb (100%) rename lib/nextgen/generators/{ => gems}/vcr.rb (100%) rename lib/nextgen/generators/{ => gems}/vite.rb (100%) rename lib/nextgen/generators/{ => workflows}/github_actions.rb (100%) rename lib/nextgen/generators/{ => workflows}/github_pr_template.rb (100%) diff --git a/config/basic.yml b/config/basic.yml new file mode 100644 index 0000000..71c20e3 --- /dev/null +++ b/config/basic.yml @@ -0,0 +1,29 @@ +initial_git_commit: + +base: + description: "Enhance base Rails template with better docs, etc" + +clean_gemfile: + description: "Clean up Gemfile" + +git_safe: + +action_mailer: + description: "Improve Action Mailer support for absolute URLs and testing" + requires: action_mailer + +rspec_rails: + description: "Install and configure rspec-rails" + requires: rspec + +rspec_system_testing: + description: "Install capybara + selenium-webdriver and set up system specs" + requires: + - rspec + - system_testing + +tailwind: + questions: + - variable: tailwind_puma__no + method: no? + question: "Integrate tailwind watch with Puma (as plugin)? *defaults to NO*" diff --git a/config/checkers.yml b/config/checkers.yml new file mode 100644 index 0000000..ed583e4 --- /dev/null +++ b/config/checkers.yml @@ -0,0 +1,41 @@ + +node: + description: "Set up Node and Yarn" + +brakeman: + prompt: "Brakeman" + description: "Install brakeman gem for security checks" + +bundler_audit: + prompt: "Bundler Audit" + description: "Install bundler-audit gem to detect CVEs in Ruby dependencies" + +erb_lint: + prompt: "ERB Lint" + description: "Install erb_lint gem and correct existing issues" + requires: frontend + +eslint: + prompt: "ESLint" + description: "Install eslint + supporting packages; apply prettier format" + requires: frontend + node: true + +good_migrations: + prompt: "good_migrations" + description: "Install good_migrations gem" + requires: active_record + +stylelint: + prompt: "Stylelint" + description: "Install stylelint and apply prettier format to CSS" + requires: frontend + node: true + +rubocop: + prompt: "RuboCop" + description: "Install rubocop gems; apply formatting rules" + +overcommit: + prompt: "Overcommit" + description: "Configure overcommit pre-commit git hooks" diff --git a/config/code_snippets.yml b/config/code_snippets.yml new file mode 100644 index 0000000..b384807 --- /dev/null +++ b/config/code_snippets.yml @@ -0,0 +1,8 @@ + +home_controller: + description: "Create a controller, view, and route for the home page" + requires: frontend + +basic_auth: + prompt: "BasicAuth controller concern" + description: "Allow app to be secured with ENV-based basic auth credentials" diff --git a/config/gems.yml b/config/gems.yml new file mode 100644 index 0000000..1106faf --- /dev/null +++ b/config/gems.yml @@ -0,0 +1,69 @@ + +node: + description: "Set up Node and Yarn" + +vite: + description: "Replace the asset pipeline with Vite in app/frontend" + requires: vite + node: true + +annotate: + prompt: "Annotate Models" + description: "Install annotate gem to auto-generate schema annotations" + requires: active_record + +capybara_lockstep: + prompt: "capybara-lockstep" + description: "Install capybara-lockstep gem for less-flaky browser testing" + requires: system_testing + +dotenv: + prompt: "dotenv" + description: "Install dotenv gem and add .env.sample" + +factory_bot_rails: + prompt: "Factory Bot" + description: "Install and configure factory_bot_rails gem" + requires: active_record + +letter_opener: + prompt: "letter_opener" + description: "Install letter_opener gem to use with Action Mailer in dev" + requires: action_mailer + +open_browser_on_start: + prompt: "Open browser on startup" + description: "Configure puma to launch browser on startup in development" + requires: frontend + +pgcli_rails: + prompt: "pgcli_rails" + description: "Install pgcli_rails gem to allow easy use of pgcli" + requires: postgresql + +rack_canonical_host: + prompt: "rack-canonical-host" + description: "Install rack-canonical-host gem; use RAILS_HOSTNAME" + +rack_mini_profiler: + prompt: "rack-mini-profiler" + description: "Install rack-mini-profiler gem in development" + requires: frontend + +shoulda: + prompt: "shoulda" + description: "Install shoulda-matchers gem for concise model testing" + requires: test_framework + +thor: + prompt: "Thor" + description: "Configure Thor for ease of writing Rails tasks" + +tomo: + prompt: "Tomo" + description: "Install tomo gem for SSH-based deployment" + +vcr: + prompt: "VCR" + description: "Install and configure vcr and webmock gems" + requires: test_framework diff --git a/config/generators.yml b/config/generators.yml deleted file mode 100644 index 988f14b..0000000 --- a/config/generators.yml +++ /dev/null @@ -1,155 +0,0 @@ -initial_git_commit: - -base: - description: "Enhance base Rails template with better docs, etc" - -clean_gemfile: - description: "Clean up Gemfile" - -rspec_rails: - description: "Install and configure rspec-rails" - requires: rspec - -rspec_system_testing: - description: "Install capybara + selenium-webdriver and set up system specs" - requires: - - rspec - - system_testing - -node: - description: "Set up Node and Yarn" - -vite: - description: "Replace the asset pipeline with Vite in app/frontend" - requires: vite - node: true - -action_mailer: - description: "Improve Action Mailer support for absolute URLs and testing" - requires: action_mailer - -annotate: - prompt: "Annotate Models" - description: "Install annotate gem to auto-generate schema annotations" - requires: active_record - -basic_auth: - prompt: "BasicAuth controller concern" - description: "Allow app to be secured with ENV-based basic auth credentials" - -brakeman: - prompt: "Brakeman" - description: "Install brakeman gem for security checks" - -bundler_audit: - prompt: "Bundler Audit" - description: "Install bundler-audit gem to detect CVEs in Ruby dependencies" - -capybara_lockstep: - prompt: "capybara-lockstep" - description: "Install capybara-lockstep gem for less-flaky browser testing" - requires: system_testing - -dotenv: - prompt: "dotenv" - description: "Install dotenv gem and add .env.sample" - -erb_lint: - prompt: "ERB Lint" - description: "Install erb_lint gem and correct existing issues" - requires: frontend - -eslint: - prompt: "ESLint" - description: "Install eslint + supporting packages; apply prettier format" - requires: frontend - node: true - -factory_bot_rails: - prompt: "Factory Bot" - description: "Install and configure factory_bot_rails gem" - requires: active_record - -github_pr_template: - prompt: "GitHub PR template" - description: "Add GitHub pull request template" - -git_safe: - -good_migrations: - prompt: "good_migrations" - description: "Install good_migrations gem" - requires: active_record - -home_controller: - description: "Create a controller, view, and route for the home page" - requires: frontend - -letter_opener: - prompt: "letter_opener" - description: "Install letter_opener gem to use with Action Mailer in dev" - requires: action_mailer - -open_browser_on_start: - prompt: "Open browser on startup" - description: "Configure puma to launch browser on startup in development" - requires: frontend - -pgcli_rails: - prompt: "pgcli_rails" - description: "Install pgcli_rails gem to allow easy use of pgcli" - requires: postgresql - -rack_canonical_host: - prompt: "rack-canonical-host" - description: "Install rack-canonical-host gem; use RAILS_HOSTNAME" - -rack_mini_profiler: - prompt: "rack-mini-profiler" - description: "Install rack-mini-profiler gem in development" - requires: frontend - -shoulda: - prompt: "shoulda" - description: "Install shoulda-matchers gem for concise model testing" - requires: test_framework - -sidekiq: - prompt: "Sidekiq" - description: "Install sidekiq gem to use in production" - requires: active_job - -stylelint: - prompt: "Stylelint" - description: "Install stylelint and apply prettier format to CSS" - requires: frontend - node: true - -thor: - prompt: "Thor" - description: "Configure Thor for ease of writing Rails tasks" - -tomo: - prompt: "Tomo" - description: "Install tomo gem for SSH-based deployment" - -vcr: - prompt: "VCR" - description: "Install and configure vcr and webmock gems" - requires: test_framework - questions: - - variable: solid_queue_puma__no - method: no? - question: "Run the SolidQueue supervisor together with Puma (as plugin)?" - -rubocop: - prompt: "RuboCop" - description: "Install rubocop gems; apply formatting rules" - -overcommit: - prompt: "Overcommit" - description: "Configure overcommit pre-commit git hooks" - -github_actions: - prompt: "GitHub Actions" - description: "Configure GitHub Actions workflow for CI" diff --git a/config/job.yml b/config/job.yml index c549437..71d2fe9 100644 --- a/config/job.yml +++ b/config/job.yml @@ -11,4 +11,4 @@ solid_queue: questions: - variable: solid_queue_puma__no method: no? - question: "Run the SolidQueue supervisor together with Puma (as plugin)?" + question: "Run the SolidQueue supervisor together with Puma (as plugin)? *defaults to NO*" diff --git a/config/workflows.yml b/config/workflows.yml new file mode 100644 index 0000000..56fadfb --- /dev/null +++ b/config/workflows.yml @@ -0,0 +1,8 @@ + +github_actions: + prompt: "GitHub Actions" + description: "Configure GitHub Actions workflow for CI" + +github_pr_template: + prompt: "GitHub PR template" + description: "Add GitHub pull request template" diff --git a/lib/nextgen/commands/create.rb b/lib/nextgen/commands/create.rb index 2239fba..599ddd1 100644 --- a/lib/nextgen/commands/create.rb +++ b/lib/nextgen/commands/create.rb @@ -22,9 +22,10 @@ def initialize(app_path, _options) @app_path = File.expand_path(app_path) @app_name = File.basename(@app_path).gsub(/\W/, "_").squeeze("_").camelize @rails_opts = RailsOptions.new + @generators = {basic: Generators.compatible_with(rails_opts: rails_opts, scope: "basic")} end - def run # rubocop:disable Metrics/PerceivedComplexity + def run # rubocop:disable Metrics/MethodLength Metrics/PerceivedComplexity say_banner continue_if "Ready to start?" @@ -41,6 +42,9 @@ def run # rubocop:disable Metrics/PerceivedComplexity if prompt.yes?("More detailed configuration? [ cache, job and gems ] ↵") ask_job_backend if rails_opts.active_job? + ask_workflows + ask_checkers + ask_code_snippets ask_optional_enhancements end @@ -52,15 +56,16 @@ def run # rubocop:disable Metrics/PerceivedComplexity copy_package_json if node? Nextgen::Rails.run "new", *rails_new_args Dir.chdir(app_path) do - Nextgen::Rails.run "app:template", "LOCATION=#{write_generators_script(generators)}" - Nextgen::Rails.run "app:template", "LOCATION=#{write_generators_script(job_backend)}" + generators.each_value do |g| + Nextgen::Rails.run "app:template", "LOCATION=#{write_generators_script(g)}" + end end say_done end private - attr_accessor :app_path, :app_name, :rails_opts, :generators, :job_backend + attr_accessor :app_path, :app_name, :rails_opts, :generators def_delegators :shell, :say, :set_color @@ -122,6 +127,7 @@ def ask_css "Sass" => "sass", "Tailwind" => "tailwind" ) + generators[:basic].ask_second_level_questions(for_selected: rails_opts.css, prompt: prompt) end def ask_javascript @@ -184,13 +190,33 @@ def ask_system_testing end def ask_job_backend - @job_backend = Generators.compatible_with(rails_opts: rails_opts, scope: "job") - job_backend.ask_select("Which #{underline("job backend")} would you like to use?") + generators[:job] = Generators.compatible_with(rails_opts: rails_opts, scope: "job").tap do |it| + it.ask_select("Which #{underline("job backend")} would you like to use?", prompt: prompt) + end + end + + def ask_workflows + generators[:workflows] = Generators.compatible_with(rails_opts: rails_opts, scope: "workflows").tap do |it| + it.ask_select("Which #{underline("workflows")} would you like to add?", multi: true, prompt: prompt) + end + end + + def ask_checkers + generators[:checkers] = Generators.compatible_with(rails_opts: rails_opts, scope: "checkers").tap do |it| + it.ask_select("Which #{underline("checkers")} would you like to add?", multi: true, prompt: prompt) + end + end + + def ask_code_snippets + generators[:code_snippets] = Generators.compatible_with(rails_opts: rails_opts, scope: "code_snippets").tap do |it| + it.ask_select("Which #{underline("code snippets")} would you like to add?", multi: true, prompt: prompt) + end end def ask_optional_enhancements - @generators = Generators.compatible_with(rails_opts: rails_opts) - generators.ask_select("Which optional enhancements would you like to add?", multi: true, sort: true) + generators[:gems] = Generators.compatible_with(rails_opts: rails_opts, scope: "gems").tap do |it| + it.ask_select("Which optional enhancements would you like to add?", multi: true, sort: true, prompt: prompt) + end end end end diff --git a/lib/nextgen/commands/helpers.rb b/lib/nextgen/commands/helpers.rb index 8341b2e..4005e27 100644 --- a/lib/nextgen/commands/helpers.rb +++ b/lib/nextgen/commands/helpers.rb @@ -10,7 +10,7 @@ def say_banner #{cyan(app_path)} - You'll be asked ~10 questions about database, test framework, and other options. + You'll be asked ~10 (or more) questions about database, test framework, and other options. The standard Rails "omakase" experience will be selected by default. BANNER @@ -99,7 +99,7 @@ def rails_new_args end def node? - generators.node_active? + generators.values.any?(&:node_active?) end def capture_version(command) @@ -110,8 +110,8 @@ def capture_version(command) end def activated_generators - activated = generators.all_active_names - activated.prepend(job_backend.all_active_names.first) unless job_backend.nil? + activated = generators[:gems].all_active_names + activated.prepend(generators[:job].all_active_names.first) unless generators[:job].nil? activated.any? ? activated.sort_by(&:downcase) : [""] end diff --git a/lib/nextgen/generators.rb b/lib/nextgen/generators.rb index 5bf47fc..ac1137d 100644 --- a/lib/nextgen/generators.rb +++ b/lib/nextgen/generators.rb @@ -2,7 +2,7 @@ module Nextgen class Generators - def self.compatible_with(rails_opts:, scope: "generators") + def self.compatible_with(rails_opts:, scope:) yaml_path = File.expand_path("../../config/#{scope}.yml", __dir__) new(scope).tap do |generators| YAML.load_file(yaml_path).each do |name, options| @@ -32,25 +32,27 @@ def initialize(scope) @scope = scope end - def ask_select(question, multi: false, sort: false) - prompt = TTY::Prompt.new + def ask_select(question, multi: false, sort: false, prompt: TTY::Prompt.new) opt = sort ? optional.sort_by { |label, _| label.downcase }.to_h : optional args = [question, opt, {cycle: true, filter: true}] answers = multi ? prompt.multi_select(*args) : [prompt.select(*args)] answers.each do |answer| - second_level_questions = generators[answer][:questions] || [] - second_level_questions.each do |q| - variables[q.fetch("variable")] = prompt.public_send( - q.fetch("method"), " ↪ #{q.fetch("question")}" - ) - end + ask_second_level_questions(for_selected: answer, prompt: prompt) end activate(*answers) end + def ask_second_level_questions(for_selected:, prompt:) + (generators[for_selected&.to_sym]&.[](:questions) || []).each do |q| + variables[q.fetch("variable").to_sym] = prompt.public_send( + q.fetch("method"), " ↪ #{q.fetch("question")}" + ) + end + end + def node_active? - !!generators.fetch(:node)[:active] + !!generators[:node]&.[](:active) end def all_active diff --git a/lib/nextgen/generators/action_mailer.rb b/lib/nextgen/generators/basic/action_mailer.rb similarity index 100% rename from lib/nextgen/generators/action_mailer.rb rename to lib/nextgen/generators/basic/action_mailer.rb diff --git a/lib/nextgen/generators/base.rb b/lib/nextgen/generators/basic/base.rb similarity index 100% rename from lib/nextgen/generators/base.rb rename to lib/nextgen/generators/basic/base.rb diff --git a/lib/nextgen/generators/clean_gemfile.rb b/lib/nextgen/generators/basic/clean_gemfile.rb similarity index 100% rename from lib/nextgen/generators/clean_gemfile.rb rename to lib/nextgen/generators/basic/clean_gemfile.rb diff --git a/lib/nextgen/generators/git_safe.rb b/lib/nextgen/generators/basic/git_safe.rb similarity index 100% rename from lib/nextgen/generators/git_safe.rb rename to lib/nextgen/generators/basic/git_safe.rb diff --git a/lib/nextgen/generators/initial_git_commit.rb b/lib/nextgen/generators/basic/initial_git_commit.rb similarity index 100% rename from lib/nextgen/generators/initial_git_commit.rb rename to lib/nextgen/generators/basic/initial_git_commit.rb diff --git a/lib/nextgen/generators/rspec_rails.rb b/lib/nextgen/generators/basic/rspec_rails.rb similarity index 100% rename from lib/nextgen/generators/rspec_rails.rb rename to lib/nextgen/generators/basic/rspec_rails.rb diff --git a/lib/nextgen/generators/rspec_system_testing.rb b/lib/nextgen/generators/basic/rspec_system_testing.rb similarity index 100% rename from lib/nextgen/generators/rspec_system_testing.rb rename to lib/nextgen/generators/basic/rspec_system_testing.rb diff --git a/lib/nextgen/generators/basic/tailwind.rb b/lib/nextgen/generators/basic/tailwind.rb index 90f4aee..3a86c8a 100644 --- a/lib/nextgen/generators/basic/tailwind.rb +++ b/lib/nextgen/generators/basic/tailwind.rb @@ -1,4 +1,5 @@ -if @variables[:tailwind_puma] + +unless @variables[:tailwind_puma__no] say_git "Add tailwindcss plugin to puma.rb" - append_to_file "config/puma.rb", "plugin :tailwindcss\n" + append_to_file "config/puma.rb", %(plugin :tailwindcss if ENV.fetch("RAILS_ENV", "development") == "development"\n) end diff --git a/lib/nextgen/generators/brakeman.rb b/lib/nextgen/generators/checkers/brakeman.rb similarity index 100% rename from lib/nextgen/generators/brakeman.rb rename to lib/nextgen/generators/checkers/brakeman.rb diff --git a/lib/nextgen/generators/bundler_audit.rb b/lib/nextgen/generators/checkers/bundler_audit.rb similarity index 100% rename from lib/nextgen/generators/bundler_audit.rb rename to lib/nextgen/generators/checkers/bundler_audit.rb diff --git a/lib/nextgen/generators/erb_lint.rb b/lib/nextgen/generators/checkers/erb_lint.rb similarity index 100% rename from lib/nextgen/generators/erb_lint.rb rename to lib/nextgen/generators/checkers/erb_lint.rb diff --git a/lib/nextgen/generators/eslint.rb b/lib/nextgen/generators/checkers/eslint.rb similarity index 100% rename from lib/nextgen/generators/eslint.rb rename to lib/nextgen/generators/checkers/eslint.rb diff --git a/lib/nextgen/generators/good_migrations.rb b/lib/nextgen/generators/checkers/good_migrations.rb similarity index 100% rename from lib/nextgen/generators/good_migrations.rb rename to lib/nextgen/generators/checkers/good_migrations.rb diff --git a/lib/nextgen/generators/node.rb b/lib/nextgen/generators/checkers/node.rb similarity index 100% rename from lib/nextgen/generators/node.rb rename to lib/nextgen/generators/checkers/node.rb diff --git a/lib/nextgen/generators/overcommit.rb b/lib/nextgen/generators/checkers/overcommit.rb similarity index 100% rename from lib/nextgen/generators/overcommit.rb rename to lib/nextgen/generators/checkers/overcommit.rb diff --git a/lib/nextgen/generators/rubocop.rb b/lib/nextgen/generators/checkers/rubocop.rb similarity index 100% rename from lib/nextgen/generators/rubocop.rb rename to lib/nextgen/generators/checkers/rubocop.rb diff --git a/lib/nextgen/generators/stylelint.rb b/lib/nextgen/generators/checkers/stylelint.rb similarity index 100% rename from lib/nextgen/generators/stylelint.rb rename to lib/nextgen/generators/checkers/stylelint.rb diff --git a/lib/nextgen/generators/basic_auth.rb b/lib/nextgen/generators/code_snippets/basic_auth.rb similarity index 100% rename from lib/nextgen/generators/basic_auth.rb rename to lib/nextgen/generators/code_snippets/basic_auth.rb diff --git a/lib/nextgen/generators/home_controller.rb b/lib/nextgen/generators/code_snippets/home_controller.rb similarity index 100% rename from lib/nextgen/generators/home_controller.rb rename to lib/nextgen/generators/code_snippets/home_controller.rb diff --git a/lib/nextgen/generators/annotate.rb b/lib/nextgen/generators/gems/annotate.rb similarity index 100% rename from lib/nextgen/generators/annotate.rb rename to lib/nextgen/generators/gems/annotate.rb diff --git a/lib/nextgen/generators/capybara_lockstep.rb b/lib/nextgen/generators/gems/capybara_lockstep.rb similarity index 100% rename from lib/nextgen/generators/capybara_lockstep.rb rename to lib/nextgen/generators/gems/capybara_lockstep.rb diff --git a/lib/nextgen/generators/dotenv.rb b/lib/nextgen/generators/gems/dotenv.rb similarity index 100% rename from lib/nextgen/generators/dotenv.rb rename to lib/nextgen/generators/gems/dotenv.rb diff --git a/lib/nextgen/generators/factory_bot_rails.rb b/lib/nextgen/generators/gems/factory_bot_rails.rb similarity index 100% rename from lib/nextgen/generators/factory_bot_rails.rb rename to lib/nextgen/generators/gems/factory_bot_rails.rb diff --git a/lib/nextgen/generators/letter_opener.rb b/lib/nextgen/generators/gems/letter_opener.rb similarity index 100% rename from lib/nextgen/generators/letter_opener.rb rename to lib/nextgen/generators/gems/letter_opener.rb diff --git a/lib/nextgen/generators/gems/node.rb b/lib/nextgen/generators/gems/node.rb new file mode 100644 index 0000000..794ae0e --- /dev/null +++ b/lib/nextgen/generators/gems/node.rb @@ -0,0 +1,7 @@ +unless File.read(".gitignore").match?("node_modules") + say_git "Add Node and Yarn prerequisites" + copy_file "package.json" unless File.exist?("package.json") + inject_into_file "README.md", "\n- Node 18 (LTS) or newer\n- Yarn 1.x (classic)", after: /^- Ruby.*$/ + inject_into_file "README.md", "\nbrew install node\nbrew install yarn", after: /^brew install rbenv.*$/ + gitignore "node_modules/" +end diff --git a/lib/nextgen/generators/open_browser_on_start.rb b/lib/nextgen/generators/gems/open_browser_on_start.rb similarity index 100% rename from lib/nextgen/generators/open_browser_on_start.rb rename to lib/nextgen/generators/gems/open_browser_on_start.rb diff --git a/lib/nextgen/generators/pgcli_rails.rb b/lib/nextgen/generators/gems/pgcli_rails.rb similarity index 100% rename from lib/nextgen/generators/pgcli_rails.rb rename to lib/nextgen/generators/gems/pgcli_rails.rb diff --git a/lib/nextgen/generators/rack_canonical_host.rb b/lib/nextgen/generators/gems/rack_canonical_host.rb similarity index 100% rename from lib/nextgen/generators/rack_canonical_host.rb rename to lib/nextgen/generators/gems/rack_canonical_host.rb diff --git a/lib/nextgen/generators/rack_mini_profiler.rb b/lib/nextgen/generators/gems/rack_mini_profiler.rb similarity index 100% rename from lib/nextgen/generators/rack_mini_profiler.rb rename to lib/nextgen/generators/gems/rack_mini_profiler.rb diff --git a/lib/nextgen/generators/shoulda.rb b/lib/nextgen/generators/gems/shoulda.rb similarity index 100% rename from lib/nextgen/generators/shoulda.rb rename to lib/nextgen/generators/gems/shoulda.rb diff --git a/lib/nextgen/generators/thor.rb b/lib/nextgen/generators/gems/thor.rb similarity index 100% rename from lib/nextgen/generators/thor.rb rename to lib/nextgen/generators/gems/thor.rb diff --git a/lib/nextgen/generators/tomo.rb b/lib/nextgen/generators/gems/tomo.rb similarity index 100% rename from lib/nextgen/generators/tomo.rb rename to lib/nextgen/generators/gems/tomo.rb diff --git a/lib/nextgen/generators/vcr.rb b/lib/nextgen/generators/gems/vcr.rb similarity index 100% rename from lib/nextgen/generators/vcr.rb rename to lib/nextgen/generators/gems/vcr.rb diff --git a/lib/nextgen/generators/vite.rb b/lib/nextgen/generators/gems/vite.rb similarity index 100% rename from lib/nextgen/generators/vite.rb rename to lib/nextgen/generators/gems/vite.rb diff --git a/lib/nextgen/generators/job/solid_queue.rb b/lib/nextgen/generators/job/solid_queue.rb index 7bf537c..994f1af 100644 --- a/lib/nextgen/generators/job/solid_queue.rb +++ b/lib/nextgen/generators/job/solid_queue.rb @@ -10,14 +10,12 @@ end say_git "Configure Active Job to use the solid_queue adapter" -uncomment_lines "config/environments/production.rb", /config\.active_job/ gsub_file "config/environments/production.rb", - /active_job\.queue_adapter\s*=\s*:.+/, - "active_job.queue_adapter = :solid_queue" -uncomment_lines "config/environments/development.rb", /config\.active_job/ -gsub_file "config/environments/production.rb", - /active_job\.queue_adapter\s*=\s*:.+/, - "active_job.queue_adapter = :solid_queue" + /(# )?config\.active_job\.queue_adapter\s+=.*/, + "config.active_job.queue_adapter = :solid_queue" +inject_into_file "config/environments/development.rb", + " config.active_job.queue_adapter = :solid_queue\n", + after: "config.active_job.verbose_enqueue_logs = true\n" copy_file "config/solid_queue.yml" say_git "Add the solid_queue migrations" diff --git a/lib/nextgen/generators/github_actions.rb b/lib/nextgen/generators/workflows/github_actions.rb similarity index 100% rename from lib/nextgen/generators/github_actions.rb rename to lib/nextgen/generators/workflows/github_actions.rb diff --git a/lib/nextgen/generators/github_pr_template.rb b/lib/nextgen/generators/workflows/github_pr_template.rb similarity index 100% rename from lib/nextgen/generators/github_pr_template.rb rename to lib/nextgen/generators/workflows/github_pr_template.rb diff --git a/lib/nextgen/rails_options.rb b/lib/nextgen/rails_options.rb index 78fc696..8a2e7de 100644 --- a/lib/nextgen/rails_options.rb +++ b/lib/nextgen/rails_options.rb @@ -156,6 +156,10 @@ def active_job? !skip_optional_framework?("active_job") end + def tailwind? + css == "tailwind" + end + def skip_optional_framework!(framework) raise ArgumentError, "Unknown framework: #{framework}" unless OPTIONAL_FRAMEWORKS.include?(framework) From 4275a014924833188fd480f811b00ab9617c9dbb Mon Sep 17 00:00:00 2001 From: zhandao Date: Wed, 13 Mar 2024 01:40:44 +0800 Subject: [PATCH 6/7] Add code snippets `Current` --- config/code_snippets.yml | 4 ++++ lib/nextgen/commands/create.rb | 4 ++-- lib/nextgen/generators.rb | 11 ++++------- lib/nextgen/generators/code_snippets/current.rb | 1 + template/app/models/current.rb | 3 +++ 5 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 lib/nextgen/generators/code_snippets/current.rb create mode 100644 template/app/models/current.rb diff --git a/config/code_snippets.yml b/config/code_snippets.yml index b384807..b9f5f53 100644 --- a/config/code_snippets.yml +++ b/config/code_snippets.yml @@ -6,3 +6,7 @@ home_controller: basic_auth: prompt: "BasicAuth controller concern" description: "Allow app to be secured with ENV-based basic auth credentials" + +current: + prompt: "Current.user" + description: "Intro thread-isolated attributes singleton `Current`, which resets automatically before and after each request" diff --git a/lib/nextgen/commands/create.rb b/lib/nextgen/commands/create.rb index 599ddd1..de7b086 100644 --- a/lib/nextgen/commands/create.rb +++ b/lib/nextgen/commands/create.rb @@ -22,7 +22,6 @@ def initialize(app_path, _options) @app_path = File.expand_path(app_path) @app_name = File.basename(@app_path).gsub(/\W/, "_").squeeze("_").camelize @rails_opts = RailsOptions.new - @generators = {basic: Generators.compatible_with(rails_opts: rails_opts, scope: "basic")} end def run # rubocop:disable Metrics/MethodLength Metrics/PerceivedComplexity @@ -40,7 +39,7 @@ def run # rubocop:disable Metrics/MethodLength Metrics/PerceivedComplexity ask_system_testing if rails_opts.frontend? && rails_opts.test_framework? say - if prompt.yes?("More detailed configuration? [ cache, job and gems ] ↵") + if prompt.yes?("More detailed configuration? [ job, code snippets, gems ... ] ↵") ask_job_backend if rails_opts.active_job? ask_workflows ask_checkers @@ -99,6 +98,7 @@ def ask_full_stack_or_api "API only" => true ) rails_opts.api! if api + @generators = {basic: Generators.compatible_with(rails_opts: rails_opts, scope: "basic")} end def ask_frontend_management diff --git a/lib/nextgen/generators.rb b/lib/nextgen/generators.rb index ac1137d..6342167 100644 --- a/lib/nextgen/generators.rb +++ b/lib/nextgen/generators.rb @@ -4,7 +4,7 @@ module Nextgen class Generators def self.compatible_with(rails_opts:, scope:) yaml_path = File.expand_path("../../config/#{scope}.yml", __dir__) - new(scope).tap do |generators| + new(scope, api: rails_opts.api?).tap do |generators| YAML.load_file(yaml_path).each do |name, options| options ||= {} requirements = Array(options["requires"]) @@ -19,16 +19,13 @@ def self.compatible_with(rails_opts:, scope:) ) end - generators.variables[:api] = rails_opts.api? generators.deactivate_node unless rails_opts.requires_node? end end - attr_accessor :variables - - def initialize(scope) + def initialize(scope, **vars) @generators = {} - @variables = {} + @variables = vars @scope = scope end @@ -113,6 +110,6 @@ def to_ruby_script private - attr_reader :generators, :scope + attr_reader :generators, :variables, :scope end end diff --git a/lib/nextgen/generators/code_snippets/current.rb b/lib/nextgen/generators/code_snippets/current.rb new file mode 100644 index 0000000..7b5ec82 --- /dev/null +++ b/lib/nextgen/generators/code_snippets/current.rb @@ -0,0 +1 @@ +copy_file "app/models/current.rb" diff --git a/template/app/models/current.rb b/template/app/models/current.rb new file mode 100644 index 0000000..73a9744 --- /dev/null +++ b/template/app/models/current.rb @@ -0,0 +1,3 @@ +class Current < ActiveSupport::CurrentAttributes + attribute :user +end From ff2b6deceec7adb64ac13b6f33dbd0997795e7b8 Mon Sep 17 00:00:00 2001 From: zhandao Date: Fri, 15 Mar 2024 15:45:36 +0800 Subject: [PATCH 7/7] Impl `style` option --- README.md | 21 +++++--- config/job_backend.yml | 5 ++ config/styles/full/checkers.yml | 0 config/styles/full/code_snippets.yml | 0 config/styles/full/gems.yml | 0 .../{job.yml => styles/full/job_backend.yml} | 5 -- config/styles/full/workflows.yml | 0 lib/nextgen.rb | 24 +++++++++ lib/nextgen/cli.rb | 1 + lib/nextgen/commands/create.rb | 51 ++++++------------- lib/nextgen/commands/helpers.rb | 4 +- lib/nextgen/generators.rb | 12 ++--- .../{job => job_backend}/sidekiq.rb | 0 .../{job => job_backend}/solid_queue.rb | 0 14 files changed, 66 insertions(+), 57 deletions(-) create mode 100644 config/job_backend.yml create mode 100644 config/styles/full/checkers.yml create mode 100644 config/styles/full/code_snippets.yml create mode 100644 config/styles/full/gems.yml rename config/{job.yml => styles/full/job_backend.yml} (70%) create mode 100644 config/styles/full/workflows.yml rename lib/nextgen/generators/{job => job_backend}/sidekiq.rb (100%) rename lib/nextgen/generators/{job => job_backend}/solid_queue.rb (100%) diff --git a/README.md b/README.md index fb53478..03343bf 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,13 @@ gem exec nextgen create myapp This will download the latest version of the `nextgen` gem and use it to create an app in the `myapp` directory. You'll be asked to configure the tech stack through several interactive prompts. If you have a `~/.railsrc` file, it will be ignored. +Options: +- `style`: control the **optional enhancements** you can choose in the generator. + - defaults to `default`, [enhancements list](config) + - presets: + - `full` (`--style=full`), [enhancements list](config/styles/full) + - your local configs: `--style=path/to/your/style_dir` + > [!TIP] > If you get an "Unknown command exec" error, fix it by upgrading rubygems: `gem update --system`. @@ -59,7 +66,7 @@ Check out the [examples directory](./examples) to see some Rails apps that were On top of that foundation, Nextgen offers dozens of useful enhancements to the vanilla Rails experience. You are free to pick and choose which (if any) of these to apply to your new project. Behind the scenes, **each enhancement is applied in a separate git commit,** so that you can later see what was applied and why, and revert the suggestions if necessary. > [!TIP] -> For the full list of what Nextgen provides, check out [config/generators.yml](https://github.com/mattbrictson/nextgen/tree/main/config/generators.yml). The source code of each generator can be found in [lib/nextgen/generators](https://github.com/mattbrictson/nextgen/tree/main/lib/nextgen/generators). +> For the full list of what Nextgen provides, check out [config/*.yml](https://github.com/mattbrictson/nextgen/tree/main/config). The source code of each generator can be found in [lib/nextgen/generators](https://github.com/mattbrictson/nextgen/tree/main/lib/nextgen/generators). Here are some highlights of what Nextgen brings to the table: @@ -71,16 +78,14 @@ Nextgen can optionally set up a GitHub Actions CI workflow for your app that aut Prefer RSpec? Nextgen can set you up with RSpec, plus the gems and configuration you need for system specs (browser testing). Or stick with the Rails Minitest defaults. In either case, Nextgen will set up a good default Rake task and appropriate CI job. -### Gems - -Nextgen can install and configure your choice of these recommended gems: - -#### Job Backends +### Job Backends - [sidekiq](https://github.com/sidekiq/sidekiq) -- [solid_queue](https://github.com/basecamp/solid_queue) +- [solid_queue](https://github.com/basecamp/solid_queue) (`--style=full`) + +### Gems -#### Other +Nextgen can install and configure your choice of these recommended gems: - [annotate](https://github.com/ctran/annotate_models) - [brakeman](https://github.com/presidentbeef/brakeman) diff --git a/config/job_backend.yml b/config/job_backend.yml new file mode 100644 index 0000000..09ea387 --- /dev/null +++ b/config/job_backend.yml @@ -0,0 +1,5 @@ + +sidekiq: + prompt: "Sidekiq (Redis-backed)" + description: "Install sidekiq gem to use in production" + requires: active_job diff --git a/config/styles/full/checkers.yml b/config/styles/full/checkers.yml new file mode 100644 index 0000000..e69de29 diff --git a/config/styles/full/code_snippets.yml b/config/styles/full/code_snippets.yml new file mode 100644 index 0000000..e69de29 diff --git a/config/styles/full/gems.yml b/config/styles/full/gems.yml new file mode 100644 index 0000000..e69de29 diff --git a/config/job.yml b/config/styles/full/job_backend.yml similarity index 70% rename from config/job.yml rename to config/styles/full/job_backend.yml index 71d2fe9..bfe741f 100644 --- a/config/job.yml +++ b/config/styles/full/job_backend.yml @@ -1,9 +1,4 @@ -sidekiq: - prompt: "Sidekiq (Redis-backed)" - description: "Install sidekiq gem to use in production" - requires: active_job - solid_queue: prompt: "SolidQueue (Database-backed)" description: "Install solid_queue as ActiveJob's backend" diff --git a/config/styles/full/workflows.yml b/config/styles/full/workflows.yml new file mode 100644 index 0000000..e69de29 diff --git a/lib/nextgen.rb b/lib/nextgen.rb index 7f8c2a2..79349e9 100644 --- a/lib/nextgen.rb +++ b/lib/nextgen.rb @@ -14,4 +14,28 @@ def self.generators_path(scope = "") def self.template_path Pathname.new(__dir__).join("../template") end + + def self.config_path(style: nil) + if style + if style.match?("/") + Pathname.new(style) + else + Pathname.new(__dir__).join("../config/styles", style) + end + else + Pathname.new(__dir__).join("../config") + end + end + + def self.config_for(scope:, style: nil) + base = YAML.load_file("#{Nextgen.config_path}/#{scope}.yml") + if style + base.merge!(YAML.load_file("#{Nextgen.config_path(style: style)}/#{scope}.yml") || {}) + end + base + end + + def self.scopes_for(style: nil) + Dir[Nextgen.config_path(style: style) + "*.yml"].map { _1.match(/([_a-z]*)\.yml/)[1] } + end end diff --git a/lib/nextgen/cli.rb b/lib/nextgen/cli.rb index 4ba5a94..a4d5197 100644 --- a/lib/nextgen/cli.rb +++ b/lib/nextgen/cli.rb @@ -6,6 +6,7 @@ class CLI < Thor map %w[-v --version] => "version" + option :style, type: :string, default: nil desc "create APP_PATH", "Generate a Rails app interactively in APP_PATH" def create(app_path) Commands::Create.run(app_path, options) diff --git a/lib/nextgen/commands/create.rb b/lib/nextgen/commands/create.rb index de7b086..9bb2b66 100644 --- a/lib/nextgen/commands/create.rb +++ b/lib/nextgen/commands/create.rb @@ -18,13 +18,14 @@ def self.run(app_path, options) new(app_path, options).run end - def initialize(app_path, _options) + def initialize(app_path, options) @app_path = File.expand_path(app_path) @app_name = File.basename(@app_path).gsub(/\W/, "_").squeeze("_").camelize @rails_opts = RailsOptions.new + @style = options[:style] end - def run # rubocop:disable Metrics/MethodLength Metrics/PerceivedComplexity + def run # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity say_banner continue_if "Ready to start?" @@ -39,12 +40,8 @@ def run # rubocop:disable Metrics/MethodLength Metrics/PerceivedComplexity ask_system_testing if rails_opts.frontend? && rails_opts.test_framework? say - if prompt.yes?("More detailed configuration? [ job, code snippets, gems ... ] ↵") - ask_job_backend if rails_opts.active_job? - ask_workflows - ask_checkers - ask_code_snippets - ask_optional_enhancements + if prompt.yes?("More enhancements? [ job, code snippets, gems ... ] ↵") + ask_styled_enhancements end say_summary @@ -98,7 +95,7 @@ def ask_full_stack_or_api "API only" => true ) rails_opts.api! if api - @generators = {basic: Generators.compatible_with(rails_opts: rails_opts, scope: "basic")} + @generators = {basic: Generators.compatible_with(rails_opts: rails_opts, style: nil, scope: "basic")} end def ask_frontend_management @@ -189,33 +186,17 @@ def ask_system_testing rails_opts.skip_system_test! unless system_testing end - def ask_job_backend - generators[:job] = Generators.compatible_with(rails_opts: rails_opts, scope: "job").tap do |it| - it.ask_select("Which #{underline("job backend")} would you like to use?", prompt: prompt) - end - end - - def ask_workflows - generators[:workflows] = Generators.compatible_with(rails_opts: rails_opts, scope: "workflows").tap do |it| - it.ask_select("Which #{underline("workflows")} would you like to add?", multi: true, prompt: prompt) - end - end - - def ask_checkers - generators[:checkers] = Generators.compatible_with(rails_opts: rails_opts, scope: "checkers").tap do |it| - it.ask_select("Which #{underline("checkers")} would you like to add?", multi: true, prompt: prompt) - end - end - - def ask_code_snippets - generators[:code_snippets] = Generators.compatible_with(rails_opts: rails_opts, scope: "code_snippets").tap do |it| - it.ask_select("Which #{underline("code snippets")} would you like to add?", multi: true, prompt: prompt) - end - end + def ask_styled_enhancements + say " ↪ style: #{cyan(@style || "default")}" + Nextgen.scopes_for(style: @style).each do |scope| + gen = Generators.compatible_with(rails_opts: rails_opts, style: @style, scope: scope) + next if gen.empty? || scope == "basic" - def ask_optional_enhancements - generators[:gems] = Generators.compatible_with(rails_opts: rails_opts, scope: "gems").tap do |it| - it.ask_select("Which optional enhancements would you like to add?", multi: true, sort: true, prompt: prompt) + key_word = underline(scope.tr("_", " ")) + multi = scope == scope.pluralize + sort = gen.optional.size > 10 + gen.ask_select("Which #{key_word} would you like to add?", prompt: prompt, multi: multi, sort: sort) + generators[scope.to_sym] = gen end end end diff --git a/lib/nextgen/commands/helpers.rb b/lib/nextgen/commands/helpers.rb index 4005e27..fcf9566 100644 --- a/lib/nextgen/commands/helpers.rb +++ b/lib/nextgen/commands/helpers.rb @@ -110,9 +110,7 @@ def capture_version(command) end def activated_generators - activated = generators[:gems].all_active_names - activated.prepend(generators[:job].all_active_names.first) unless generators[:job].nil? - + activated = generators.values.flat_map(&:all_active_names) activated.any? ? activated.sort_by(&:downcase) : [""] end diff --git a/lib/nextgen/generators.rb b/lib/nextgen/generators.rb index 6342167..0105871 100644 --- a/lib/nextgen/generators.rb +++ b/lib/nextgen/generators.rb @@ -2,10 +2,9 @@ module Nextgen class Generators - def self.compatible_with(rails_opts:, scope:) - yaml_path = File.expand_path("../../config/#{scope}.yml", __dir__) + def self.compatible_with(rails_opts:, style:, scope:) new(scope, api: rails_opts.api?).tap do |generators| - YAML.load_file(yaml_path).each do |name, options| + Nextgen.config_for(style: style, scope: scope).each do |name, options| options ||= {} requirements = Array(options["requires"]) next unless requirements.all? { |req| rails_opts.public_send(:"#{req}?") } @@ -18,7 +17,6 @@ def self.compatible_with(rails_opts:, scope:) questions: options["questions"] ) end - generators.deactivate_node unless rails_opts.requires_node? end end @@ -29,9 +27,11 @@ def initialize(scope, **vars) @scope = scope end + def empty? = @generators.empty? + def ask_select(question, multi: false, sort: false, prompt: TTY::Prompt.new) - opt = sort ? optional.sort_by { |label, _| label.downcase }.to_h : optional - args = [question, opt, {cycle: true, filter: true}] + opts = sort ? optional.sort_by { |label, _| label.downcase }.to_h : optional + args = [question, opts, {cycle: true, filter: true}] answers = multi ? prompt.multi_select(*args) : [prompt.select(*args)] answers.each do |answer| diff --git a/lib/nextgen/generators/job/sidekiq.rb b/lib/nextgen/generators/job_backend/sidekiq.rb similarity index 100% rename from lib/nextgen/generators/job/sidekiq.rb rename to lib/nextgen/generators/job_backend/sidekiq.rb diff --git a/lib/nextgen/generators/job/solid_queue.rb b/lib/nextgen/generators/job_backend/solid_queue.rb similarity index 100% rename from lib/nextgen/generators/job/solid_queue.rb rename to lib/nextgen/generators/job_backend/solid_queue.rb