nextgen 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +143 -0
  4. data/config/generators.yml +146 -0
  5. data/exe/nextgen +13 -0
  6. data/lib/nextgen/actions/bundler.rb +62 -0
  7. data/lib/nextgen/actions/git.rb +56 -0
  8. data/lib/nextgen/actions/yarn.rb +32 -0
  9. data/lib/nextgen/actions.rb +155 -0
  10. data/lib/nextgen/cli.rb +19 -0
  11. data/lib/nextgen/commands/create.rb +312 -0
  12. data/lib/nextgen/ext/prompt/list.rb +13 -0
  13. data/lib/nextgen/ext/prompt/multilist.rb +16 -0
  14. data/lib/nextgen/generators/action_mailer.rb +26 -0
  15. data/lib/nextgen/generators/annotate.rb +2 -0
  16. data/lib/nextgen/generators/base.rb +60 -0
  17. data/lib/nextgen/generators/basic_auth.rb +5 -0
  18. data/lib/nextgen/generators/brakeman.rb +2 -0
  19. data/lib/nextgen/generators/bundler_audit.rb +2 -0
  20. data/lib/nextgen/generators/capybara_lockstep.rb +4 -0
  21. data/lib/nextgen/generators/clean_gemfile.rb +1 -0
  22. data/lib/nextgen/generators/dotenv.rb +3 -0
  23. data/lib/nextgen/generators/erb_lint.rb +11 -0
  24. data/lib/nextgen/generators/eslint.rb +24 -0
  25. data/lib/nextgen/generators/factory_bot_rails.rb +16 -0
  26. data/lib/nextgen/generators/git_safe.rb +4 -0
  27. data/lib/nextgen/generators/github_actions.rb +2 -0
  28. data/lib/nextgen/generators/good_migrations.rb +1 -0
  29. data/lib/nextgen/generators/home_controller.rb +3 -0
  30. data/lib/nextgen/generators/initial_git_commit.rb +5 -0
  31. data/lib/nextgen/generators/initial_migrations.rb +11 -0
  32. data/lib/nextgen/generators/letter_opener.rb +8 -0
  33. data/lib/nextgen/generators/node.rb +5 -0
  34. data/lib/nextgen/generators/open_browser_on_start.rb +12 -0
  35. data/lib/nextgen/generators/overcommit.rb +1 -0
  36. data/lib/nextgen/generators/pgcli_rails.rb +1 -0
  37. data/lib/nextgen/generators/rack_canonical_host.rb +8 -0
  38. data/lib/nextgen/generators/rack_mini_profiler.rb +2 -0
  39. data/lib/nextgen/generators/rspec_rails.rb +19 -0
  40. data/lib/nextgen/generators/rspec_system_testing.rb +5 -0
  41. data/lib/nextgen/generators/rubocop.rb +32 -0
  42. data/lib/nextgen/generators/shoulda.rb +6 -0
  43. data/lib/nextgen/generators/sidekiq.rb +30 -0
  44. data/lib/nextgen/generators/stylelint.rb +24 -0
  45. data/lib/nextgen/generators/thor.rb +2 -0
  46. data/lib/nextgen/generators/tomo.rb +10 -0
  47. data/lib/nextgen/generators/vite.rb +103 -0
  48. data/lib/nextgen/generators.rb +87 -0
  49. data/lib/nextgen/rails.rb +39 -0
  50. data/lib/nextgen/rails_options.rb +191 -0
  51. data/lib/nextgen/thor_extensions.rb +48 -0
  52. data/lib/nextgen/tidy_gemfile.rb +71 -0
  53. data/lib/nextgen/version.rb +3 -0
  54. data/lib/nextgen.rb +17 -0
  55. data/template/.editorconfig +14 -0
  56. data/template/.env.sample +2 -0
  57. data/template/.erb-lint.yml.tt +25 -0
  58. data/template/.eslintrc.cjs +26 -0
  59. data/template/.github/workflows/ci.yml.tt +142 -0
  60. data/template/.overcommit.yml.tt +86 -0
  61. data/template/.prettierrc.cjs +6 -0
  62. data/template/.rubocop.yml.tt +269 -0
  63. data/template/.stylelintrc.cjs +52 -0
  64. data/template/DEPLOYMENT.md +10 -0
  65. data/template/Procfile.tt +4 -0
  66. data/template/README.md.tt +52 -0
  67. data/template/Thorfile +7 -0
  68. data/template/app/controllers/concerns/basic_auth.rb +20 -0
  69. data/template/app/controllers/home_controller.rb +4 -0
  70. data/template/app/frontend/controllers/index.js +5 -0
  71. data/template/app/frontend/images/example.svg +3 -0
  72. data/template/app/frontend/stylesheets/base.css +8 -0
  73. data/template/app/frontend/stylesheets/index.css +3 -0
  74. data/template/app/frontend/stylesheets/reset.css +36 -0
  75. data/template/app/helpers/inline_svg_helper.rb +9 -0
  76. data/template/app/views/home/index.html.erb.tt +5 -0
  77. data/template/bin/setup +107 -0
  78. data/template/config/initializers/generators.rb +5 -0
  79. data/template/config/initializers/rack_mini_profiler.rb +4 -0
  80. data/template/config/initializers/sidekiq.rb +32 -0
  81. data/template/config/sidekiq.yml +5 -0
  82. data/template/lib/puma/plugin/open.rb +14 -0
  83. data/template/lib/tasks/auto_annotate_models.rake +52 -0
  84. data/template/lib/tasks/erblint.rake +11 -0
  85. data/template/lib/tasks/eslint.rake +11 -0
  86. data/template/lib/tasks/rubocop.rake +4 -0
  87. data/template/lib/tasks/stylelint.rake +11 -0
  88. data/template/lib/templates/rspec/system/system_spec.rb +10 -0
  89. data/template/lib/vite_inline_svg_file_loader.rb +25 -0
  90. data/template/package.json +6 -0
  91. data/template/postcss.config.js +3 -0
  92. data/template/spec/support/factory_bot.rb +3 -0
  93. data/template/spec/support/mailer.rb +5 -0
  94. data/template/spec/support/shoulda.rb +6 -0
  95. data/template/spec/support/system.rb +17 -0
  96. data/template/test/application_system_test_case.rb +18 -0
  97. data/template/test/helpers/inline_svg_helper_test.rb +23 -0
  98. data/template/test/support/factory_bot.rb +3 -0
  99. data/template/test/support/mailer.rb +3 -0
  100. data/template/test/support/shoulda.rb +6 -0
  101. data/template/test/support/vite.rb +5 -0
  102. metadata +220 -0
@@ -0,0 +1,312 @@
1
+ require "active_support/core_ext/string/inflections"
2
+ require "fileutils"
3
+ require "forwardable"
4
+ require "shellwords"
5
+ require "open3"
6
+ require "tmpdir"
7
+ require "tty-prompt"
8
+ require "nextgen/ext/prompt/list"
9
+ require "nextgen/ext/prompt/multilist"
10
+
11
+ module Nextgen
12
+ class Commands::Create # rubocop:disable Metrics/ClassLength
13
+ extend Forwardable
14
+
15
+ def self.run(app_path, options)
16
+ new(app_path, options).run
17
+ end
18
+
19
+ def initialize(app_path, _options)
20
+ @app_path = File.expand_path(app_path)
21
+ @app_name = File.basename(@app_path).gsub(/\W/, "_").squeeze("_").camelize
22
+ @rails_opts = RailsOptions.new
23
+ end
24
+
25
+ def run # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
26
+ say <<~BANNER
27
+ Welcome to nextgen, the interactive Rails app generator!
28
+
29
+ You are about to create a Rails app named "#{app_name}" in the following directory:
30
+
31
+ #{app_path}
32
+
33
+ You'll be asked ~10 questions about database, test framework, and other options.
34
+ The standard Rails "omakase" experience will be selected by default.
35
+
36
+ BANNER
37
+
38
+ continue_if "Ready to start?"
39
+
40
+ ask_rails_version
41
+ ask_database
42
+ ask_full_stack_or_api
43
+ ask_frontend_management unless rails_opts.api?
44
+ ask_css unless rails_opts.api? || rails_opts.skip_asset_pipeline?
45
+ ask_javascript unless rails_opts.api? || rails_opts.skip_asset_pipeline?
46
+ ask_rails_frameworks
47
+ ask_test_framework
48
+ ask_system_testing if rails_opts.frontend? && rails_opts.test_framework?
49
+ ask_optional_enhancements
50
+
51
+ say <<~SUMMARY
52
+
53
+ OK! Your Rails app is ready to be created.
54
+ The following options will be passed to `rails new`:
55
+
56
+ #{rails_new_args.join("\n ")}
57
+
58
+ The following nextgen enhancements will also be applied in individual git commits via `rails app:template`:
59
+
60
+ #{selected_generators.join(", ").scan(/\S.{0,75}(?:,|$)/).join("\n ")}
61
+
62
+ SUMMARY
63
+
64
+ if node?
65
+ say <<~NODE
66
+ Based on the options you selected, your app will require Node and Yarn. For reference, you are using these versions:
67
+
68
+ Node: #{capture_version("node")}
69
+ Yarn: #{capture_version("yarn")}
70
+
71
+ NODE
72
+ end
73
+
74
+ continue_if "Continue?"
75
+
76
+ create_initial_commit_message
77
+ copy_package_json if node?
78
+ Nextgen::Rails.run "new", *rails_new_args
79
+ Dir.chdir(app_path) do
80
+ Nextgen::Rails.run "app:template", "LOCATION=#{write_generators_script}"
81
+ end
82
+
83
+ say <<~DONE.gsub(/^/, " ")
84
+
85
+
86
+ #{set_color("Done!", :green)}
87
+
88
+ A Rails #{rails_version} app was generated in #{set_color(app_path, :cyan)}.
89
+ Run #{set_color("bin/setup", :yellow)} in that directory to get started.
90
+
91
+
92
+ DONE
93
+ end
94
+
95
+ private
96
+
97
+ attr_accessor :app_path, :app_name, :rails_opts, :generators
98
+
99
+ def_delegators :shell, :say, :set_color
100
+
101
+ def continue_if(question)
102
+ if prompt.yes?(question)
103
+ say
104
+ else
105
+ say "Canceled", :red
106
+ exit
107
+ end
108
+ end
109
+
110
+ def copy_package_json
111
+ FileUtils.mkdir_p(app_path)
112
+ FileUtils.cp(
113
+ Nextgen.template_path.join("package.json"),
114
+ File.join(app_path, "package.json")
115
+ )
116
+ end
117
+
118
+ def rails_version
119
+ rails_opts.edge? ? "edge (#{Rails.edge_branch} branch)" : Rails.version
120
+ end
121
+
122
+ def ask_rails_version
123
+ selected = prompt.select(
124
+ "What version of Rails will you use?",
125
+ Rails.version => :current,
126
+ "edge (#{Rails.edge_branch} branch)" => :edge
127
+ )
128
+ rails_opts.edge! if selected == :edge
129
+ end
130
+
131
+ def ask_database
132
+ common_databases = {
133
+ "SQLite3 (default)" => "sqlite3",
134
+ "PostgreSQL (recommended)" => "postgresql",
135
+ "MySQL" => "mysql"
136
+ }
137
+ all_databases = common_databases.merge(
138
+ %w[MySQL Oracle SQLServer JDBCMySQL JDBCSQLite3 JDBCPostgreSQL JDBC].to_h do |name|
139
+ [name, name.downcase]
140
+ end,
141
+ "None (disable Active Record)" => nil
142
+ )
143
+ rails_opts.database =
144
+ prompt.select("Which database?", common_databases.merge("More options..." => false)) ||
145
+ prompt.select("Which database?", all_databases)
146
+ end
147
+
148
+ def ask_full_stack_or_api
149
+ api = prompt.select(
150
+ "What style of Rails app do you need?",
151
+ "Standard, full-stack Rails (default)" => false,
152
+ "API only" => true
153
+ )
154
+ rails_opts.api! if api
155
+ end
156
+
157
+ def ask_frontend_management
158
+ frontend = prompt.select(
159
+ "How will you manage frontend assets?",
160
+ "Sprockets (default)" => "sprockets",
161
+ "Propshaft" => "propshaft",
162
+ "Vite" => :vite
163
+ )
164
+ if frontend == :vite
165
+ rails_opts.asset_pipeline = nil
166
+ rails_opts.javascript = "vite"
167
+ else
168
+ rails_opts.asset_pipeline = frontend
169
+ end
170
+ end
171
+
172
+ def ask_css
173
+ rails_opts.css = prompt.select(
174
+ "Which CSS framework will you use with the asset pipeline?",
175
+ "None (default)" => nil,
176
+ "Bootstrap" => "bootstrap",
177
+ "Bulma" => "bulma",
178
+ "PostCSS" => "postcss",
179
+ "Sass" => "sass",
180
+ "Tailwind" => "tailwind"
181
+ )
182
+ end
183
+
184
+ def ask_javascript
185
+ rails_opts.javascript = prompt.select(
186
+ "Which JavaScript bundler will you use with the asset pipeline?",
187
+ "Importmap (default)" => "importmap",
188
+ "ESBuild" => "esbuild",
189
+ "Rollup" => "rollup",
190
+ "Webpack" => "webpack",
191
+ "None" => nil
192
+ )
193
+ end
194
+
195
+ def ask_rails_frameworks
196
+ frameworks = {
197
+ "JBuilder" => "jbuilder",
198
+ "Action Mailer" => "action_mailer",
199
+ "Active Job" => "active_job",
200
+ "Action Cable" => "action_cable"
201
+ }
202
+
203
+ unless rails_opts.api? || rails_opts.skip_javascript?
204
+ frameworks = {"Hotwire" => "hotwire"}.merge(frameworks)
205
+ end
206
+
207
+ unless rails_opts.skip_active_record?
208
+ frameworks.merge!(
209
+ "Active Storage" => "active_storage",
210
+ "Action Text" => "action_text",
211
+ "Action Mailbox" => "action_mailbox"
212
+ )
213
+ end
214
+
215
+ answers = prompt.multi_select(
216
+ "Which optional Rails frameworks do you need?",
217
+ frameworks,
218
+ default: frameworks.keys.reverse
219
+ )
220
+
221
+ (frameworks.values - answers).each { rails_opts.skip_optional_framework!(_1) }
222
+ end
223
+
224
+ def ask_test_framework
225
+ rails_opts.test_framework = prompt.select(
226
+ "Which test framework will you use?",
227
+ "Minitest (default)" => "minitest",
228
+ "RSpec" => "rspec",
229
+ "None" => nil
230
+ )
231
+ end
232
+
233
+ def ask_system_testing
234
+ system_testing = prompt.select(
235
+ "Include system testing (capybara)?",
236
+ "Yes (default)" => true,
237
+ "No" => false
238
+ )
239
+ rails_opts.skip_system_test! unless system_testing
240
+ end
241
+
242
+ def ask_optional_enhancements
243
+ @generators = Generators.compatible_with(rails_opts: rails_opts)
244
+
245
+ answers = prompt.multi_select(
246
+ "Which optional enhancements would you like to add?",
247
+ generators.optional.sort_by { |label, _| label.downcase }.to_h
248
+ )
249
+ generators.activate(*answers)
250
+ end
251
+
252
+ def create_initial_commit_message
253
+ path = File.join(app_path, "tmp", "initial_nextgen_commit")
254
+ FileUtils.mkdir_p(File.dirname(path))
255
+ File.write(path, <<~COMMIT)
256
+ Init project with `rails new` (#{Nextgen::Rails.version})
257
+
258
+ Nextgen generated this project with the following `rails new` options:
259
+
260
+ ```
261
+ #{rails_opts.to_args.join("\n")}
262
+ ```
263
+ COMMIT
264
+ end
265
+
266
+ def rails_new_args
267
+ [app_path, "--no-rc", *rails_opts.to_args].tap do |args|
268
+ # Work around a Rails bug where --edge causes --no-rc to get ignored.
269
+ # Specifying --rc= with a non-existent file has the same effect as --no-rc.
270
+ @rc_token ||= SecureRandom.hex(8)
271
+ args << "--rc=#{@rc_token}" if rails_opts.edge?
272
+ end
273
+ end
274
+
275
+ def node?
276
+ generators.node_active?
277
+ end
278
+
279
+ def capture_version(command)
280
+ out, _err, status = Open3.capture3(command, "--version")
281
+ version = status.success? && out[/\d[.\d]+\d/]
282
+
283
+ version || "<unknown>"
284
+ end
285
+
286
+ def selected_generators
287
+ optional = generators.optional.invert
288
+ selected = generators.all_active.filter_map { |name| optional[name] }
289
+
290
+ selected.any? ? selected.sort_by(&:downcase) : ["<None>"]
291
+ end
292
+
293
+ def write_generators_script
294
+ new_tempfile_path.tap do |location|
295
+ File.write(location, generators.to_ruby_script)
296
+ end
297
+ end
298
+
299
+ def new_tempfile_path
300
+ token = SecureRandom.hex(8)
301
+ File.join(Dir.tmpdir, "nextgen_create_#{token}.rb")
302
+ end
303
+
304
+ def prompt
305
+ @prompt ||= TTY::Prompt.new
306
+ end
307
+
308
+ def shell
309
+ @shell ||= Thor::Base.shell.new
310
+ end
311
+ end
312
+ end
@@ -0,0 +1,13 @@
1
+ require "tty-prompt"
2
+ require "tty-screen"
3
+
4
+ module Nextgen::Ext
5
+ module Prompt::List
6
+ def initialize(...)
7
+ super
8
+ @per_page ||= [TTY::Prompt::Paginator::DEFAULT_PAGE_SIZE, TTY::Screen.height.to_i - 3].max
9
+ end
10
+ end
11
+ end
12
+
13
+ TTY::Prompt::List.prepend(Nextgen::Ext::Prompt::List)
@@ -0,0 +1,16 @@
1
+ require "tty-prompt"
2
+
3
+ module Nextgen::Ext
4
+ module Prompt::Multilist
5
+ private
6
+
7
+ def selected_names
8
+ return "" unless @done
9
+ return super if @selected.size < 3
10
+
11
+ @selected.first.name + " + #{@selected.size - 1} more"
12
+ end
13
+ end
14
+ end
15
+
16
+ TTY::Prompt::MultiList.prepend(Nextgen::Ext::Prompt::Multilist)
@@ -0,0 +1,26 @@
1
+ say_git "Configure Action Mailer for testing"
2
+ copy_test_support_file "mailer.rb"
3
+ if minitest?
4
+ empty_directory_with_keep_file "test/mailers"
5
+ elsif rspec?
6
+ empty_directory_with_keep_file "spec/mailers"
7
+ end
8
+
9
+ say_git "Ensure absolute URLs can be used in all environments"
10
+ insert_into_file "config/environments/development.rb", <<-RUBY, after: "raise_delivery_errors = false\n"
11
+ config.action_mailer.default_url_options = {host: "localhost:3000"}
12
+ config.action_mailer.asset_host = "http://localhost:3000"
13
+ RUBY
14
+
15
+ insert_into_file "config/environments/test.rb", <<-RUBY, after: "config.action_mailer.delivery_method = :test\n"
16
+ config.action_mailer.default_url_options = {host: "localhost:3000"}
17
+ config.action_mailer.asset_host = "http://localhost:3000"
18
+ RUBY
19
+
20
+ insert_into_file "config/environments/production.rb", <<-RUBY, after: /config\.action_mailer\.raise_deliv.*\n/
21
+ config.action_mailer.default_url_options = {
22
+ host: ENV.fetch("RAILS_HOSTNAME", "app.example.com"),
23
+ protocol: "https"
24
+ }
25
+ config.action_mailer.asset_host = "https://\#{ENV.fetch("RAILS_HOSTNAME", "app.example.com")}"
26
+ RUBY
@@ -0,0 +1,2 @@
1
+ install_gem "annotate", group: :development
2
+ copy_file "lib/tasks/auto_annotate_models.rake"
@@ -0,0 +1,60 @@
1
+ say_git "Add improved bin/setup script"
2
+ copy_file "bin/setup", mode: :preserve, force: true
3
+
4
+ say_git "Specify 2-space indent and other general editor settings"
5
+ copy_file ".editorconfig"
6
+ copy_file ".prettierrc.cjs"
7
+
8
+ say_git "Generate documentation"
9
+ postgres = File.read("Gemfile").match?(/^\s*gem ['"]pg['"]/)
10
+ template "README.md.tt", force: true, context: binding
11
+ copy_file "DEPLOYMENT.md"
12
+
13
+ say_git "Create a Procfile"
14
+ template "Procfile.tt"
15
+
16
+ say_git "Set up default rake task"
17
+ test_task = "test:all" if minitest?
18
+ append_to_file "Rakefile", <<~RUBY
19
+
20
+ Rake::Task[:default].prerequisites.clear if Rake::Task.task_defined?(:default)
21
+
22
+ desc "Run all checks"
23
+ task default: %w[#{test_task}] do
24
+ Thor::Base.shell.new.say_status :OK, "All checks passed!"
25
+ end
26
+ RUBY
27
+
28
+ if File.exist?("test/application_system_test_case.rb")
29
+ say_git "Configure system tests"
30
+ copy_file "test/application_system_test_case.rb", force: true
31
+ end
32
+
33
+ if File.exist?(".ruby-version") && File.read(".ruby-version").match?(/\A\d+\.\d+.\d+.\s*\z/m)
34
+ say_git "DRY up Gemfile and .ruby-version file"
35
+ replacement = 'ruby Pathname.new(__dir__).join(".ruby-version").read.strip'
36
+ gsub_file "Gemfile", /^ruby "\d.*"$/, replacement
37
+ end
38
+
39
+ if File.exist?("app/views/layouts/application.html.erb")
40
+ say_git "Improve title and meta information for HTML layout"
41
+ gsub_file "app/views/layouts/application.html.erb", "<html>", '<html lang="en">'
42
+ gsub_file "app/views/layouts/application.html.erb", %r{^\s*<title>.*</title>}, <<~ERB.gsub(/^/, " ").rstrip
43
+ <title><%= content_for?(:title) ? strip_tags(yield(:title)) : #{app_const_base.titleize.inspect} %></title>
44
+ <meta name="apple-mobile-web-app-title" content="#{app_const_base.titleize}">
45
+ ERB
46
+ end
47
+
48
+ say_git "Disable legacy javascript and stylesheet generators"
49
+ copy_file "config/initializers/generators.rb"
50
+
51
+ say_git "Allow force_ssl to be controlled via env var"
52
+ uncomment_lines "config/environments/production.rb", "config.force_ssl = true"
53
+ gsub_file "config/environments/production.rb",
54
+ "config.force_ssl = true",
55
+ 'config.force_ssl = ENV["RAILS_DISABLE_SSL"].blank?'
56
+
57
+ if File.exist?("config/database.yml")
58
+ say_git "Create initial schema.rb"
59
+ rails_command "db:prepare"
60
+ end
@@ -0,0 +1,5 @@
1
+ document_deploy_var "BASIC_AUTH_PASSWORD"
2
+ document_deploy_var "BASIC_AUTH_USERNAME",
3
+ "If this and `BASIC_AUTH_PASSWORD` are present, visitors must use these credentials to access the app"
4
+ copy_file "app/controllers/concerns/basic_auth.rb"
5
+ inject_into_class "app/controllers/application_controller.rb", "ApplicationController", " include BasicAuth\n"
@@ -0,0 +1,2 @@
1
+ install_gem "brakeman", group: :development, require: false
2
+ binstub "brakeman"
@@ -0,0 +1,2 @@
1
+ install_gem "bundler-audit", group: :development, require: false
2
+ binstub "bundler-audit"
@@ -0,0 +1,4 @@
1
+ install_gem "capybara-lockstep", group: :test
2
+ inject_into_file "app/views/layouts/application.html.erb",
3
+ "\n <%= capybara_lockstep if defined?(Capybara::Lockstep) %>",
4
+ after: /<head\b.*$/
@@ -0,0 +1 @@
1
+ Nextgen::TidyGemfile.clean!
@@ -0,0 +1,3 @@
1
+ install_gem "dotenv-rails", group: %i[development test]
2
+ copy_file ".env.sample"
3
+ gitignore "/.env*", "!/.env.sample"
@@ -0,0 +1,11 @@
1
+ say_git "Install erb_lint"
2
+ install_gem "erb_lint", group: :development, require: false
3
+ binstub "erb_lint"
4
+ template ".erb-lint.yml.tt"
5
+
6
+ say_git "Add erblint to default rake task"
7
+ copy_file "lib/tasks/erblint.rake"
8
+ add_lint_task "erblint"
9
+
10
+ say_git "Auto-correct any existing issues"
11
+ run "bin/erblint --lint-all -a", capture: true
@@ -0,0 +1,24 @@
1
+ say_git "Install eslint"
2
+ add_yarn_packages(
3
+ "eslint",
4
+ "eslint-config-prettier",
5
+ "eslint-plugin-prettier",
6
+ "prettier",
7
+ "npm-run-all",
8
+ "@types/eslint",
9
+ dev: true
10
+ )
11
+ add_package_json_scripts(
12
+ "lint:js": "eslint 'app/{components,frontend,javascript}/**/*.{js,jsx,ts,tsx}'",
13
+ "fix:js": "npm run -- lint:js --fix",
14
+ lint: "npm-run-all lint:**",
15
+ fix: "npm-run-all fix:**"
16
+ )
17
+ copy_file ".eslintrc.cjs"
18
+
19
+ say_git "Add eslint to default rake task"
20
+ copy_file "lib/tasks/eslint.rake"
21
+ add_lint_task "eslint"
22
+
23
+ say_git "Auto-correct any existing issues"
24
+ run "yarn fix:js", capture: true
@@ -0,0 +1,16 @@
1
+ say_git "Install factory_bot_rails"
2
+ install_gem "factory_bot_rails", group: %i[development test]
3
+
4
+ say_git "Include factory_bot methods in tests"
5
+ copy_test_support_file "factory_bot.rb"
6
+
7
+ say_git "Replace fixtures with factories in generators"
8
+ if minitest?
9
+ inject_into_file "config/initializers/generators.rb", <<~RUBY, after: "Rails.application.config.generators do |g|\n"
10
+ g.test_framework :test_unit, fixture: false, fixture_replacement: :factory_bot
11
+ RUBY
12
+ end
13
+ inject_into_file "config/initializers/generators.rb", <<~RUBY, after: "Rails.application.config.generators do |g|\n"
14
+ # Generate "users_factory.rb" instead of "users.rb"
15
+ g.factory_bot suffix: "factory"
16
+ RUBY
@@ -0,0 +1,4 @@
1
+ if Dir.exist?(".git")
2
+ say_git "Mark project as trusted so bin/ can be added to PATH"
3
+ empty_directory ".git/safe"
4
+ end
@@ -0,0 +1,2 @@
1
+ gems = File.exist?("Gemfile") ? File.read("Gemfile").scan(/^\s*gem ["'](.+?)["']/).flatten : []
2
+ template ".github/workflows/ci.yml.tt", context: binding
@@ -0,0 +1 @@
1
+ install_gem "good_migrations"
@@ -0,0 +1,3 @@
1
+ copy_file "app/controllers/home_controller.rb"
2
+ template "app/views/home/index.html.erb.tt"
3
+ route 'root "home#index"'
@@ -0,0 +1,5 @@
1
+ commit_msg = "tmp/initial_nextgen_commit"
2
+ return unless git_working? && File.exist?(commit_msg)
3
+
4
+ gitignore "node_modules/" if Dir.exist?("node_modules")
5
+ git_commit_all(File.read(commit_msg))
@@ -0,0 +1,11 @@
1
+ migration_tasks = %w[
2
+ action_mailbox:install:migrations
3
+ action_text:install:migrations
4
+ active_storage:install
5
+ ]
6
+ all_tasks = rails_command("-T", capture: true).scan(/^\S+\s+(\S+)/).flatten
7
+ tasks_to_run = migration_tasks & all_tasks
8
+ return unless tasks_to_run.any?
9
+
10
+ tasks_to_run.each { |task| rails_command(task) }
11
+ rails_command "db:migrate"
@@ -0,0 +1,8 @@
1
+ say_git "Install letter_opener"
2
+ install_gem "letter_opener", group: :development
3
+
4
+ say_git "Configure Action Mailer to use letter_opener"
5
+ insert_into_file "config/environments/development.rb", <<-RUBY, after: "raise_delivery_errors = false\n"
6
+
7
+ config.action_mailer.delivery_method = :letter_opener
8
+ RUBY
@@ -0,0 +1,5 @@
1
+ say_git "Add Node and Yarn prerequisites"
2
+ copy_file "package.json" unless File.exist?("package.json")
3
+ inject_into_file "README.md", "\n- Node 18 (LTS) or newer\n- Yarn 1.x (classic)", after: /^- Ruby.*$/
4
+ inject_into_file "README.md", "\nbrew install node\nbrew install yarn", after: /^brew install rbenv.*$/
5
+ gitignore "node_modules/"
@@ -0,0 +1,12 @@
1
+ say_git "Install launchy"
2
+ install_gem "launchy", group: %i[development test]
3
+
4
+ say_git "Configure puma to open browser on startup"
5
+ copy_file "lib/puma/plugin/open.rb"
6
+ append_to_file "config/puma.rb", <<~RUBY
7
+
8
+ # Automatically open the browser when in development
9
+ require_relative "../lib/puma/plugin/open"
10
+ plugin :open
11
+ RUBY
12
+ prevent_autoload_lib "puma/plugin"
@@ -0,0 +1 @@
1
+ template ".overcommit.yml"
@@ -0,0 +1 @@
1
+ install_gem "pgcli-rails", group: :development
@@ -0,0 +1,8 @@
1
+ say_git "Install rack-canonical-host"
2
+ install_gem "rack-canonical-host"
3
+
4
+ say_git "Use RAILS_HOSTNAME env var"
5
+ document_deploy_var "RAILS_HOSTNAME", "Redirect all requests to the specified canonical hostname"
6
+ insert_into_file "config.ru",
7
+ %(use Rack::CanonicalHost, ENV.fetch("RAILS_HOSTNAME", nil) if ENV["RAILS_HOSTNAME"].present?\n),
8
+ before: /^run Rails.application/
@@ -0,0 +1,2 @@
1
+ install_gem "rack-mini-profiler", group: :development
2
+ copy_file "config/initializers/rack_mini_profiler.rb" if File.read("Gemfile").match?(/^\s*gem ['"]turbo-rails['"]/)
@@ -0,0 +1,19 @@
1
+ say_git "Install the rspec-rails gem"
2
+ install_gem "rspec-rails", group: %i[development test]
3
+
4
+ say_git "Run the rspec installer"
5
+ generate "rspec:install"
6
+ gitignore "/spec/examples.txt"
7
+ binstub "rspec-core"
8
+
9
+ say_git "Disable auto-generation of routing and view specs"
10
+ inject_into_file "config/initializers/generators.rb", <<~RUBY, after: "g.stylesheets false\n"
11
+ g.routing_specs false
12
+ g.view_specs false
13
+ RUBY
14
+
15
+ say_git "Add spec to default rake task"
16
+ inject_into_file "Rakefile", "spec", after: /task default: %w\[/
17
+
18
+ say_git "Enable auto-loading of the spec/support directory"
19
+ uncomment_lines("spec/rails_helper.rb", /Dir\[Rails.root.join/)
@@ -0,0 +1,5 @@
1
+ install_gems "capybara", "selenium-webdriver", group: :test
2
+ copy_test_support_file "system.rb"
3
+
4
+ copy_file "lib/templates/rspec/system/system_spec.rb"
5
+ prevent_autoload_lib "templates"
@@ -0,0 +1,32 @@
1
+ say_git "Install rubocop gems"
2
+ gemfile = File.read("Gemfile")
3
+ plugins = []
4
+ plugins << "capybara" if gemfile.match?(/^\s*gem ['"]capybara['"]/)
5
+ plugins << "factory_bot" if gemfile.match?(/^\s*gem ['"]factory_bot/)
6
+ plugins << "minitest" if minitest?
7
+ plugins << "performance"
8
+ plugins << "rails"
9
+ install_gems "rubocop", *plugins.map { "rubocop-#{_1}" }, group: :development, require: false
10
+ binstub "rubocop"
11
+
12
+ say_git "Generate .rubocop.yml"
13
+ template ".rubocop.yml", context: binding
14
+
15
+ if File.exist?(".erb-lint.yml")
16
+ say_git "Regenerate .erb-lint.yml with rubocop support"
17
+ template ".erb-lint.yml", force: true
18
+ end
19
+
20
+ say_git "Add rubocop to default rake task"
21
+ copy_file "lib/tasks/rubocop.rake"
22
+ add_lint_task "rubocop", fix: "rubocop:autocorrect_all"
23
+ inject_into_file "README.md", <<~MARKDOWN, after: /rake fix\n```\n/
24
+
25
+ > [!WARNING]
26
+ > A small number of Rubocop's auto-corrections are considered "unsafe" and may
27
+ > occasionally produce incorrect results. After running `fix`, you should
28
+ > review the changes and make sure the code still works as intended.
29
+ MARKDOWN
30
+
31
+ say_git "Auto-correct any existing issues"
32
+ run "bin/rubocop -A --fail-level F", capture: true