opinionated_rails 0.1.4
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +59 -0
- data/exe/opinionated_rails +7 -0
- data/lib/opinionated_rails/answer_shell.rb +131 -0
- data/lib/opinionated_rails/cli.rb +291 -0
- data/lib/opinionated_rails/context.rb +39 -0
- data/lib/opinionated_rails/error.rb +6 -0
- data/lib/opinionated_rails/generators/file_writer.rb +14 -0
- data/lib/opinionated_rails/generators/rails_new_invoker.rb +26 -0
- data/lib/opinionated_rails/kamal_config.rb +94 -0
- data/lib/opinionated_rails/manifest.rb +42 -0
- data/lib/opinionated_rails/pins.rb +83 -0
- data/lib/opinionated_rails/prerequisites.rb +39 -0
- data/lib/opinionated_rails/rails_version_guard.rb +49 -0
- data/lib/opinionated_rails/step.rb +41 -0
- data/lib/opinionated_rails/step_graph.rb +53 -0
- data/lib/opinionated_rails/steps/addons/auth_step.rb +170 -0
- data/lib/opinionated_rails/steps/addons/ci_step.rb +226 -0
- data/lib/opinionated_rails/steps/addons/docker_kamal_step.rb +133 -0
- data/lib/opinionated_rails/steps/addons/docs_step.rb +35 -0
- data/lib/opinionated_rails/steps/addons/factory_or_fixtures_step.rb +128 -0
- data/lib/opinionated_rails/steps/addons/faker_step.rb +55 -0
- data/lib/opinionated_rails/steps/addons/js_lint_step.rb +132 -0
- data/lib/opinionated_rails/steps/addons/mailer_step.rb +183 -0
- data/lib/opinionated_rails/steps/addons/pagination_step.rb +116 -0
- data/lib/opinionated_rails/steps/addons/pr_issue_templates_step.rb +43 -0
- data/lib/opinionated_rails/steps/addons/rubocop_step.rb +111 -0
- data/lib/opinionated_rails/steps/addons/secrets_handling_step.rb +138 -0
- data/lib/opinionated_rails/steps/addons/services_step.rb +64 -0
- data/lib/opinionated_rails/steps/addons/solid_stack_step.rb +252 -0
- data/lib/opinionated_rails/steps/addons/spa_controller_step.rb +87 -0
- data/lib/opinionated_rails/steps/addons/staging_env_step.rb +108 -0
- data/lib/opinionated_rails/steps/addons/tailwind_step.rb +148 -0
- data/lib/opinionated_rails/steps/addons/vcr_step.rb +170 -0
- data/lib/opinionated_rails/steps/core/app_name_step.rb +85 -0
- data/lib/opinionated_rails/steps/core/app_type_step.rb +51 -0
- data/lib/opinionated_rails/steps/core/database_step.rb +53 -0
- data/lib/opinionated_rails/steps/core/frontend_step.rb +282 -0
- data/lib/opinionated_rails/steps/core/testing_step.rb +82 -0
- data/lib/opinionated_rails/templates/docs/STYLE.md +39 -0
- data/lib/opinionated_rails/templates/github/ISSUE_TEMPLATE/bug_report.md +25 -0
- data/lib/opinionated_rails/templates/github/ISSUE_TEMPLATE/feature_request.md +17 -0
- data/lib/opinionated_rails/templates/github/PULL_REQUEST_TEMPLATE.md +9 -0
- data/lib/opinionated_rails/templates/rubocop/custom_overrides.yml +5 -0
- data/lib/opinionated_rails/templates/services/application_service.rb +50 -0
- data/lib/opinionated_rails/templates/services/result.rb +44 -0
- data/lib/opinionated_rails/version.rb +5 -0
- data/lib/opinionated_rails.rb +41 -0
- metadata +141 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpinionatedRails
|
|
4
|
+
module Steps
|
|
5
|
+
module Addons
|
|
6
|
+
class JsLintStep < Step
|
|
7
|
+
def self.id = :js_lint
|
|
8
|
+
def self.tier = :addon
|
|
9
|
+
def self.needs = [ :frontend ]
|
|
10
|
+
def self.summary = "Configure JavaScript linting"
|
|
11
|
+
def self.description = "Installs ESLint and writes an ESLint 9 flat config (eslint.config.js) for the chosen frontend stack."
|
|
12
|
+
def self.prompts = "Enable JS linting? (default: yes)"
|
|
13
|
+
|
|
14
|
+
def if_condition(context)
|
|
15
|
+
context.output(:frontend, :frontend) != :none
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def prompt(context)
|
|
19
|
+
shell = context.shell
|
|
20
|
+
choice = shell.ask("Enable JS linting? (default: yes):").to_s.strip
|
|
21
|
+
choice = "yes" if choice.empty?
|
|
22
|
+
|
|
23
|
+
loop do
|
|
24
|
+
case choice.downcase
|
|
25
|
+
when "yes", "y"
|
|
26
|
+
context.answer(:js_lint, true)
|
|
27
|
+
break
|
|
28
|
+
when "no", "n"
|
|
29
|
+
context.answer(:js_lint, false)
|
|
30
|
+
break
|
|
31
|
+
else
|
|
32
|
+
shell.say("Invalid. Enter 'yes' or 'no'.", :red)
|
|
33
|
+
choice = shell.ask("Enable JS linting? (default: yes):").to_s.strip
|
|
34
|
+
choice = "yes" if choice.empty?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def generate(context)
|
|
40
|
+
enabled = if if_condition(context)
|
|
41
|
+
context.get(:js_lint) != false
|
|
42
|
+
else
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
data = { js_lint: enabled }
|
|
47
|
+
data[:js_lint_tool] = :eslint if enabled
|
|
48
|
+
|
|
49
|
+
context.set_output(:js_lint, data)
|
|
50
|
+
|
|
51
|
+
return unless enabled
|
|
52
|
+
|
|
53
|
+
write_eslint_config(context)
|
|
54
|
+
update_package_json(context)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def outputs(context)
|
|
58
|
+
context.output(:js_lint)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# ESLint 9 flat config. `files` names .jsx explicitly: ESLint lints only .js unless told otherwise,
|
|
64
|
+
# which previously left every React component unlinted.
|
|
65
|
+
PLAIN_ESLINT_CONFIG = <<~JS
|
|
66
|
+
const js = require("@eslint/js")
|
|
67
|
+
const globals = require("globals")
|
|
68
|
+
|
|
69
|
+
module.exports = [
|
|
70
|
+
{ ignores: ["app/assets/builds/**", "node_modules/**"] },
|
|
71
|
+
js.configs.recommended,
|
|
72
|
+
{
|
|
73
|
+
files: ["**/*.js"],
|
|
74
|
+
languageOptions: {
|
|
75
|
+
ecmaVersion: "latest",
|
|
76
|
+
sourceType: "module",
|
|
77
|
+
globals: { ...globals.browser }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
JS
|
|
82
|
+
|
|
83
|
+
REACT_ESLINT_CONFIG = <<~JS
|
|
84
|
+
const js = require("@eslint/js")
|
|
85
|
+
const globals = require("globals")
|
|
86
|
+
const react = require("eslint-plugin-react")
|
|
87
|
+
const reactHooks = require("eslint-plugin-react-hooks")
|
|
88
|
+
|
|
89
|
+
module.exports = [
|
|
90
|
+
{ ignores: ["app/assets/builds/**", "node_modules/**"] },
|
|
91
|
+
js.configs.recommended,
|
|
92
|
+
{ ...react.configs.flat.recommended, files: ["**/*.{js,jsx}"] },
|
|
93
|
+
reactHooks.configs.flat.recommended,
|
|
94
|
+
{
|
|
95
|
+
files: ["**/*.{js,jsx}"],
|
|
96
|
+
languageOptions: {
|
|
97
|
+
ecmaVersion: "latest",
|
|
98
|
+
sourceType: "module",
|
|
99
|
+
globals: { ...globals.browser }
|
|
100
|
+
},
|
|
101
|
+
settings: { react: { version: "detect" } }
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
JS
|
|
105
|
+
|
|
106
|
+
def write_eslint_config(context)
|
|
107
|
+
react = context.output(:frontend, :frontend) == :react
|
|
108
|
+
File.write(File.join(context.app_path, "eslint.config.js"), react ? REACT_ESLINT_CONFIG : PLAIN_ESLINT_CONFIG)
|
|
109
|
+
FileUtils.rm_f(File.join(context.app_path, ".eslintrc.json")) # ESLint 8 format, ignored by ESLint 9
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def update_package_json(context)
|
|
113
|
+
package_json_path = File.join(context.app_path, "package.json")
|
|
114
|
+
package = File.exist?(package_json_path) ? JSON.parse(File.read(package_json_path)) : {}
|
|
115
|
+
package["scripts"] ||= {}
|
|
116
|
+
package["scripts"]["lint:js"] = "eslint app/javascript"
|
|
117
|
+
package["devDependencies"] ||= {}
|
|
118
|
+
package["devDependencies"]["eslint"] = "9.39.5"
|
|
119
|
+
package["devDependencies"]["@eslint/js"] = "9.39.5"
|
|
120
|
+
package["devDependencies"]["globals"] = "17.13.0"
|
|
121
|
+
|
|
122
|
+
if context.output(:frontend, :frontend) == :react
|
|
123
|
+
package["devDependencies"]["eslint-plugin-react"] = "7.37.5"
|
|
124
|
+
package["devDependencies"]["eslint-plugin-react-hooks"] = "7.1.1"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
File.write(package_json_path, JSON.pretty_generate(package))
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpinionatedRails
|
|
4
|
+
module Steps
|
|
5
|
+
module Addons
|
|
6
|
+
class MailerStep < Step
|
|
7
|
+
def self.id = :mailer
|
|
8
|
+
def self.tier = :addon
|
|
9
|
+
def self.needs = [ :secrets_handling ]
|
|
10
|
+
def self.summary = "Configure mailers"
|
|
11
|
+
def self.description = "Sets up Letter Opener in development and Postmark in production."
|
|
12
|
+
def self.prompts = "Enable mailer setup (Letter Opener dev / Postmark production)? (default: yes)"
|
|
13
|
+
|
|
14
|
+
LETTER_OPENER_VERSION = "1.10.0".freeze
|
|
15
|
+
POSTMARK_RAILS_VERSION = "0.22.1".freeze
|
|
16
|
+
|
|
17
|
+
DEVELOPMENT_ANCHOR = 'config.action_mailer.default_url_options = { host: "localhost", port: 3000 }'
|
|
18
|
+
DEVELOPMENT_LINE = "config.action_mailer.delivery_method = :letter_opener"
|
|
19
|
+
|
|
20
|
+
PRODUCTION_ANCHOR = 'config.action_mailer.default_url_options = { host: "example.com" }'
|
|
21
|
+
|
|
22
|
+
POSTMARK_ENV_VAR_LINE = "POSTMARK_API_TOKEN="
|
|
23
|
+
|
|
24
|
+
def if_condition(_context) = true
|
|
25
|
+
|
|
26
|
+
def prompt(context)
|
|
27
|
+
shell = context.shell
|
|
28
|
+
choice = shell.ask("Enable mailer setup (Letter Opener dev / Postmark production)? (default: yes):").to_s.strip
|
|
29
|
+
choice = "yes" if choice.empty?
|
|
30
|
+
|
|
31
|
+
loop do
|
|
32
|
+
case choice.downcase
|
|
33
|
+
when "yes", "y"
|
|
34
|
+
context.answer(:mailer, true)
|
|
35
|
+
break
|
|
36
|
+
when "no", "n"
|
|
37
|
+
context.answer(:mailer, false)
|
|
38
|
+
break
|
|
39
|
+
else
|
|
40
|
+
shell.say("Invalid. Enter 'yes' or 'no'.", :red)
|
|
41
|
+
choice = shell.ask("Enable mailer setup (Letter Opener dev / Postmark production)? (default: yes):").to_s.strip
|
|
42
|
+
choice = "yes" if choice.empty?
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def generate(context)
|
|
48
|
+
enabled = context.get(:mailer) != false
|
|
49
|
+
|
|
50
|
+
if enabled
|
|
51
|
+
add_development_group_gem(context, "gem \"letter_opener\", \"#{LETTER_OPENER_VERSION}\"")
|
|
52
|
+
add_top_level_gem(context, "gem \"postmark-rails\", \"#{POSTMARK_RAILS_VERSION}\"")
|
|
53
|
+
configure_development(context)
|
|
54
|
+
configure_production(context)
|
|
55
|
+
ensure_application_mailer(context)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
context.set_output(:mailer, { mailer: enabled })
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def outputs(context)
|
|
62
|
+
context.output(:mailer)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def configure_development(context)
|
|
68
|
+
insert_after_anchor(
|
|
69
|
+
context,
|
|
70
|
+
File.join(context.app_path, "config/environments/development.rb"),
|
|
71
|
+
DEVELOPMENT_ANCHOR,
|
|
72
|
+
[ DEVELOPMENT_LINE ]
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def configure_production(context)
|
|
77
|
+
insert_after_anchor(
|
|
78
|
+
context,
|
|
79
|
+
File.join(context.app_path, "config/environments/production.rb"),
|
|
80
|
+
PRODUCTION_ANCHOR,
|
|
81
|
+
production_lines(context)
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def production_lines(context)
|
|
86
|
+
secrets_handling = context.output(:secrets_handling, :secrets_handling) || :rails_credentials
|
|
87
|
+
|
|
88
|
+
api_token_line = case secrets_handling
|
|
89
|
+
when :dotenv
|
|
90
|
+
append_env_placeholder(context)
|
|
91
|
+
'config.action_mailer.postmark_settings = { api_token: ENV["POSTMARK_API_TOKEN"] }'
|
|
92
|
+
else
|
|
93
|
+
"config.action_mailer.postmark_settings = { api_token: Rails.application.credentials.dig(:postmark, :api_token) }"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
[ "config.action_mailer.delivery_method = :postmark", api_token_line ]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def append_env_placeholder(context)
|
|
100
|
+
path = File.join(context.app_path, ".env.example")
|
|
101
|
+
return unless File.exist?(path)
|
|
102
|
+
|
|
103
|
+
content = File.read(path)
|
|
104
|
+
return if content.include?(POSTMARK_ENV_VAR_LINE)
|
|
105
|
+
|
|
106
|
+
File.open(path, "a") { |f| f.puts(POSTMARK_ENV_VAR_LINE) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# A Rails release that rewords the anchor must not silently leave mail unconfigured.
|
|
110
|
+
def insert_after_anchor(context, path, anchor, new_lines)
|
|
111
|
+
relative = path.delete_prefix("#{context.app_path}/")
|
|
112
|
+
unless File.exist?(path)
|
|
113
|
+
context.shell.say("Warning: #{relative} not found; mailer settings were not added.", :yellow)
|
|
114
|
+
return
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
content = File.read(path)
|
|
118
|
+
return if new_lines.all? { |line| content.include?(line) }
|
|
119
|
+
|
|
120
|
+
lines = content.lines
|
|
121
|
+
anchor_index = lines.index { |line| line.include?(anchor) }
|
|
122
|
+
unless anchor_index
|
|
123
|
+
context.shell.say(
|
|
124
|
+
"Warning: could not find `#{anchor}` in #{relative}; add these lines yourself:\n #{new_lines.join("\n ")}",
|
|
125
|
+
:yellow
|
|
126
|
+
)
|
|
127
|
+
return
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
indent = lines[anchor_index][/\A\s*/]
|
|
131
|
+
insertion = new_lines.map { |line| "#{indent}#{line}\n" }
|
|
132
|
+
lines.insert(anchor_index + 1, *insertion)
|
|
133
|
+
|
|
134
|
+
File.write(path, lines.join)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def ensure_application_mailer(context)
|
|
138
|
+
path = File.join(context.app_path, "app/mailers/application_mailer.rb")
|
|
139
|
+
return if File.exist?(path)
|
|
140
|
+
|
|
141
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
142
|
+
File.write(path, <<~RUBY)
|
|
143
|
+
class ApplicationMailer < ActionMailer::Base
|
|
144
|
+
default from: "from@example.com"
|
|
145
|
+
layout "mailer"
|
|
146
|
+
end
|
|
147
|
+
RUBY
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def add_top_level_gem(context, gem_line)
|
|
151
|
+
gemfile_path = File.join(context.app_path, "Gemfile")
|
|
152
|
+
return unless File.exist?(gemfile_path)
|
|
153
|
+
|
|
154
|
+
content = File.read(gemfile_path)
|
|
155
|
+
return if content.include?(gem_line)
|
|
156
|
+
|
|
157
|
+
File.open(gemfile_path, "a") { |f| f.puts(gem_line) }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def add_development_group_gem(context, gem_line)
|
|
161
|
+
gemfile_path = File.join(context.app_path, "Gemfile")
|
|
162
|
+
return unless File.exist?(gemfile_path)
|
|
163
|
+
|
|
164
|
+
content = File.read(gemfile_path)
|
|
165
|
+
return if content.include?(gem_line)
|
|
166
|
+
|
|
167
|
+
lines = content.lines
|
|
168
|
+
group_start = lines.index { |line| line.strip == "group :development do" }
|
|
169
|
+
|
|
170
|
+
if group_start
|
|
171
|
+
end_index = (group_start + 1...lines.size).find { |i| lines[i].strip == "end" }
|
|
172
|
+
lines.insert(end_index, " #{gem_line}\n")
|
|
173
|
+
else
|
|
174
|
+
lines << "\n" unless lines.last.nil? || lines.last.end_with?("\n")
|
|
175
|
+
lines << "\ngroup :development do\n #{gem_line}\nend\n"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
File.write(gemfile_path, lines.join)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpinionatedRails
|
|
4
|
+
module Steps
|
|
5
|
+
module Addons
|
|
6
|
+
class PaginationStep < Step
|
|
7
|
+
def self.id = :pagination
|
|
8
|
+
def self.tier = :addon
|
|
9
|
+
def self.needs = []
|
|
10
|
+
def self.summary = "Configure pagination"
|
|
11
|
+
def self.description = "Installs geared_pagination or pagy for paginated collections."
|
|
12
|
+
def self.prompts = "Pagination: geared_pagination or pagy (default: pagy)"
|
|
13
|
+
|
|
14
|
+
GEARED_PAGINATION_VERSION = "1.2.0".freeze
|
|
15
|
+
PAGY_VERSION = "43.6.1".freeze
|
|
16
|
+
|
|
17
|
+
PAGY_INITIALIZER = <<~RUBY
|
|
18
|
+
# frozen_string_literal: true
|
|
19
|
+
|
|
20
|
+
# Pagy initializer — see https://ddnexus.github.io/pagy/toolbox/configuration/initializer/
|
|
21
|
+
# for the full list of available options.
|
|
22
|
+
|
|
23
|
+
Pagy::OPTIONS.freeze
|
|
24
|
+
RUBY
|
|
25
|
+
|
|
26
|
+
def if_condition(_context) = true
|
|
27
|
+
|
|
28
|
+
def prompt(context)
|
|
29
|
+
shell = context.shell
|
|
30
|
+
shell.say("Choose a pagination library:")
|
|
31
|
+
shell.say(" 1. geared_pagination")
|
|
32
|
+
shell.say(" 2. pagy (default)")
|
|
33
|
+
|
|
34
|
+
choice = shell.ask("Pagination (default: pagy):").to_s.strip
|
|
35
|
+
choice = "2" if choice.empty?
|
|
36
|
+
|
|
37
|
+
loop do
|
|
38
|
+
case choice.downcase
|
|
39
|
+
when "geared_pagination", "1"
|
|
40
|
+
context.answer(:pagination, :geared_pagination)
|
|
41
|
+
break
|
|
42
|
+
when "pagy", "2"
|
|
43
|
+
context.answer(:pagination, :pagy)
|
|
44
|
+
break
|
|
45
|
+
else
|
|
46
|
+
shell.say("Invalid. Enter 'geared_pagination', 'pagy', 1, or 2.", :red)
|
|
47
|
+
choice = shell.ask("Pagination (default: pagy):").to_s.strip
|
|
48
|
+
choice = "2" if choice.empty?
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def generate(context)
|
|
54
|
+
strategy = context.get(:pagination) || :pagy
|
|
55
|
+
|
|
56
|
+
if strategy == :pagy
|
|
57
|
+
install_pagy(context)
|
|
58
|
+
else
|
|
59
|
+
install_geared_pagination(context)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
context.set_output(:pagination, { pagination: strategy })
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def outputs(context)
|
|
66
|
+
context.output(:pagination)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def install_geared_pagination(context)
|
|
72
|
+
add_top_level_gem(context, "gem \"geared_pagination\", \"#{GEARED_PAGINATION_VERSION}\"")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def install_pagy(context)
|
|
76
|
+
add_top_level_gem(context, "gem \"pagy\", \"#{PAGY_VERSION}\"")
|
|
77
|
+
write_pagy_initializer(context)
|
|
78
|
+
# Pagy 43 replaced Backend/Frontend with Pagy::Method (controller only); view helpers live on @pagy.
|
|
79
|
+
include_module(context, "app/controllers/application_controller.rb", "ApplicationController", "Pagy::Method")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def write_pagy_initializer(context)
|
|
83
|
+
path = File.join(context.app_path, "config/initializers/pagy.rb")
|
|
84
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
85
|
+
File.write(path, PAGY_INITIALIZER)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def include_module(context, relative_path, class_or_module_name, module_to_include)
|
|
89
|
+
path = File.join(context.app_path, relative_path)
|
|
90
|
+
return unless File.exist?(path)
|
|
91
|
+
|
|
92
|
+
content = File.read(path)
|
|
93
|
+
include_line = "include #{module_to_include}"
|
|
94
|
+
return if content.include?(include_line)
|
|
95
|
+
|
|
96
|
+
lines = content.lines
|
|
97
|
+
open_index = lines.index { |line| line.match?(/\A(class|module)\s+#{Regexp.escape(class_or_module_name)}\b/) }
|
|
98
|
+
return unless open_index
|
|
99
|
+
|
|
100
|
+
lines.insert(open_index + 1, " #{include_line}\n")
|
|
101
|
+
File.write(path, lines.join)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def add_top_level_gem(context, gem_line)
|
|
105
|
+
gemfile_path = File.join(context.app_path, "Gemfile")
|
|
106
|
+
return unless File.exist?(gemfile_path)
|
|
107
|
+
|
|
108
|
+
content = File.read(gemfile_path)
|
|
109
|
+
return if content.include?(gem_line)
|
|
110
|
+
|
|
111
|
+
File.open(gemfile_path, "a") { |f| f.puts(gem_line) }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpinionatedRails
|
|
4
|
+
module Steps
|
|
5
|
+
module Addons
|
|
6
|
+
class PrIssueTemplatesStep < Step
|
|
7
|
+
def self.id = :pr_issue_templates
|
|
8
|
+
def self.tier = :addon
|
|
9
|
+
def self.needs = []
|
|
10
|
+
def self.summary = "Generate GitHub templates"
|
|
11
|
+
def self.description = "Adds default pull request and issue templates."
|
|
12
|
+
def self.prompts = "None"
|
|
13
|
+
|
|
14
|
+
TEMPLATES_DIR = File.expand_path("../../templates/github", __dir__)
|
|
15
|
+
|
|
16
|
+
FILES = {
|
|
17
|
+
"PULL_REQUEST_TEMPLATE.md" => ".github/PULL_REQUEST_TEMPLATE.md",
|
|
18
|
+
"ISSUE_TEMPLATE/bug_report.md" => ".github/ISSUE_TEMPLATE/bug_report.md",
|
|
19
|
+
"ISSUE_TEMPLATE/feature_request.md" => ".github/ISSUE_TEMPLATE/feature_request.md"
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
def if_condition(_context) = true
|
|
23
|
+
|
|
24
|
+
def prompt(_context); end
|
|
25
|
+
|
|
26
|
+
def generate(context)
|
|
27
|
+
writer = OpinionatedRails::Generators::FileWriter.new
|
|
28
|
+
|
|
29
|
+
FILES.each do |source, destination|
|
|
30
|
+
content = File.read(File.join(TEMPLATES_DIR, source))
|
|
31
|
+
writer.write(File.join(context.app_path, destination), content)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
context.set_output(:pr_issue_templates, { pr_issue_templates: true })
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def outputs(context)
|
|
38
|
+
context.output(:pr_issue_templates)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpinionatedRails
|
|
4
|
+
module Steps
|
|
5
|
+
module Addons
|
|
6
|
+
class RubocopStep < Step
|
|
7
|
+
def self.id = :rubocop
|
|
8
|
+
def self.tier = :addon
|
|
9
|
+
def self.needs = []
|
|
10
|
+
def self.summary = "Configure RuboCop"
|
|
11
|
+
def self.description = "Installs and configures RuboCop with Omakase defaults or a custom setup."
|
|
12
|
+
def self.prompts = "RuboCop style: Omakase or Custom (default: Omakase)"
|
|
13
|
+
|
|
14
|
+
RUBOCOP_OMAKASE_VERSION = "1.1.0".freeze
|
|
15
|
+
UNPINNED_GEM_LINE = 'gem "rubocop-rails-omakase", require: false'
|
|
16
|
+
CUSTOM_OVERRIDES_TEMPLATE = File.expand_path("../../templates/rubocop/custom_overrides.yml", __dir__)
|
|
17
|
+
|
|
18
|
+
def if_condition(_context) = true
|
|
19
|
+
|
|
20
|
+
def prompt(context)
|
|
21
|
+
shell = context.shell
|
|
22
|
+
shell.say("Choose a RuboCop style:")
|
|
23
|
+
shell.say(" 1. Omakase (default)")
|
|
24
|
+
shell.say(" 2. Custom")
|
|
25
|
+
|
|
26
|
+
choice = shell.ask("RuboCop style (default: Omakase):").to_s.strip
|
|
27
|
+
choice = "1" if choice.empty?
|
|
28
|
+
|
|
29
|
+
loop do
|
|
30
|
+
case choice.downcase
|
|
31
|
+
when "omakase", "1"
|
|
32
|
+
context.answer(:rubocop, :omakase)
|
|
33
|
+
break
|
|
34
|
+
when "custom", "2"
|
|
35
|
+
context.answer(:rubocop, :custom)
|
|
36
|
+
break
|
|
37
|
+
else
|
|
38
|
+
shell.say("Invalid. Enter 'Omakase', 'Custom', 1, or 2.", :red)
|
|
39
|
+
choice = shell.ask("RuboCop style (default: Omakase):").to_s.strip
|
|
40
|
+
choice = "1" if choice.empty?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def generate(context)
|
|
46
|
+
style = context.get(:rubocop) || :omakase
|
|
47
|
+
|
|
48
|
+
pin_rubocop_gem(context)
|
|
49
|
+
write_custom_overrides(context) if style == :custom
|
|
50
|
+
|
|
51
|
+
context.set_output(:rubocop, { rubocop: style })
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def outputs(context)
|
|
55
|
+
context.output(:rubocop)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def pin_rubocop_gem(context)
|
|
61
|
+
gemfile_path = File.join(context.app_path, "Gemfile")
|
|
62
|
+
return unless File.exist?(gemfile_path)
|
|
63
|
+
|
|
64
|
+
pinned_line = "gem \"rubocop-rails-omakase\", \"#{RUBOCOP_OMAKASE_VERSION}\", require: false"
|
|
65
|
+
content = File.read(gemfile_path)
|
|
66
|
+
return if content.include?(pinned_line)
|
|
67
|
+
|
|
68
|
+
if content.include?(UNPINNED_GEM_LINE)
|
|
69
|
+
File.write(gemfile_path, content.sub(UNPINNED_GEM_LINE, pinned_line))
|
|
70
|
+
else
|
|
71
|
+
add_dev_test_group_gem(context, pinned_line)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def add_dev_test_group_gem(context, gem_line)
|
|
76
|
+
gemfile_path = File.join(context.app_path, "Gemfile")
|
|
77
|
+
content = File.read(gemfile_path)
|
|
78
|
+
return if content.include?(gem_line)
|
|
79
|
+
|
|
80
|
+
lines = content.lines
|
|
81
|
+
group_start = lines.index { |line| line.strip == "group :development, :test do" }
|
|
82
|
+
|
|
83
|
+
if group_start
|
|
84
|
+
end_index = (group_start + 1...lines.size).find { |i| lines[i].strip == "end" }
|
|
85
|
+
lines.insert(end_index, " #{gem_line}\n")
|
|
86
|
+
else
|
|
87
|
+
lines << "\n" unless lines.last.nil? || lines.last.end_with?("\n")
|
|
88
|
+
lines << "\ngroup :development, :test do\n #{gem_line}\nend\n"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
File.write(gemfile_path, lines.join)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def write_custom_overrides(context)
|
|
95
|
+
rubocop_yml_path = File.join(context.app_path, ".rubocop.yml")
|
|
96
|
+
overrides = File.read(CUSTOM_OVERRIDES_TEMPLATE)
|
|
97
|
+
|
|
98
|
+
content = <<~YAML
|
|
99
|
+
# Omakase Ruby styling for Rails
|
|
100
|
+
inherit_gem: { rubocop-rails-omakase: rubocop.yml }
|
|
101
|
+
|
|
102
|
+
# --- user overrides below ---
|
|
103
|
+
#{overrides}
|
|
104
|
+
YAML
|
|
105
|
+
|
|
106
|
+
File.write(rubocop_yml_path, content)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|