Skip to content

Commit a6cb453

Browse files
Merge pull request rails#45520 from jonathanhefner/app_generator-implied-options
Improve generator implied option handling
2 parents 0e77c83 + 4c67975 commit a6cb453

File tree

6 files changed

+232
-92
lines changed

6 files changed

+232
-92
lines changed

railties/CHANGELOG.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,42 @@
1+
* `--no-*` options now work with the app generator's `--minimal` option, and
2+
are both comprehensive and precise. For example:
3+
4+
```console
5+
$ rails new my_cool_app --minimal
6+
Based on the specified options, the following options will also be activated:
7+
8+
--skip-active-job [due to --minimal]
9+
--skip-action-mailer [due to --skip-active-job, --minimal]
10+
--skip-active-storage [due to --skip-active-job, --minimal]
11+
--skip-action-mailbox [due to --skip-active-storage, --minimal]
12+
--skip-action-text [due to --skip-active-storage, --minimal]
13+
--skip-javascript [due to --minimal]
14+
--skip-hotwire [due to --skip-javascript, --minimal]
15+
--skip-action-cable [due to --minimal]
16+
--skip-bootsnap [due to --minimal]
17+
--skip-dev-gems [due to --minimal]
18+
--skip-system-test [due to --minimal]
19+
20+
...
21+
22+
$ rails new my_cool_app --minimal --no-skip-active-storage
23+
Based on the specified options, the following options will also be activated:
24+
25+
--skip-action-mailer [due to --minimal]
26+
--skip-action-mailbox [due to --minimal]
27+
--skip-action-text [due to --minimal]
28+
--skip-javascript [due to --minimal]
29+
--skip-hotwire [due to --skip-javascript, --minimal]
30+
--skip-action-cable [due to --minimal]
31+
--skip-bootsnap [due to --minimal]
32+
--skip-dev-gems [due to --minimal]
33+
--skip-system-test [due to --minimal]
34+
35+
...
36+
```
37+
38+
*Brad Trick* and *Jonathan Hefner*
39+
140
* Add `--skip-dev-gems` option to app generator to skip adding development
241
gems (like `web-console`) to the Gemfile.
342

railties/lib/rails/generators/app_base.rb

Lines changed: 102 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require "digest/md5"
55
require "rails/version" unless defined?(Rails::VERSION)
66
require "open-uri"
7+
require "tsort"
78
require "uri"
89
require "rails/generators"
910
require "active_support/core_ext/array/extract_options"
@@ -33,73 +34,73 @@ def self.add_shared_options_for(name)
3334
class_option :database, type: :string, aliases: "-d", default: "sqlite3",
3435
desc: "Preconfigure for selected database (options: #{DATABASES.join('/')})"
3536

36-
class_option :skip_git, type: :boolean, aliases: "-G", default: false,
37+
class_option :skip_git, type: :boolean, aliases: "-G", default: nil,
3738
desc: "Skip git init, .gitignore and .gitattributes"
3839

39-
class_option :skip_keeps, type: :boolean, default: false,
40+
class_option :skip_keeps, type: :boolean, default: nil,
4041
desc: "Skip source control .keep files"
4142

4243
class_option :skip_action_mailer, type: :boolean, aliases: "-M",
43-
default: false,
44+
default: nil,
4445
desc: "Skip Action Mailer files"
4546

46-
class_option :skip_action_mailbox, type: :boolean, default: false,
47+
class_option :skip_action_mailbox, type: :boolean, default: nil,
4748
desc: "Skip Action Mailbox gem"
4849

49-
class_option :skip_action_text, type: :boolean, default: false,
50+
class_option :skip_action_text, type: :boolean, default: nil,
5051
desc: "Skip Action Text gem"
5152

52-
class_option :skip_active_record, type: :boolean, aliases: "-O", default: false,
53+
class_option :skip_active_record, type: :boolean, aliases: "-O", default: nil,
5354
desc: "Skip Active Record files"
5455

55-
class_option :skip_active_job, type: :boolean, default: false,
56+
class_option :skip_active_job, type: :boolean, default: nil,
5657
desc: "Skip Active Job"
5758

58-
class_option :skip_active_storage, type: :boolean, default: false,
59+
class_option :skip_active_storage, type: :boolean, default: nil,
5960
desc: "Skip Active Storage files"
6061

61-
class_option :skip_action_cable, type: :boolean, aliases: "-C", default: false,
62+
class_option :skip_action_cable, type: :boolean, aliases: "-C", default: nil,
6263
desc: "Skip Action Cable files"
6364

64-
class_option :skip_asset_pipeline, type: :boolean, aliases: "-A", default: false
65+
class_option :skip_asset_pipeline, type: :boolean, aliases: "-A", default: nil
6566

6667
class_option :asset_pipeline, type: :string, aliases: "-a", default: "sprockets",
6768
desc: "Choose your asset pipeline [options: sprockets (default), propshaft]"
6869

69-
class_option :skip_javascript, type: :boolean, aliases: ["-J", "--skip-js"], default: name == "plugin",
70+
class_option :skip_javascript, type: :boolean, aliases: ["-J", "--skip-js"], default: (true if name == "plugin"),
7071
desc: "Skip JavaScript files"
7172

72-
class_option :skip_hotwire, type: :boolean, default: false,
73+
class_option :skip_hotwire, type: :boolean, default: nil,
7374
desc: "Skip Hotwire integration"
7475

75-
class_option :skip_jbuilder, type: :boolean, default: false,
76+
class_option :skip_jbuilder, type: :boolean, default: nil,
7677
desc: "Skip jbuilder gem"
7778

78-
class_option :skip_test, type: :boolean, aliases: "-T", default: false,
79+
class_option :skip_test, type: :boolean, aliases: "-T", default: nil,
7980
desc: "Skip test files"
8081

81-
class_option :skip_system_test, type: :boolean, default: false,
82+
class_option :skip_system_test, type: :boolean, default: nil,
8283
desc: "Skip system test files"
8384

84-
class_option :skip_bootsnap, type: :boolean, default: false,
85+
class_option :skip_bootsnap, type: :boolean, default: nil,
8586
desc: "Skip bootsnap gem"
8687

87-
class_option :skip_dev_gems, type: :boolean, default: false,
88+
class_option :skip_dev_gems, type: :boolean, default: nil,
8889
desc: "Skip development gems (e.g., web-console)"
8990

90-
class_option :dev, type: :boolean, default: false,
91+
class_option :dev, type: :boolean, default: nil,
9192
desc: "Set up the #{name} with Gemfile pointing to your Rails checkout"
9293

93-
class_option :edge, type: :boolean, default: false,
94+
class_option :edge, type: :boolean, default: nil,
9495
desc: "Set up the #{name} with Gemfile pointing to Rails repository"
9596

96-
class_option :main, type: :boolean, default: false, aliases: "--master",
97+
class_option :main, type: :boolean, default: nil, aliases: "--master",
9798
desc: "Set up the #{name} with Gemfile pointing to Rails repository main branch"
9899

99100
class_option :rc, type: :string, default: nil,
100101
desc: "Path to file containing extra configuration options for rails command"
101102

102-
class_option :no_rc, type: :boolean, default: false,
103+
class_option :no_rc, type: :boolean, default: nil,
103104
desc: "Skip loading of extra configuration options from .railsrc file"
104105

105106
class_option :help, type: :boolean, aliases: "-h", group: :rails,
@@ -140,6 +141,70 @@ def build(meth, *args) # :doc:
140141
builder.public_send(meth, *args) if builder.respond_to?(meth)
141142
end
142143

144+
def deduce_implied_options(options, option_reasons, meta_options)
145+
active = options.transform_values { |value| [] if value }.compact
146+
irrevocable = (active.keys - meta_options).to_set
147+
148+
deduction_order = TSort.tsort(
149+
->(&block) { option_reasons.each_key(&block) },
150+
->(key, &block) { option_reasons[key]&.each(&block) }
151+
)
152+
153+
deduction_order.each do |name|
154+
reasons = option_reasons[name]&.select(&active).presence
155+
active[name] ||= reasons if reasons
156+
irrevocable << name if reasons&.any?(irrevocable)
157+
end
158+
159+
revoked = options.select { |name, value| value == false }.keys.to_set - irrevocable
160+
deduction_order.reverse_each do |name|
161+
revoked += option_reasons[name].to_a if revoked.include?(name)
162+
end
163+
revoked -= meta_options
164+
165+
active.filter_map do |name, reasons|
166+
reasons -= revoked.to_a
167+
[name, reasons] unless revoked.include?(name) || reasons.empty?
168+
end.to_h
169+
end
170+
171+
OPTION_IMPLICATIONS = { # :nodoc:
172+
skip_active_job: [:skip_action_mailer, :skip_active_storage],
173+
skip_active_record: [:skip_active_storage],
174+
skip_active_storage: [:skip_action_mailbox, :skip_action_text],
175+
skip_javascript: [:skip_hotwire],
176+
}
177+
178+
def imply_options(option_implications = OPTION_IMPLICATIONS, meta_options: [])
179+
option_reasons = {}
180+
option_implications.each do |reason, implications|
181+
implications.each do |implication|
182+
(option_reasons[implication.to_s] ||= []) << reason.to_s
183+
end
184+
end
185+
186+
@implied_options = deduce_implied_options(options, option_reasons, meta_options.map(&:to_s))
187+
@implied_options_conflicts = @implied_options.keys.select { |name| options[name] == false }
188+
self.options = options.merge(@implied_options.transform_values { true }).freeze
189+
end
190+
191+
def report_implied_options
192+
return if @implied_options.blank?
193+
194+
say "Based on the specified options, the following options will also be activated:"
195+
say ""
196+
@implied_options.each do |name, reasons|
197+
due_to = reasons.map { |reason| "--#{reason.dasherize}" }.join(", ")
198+
say " --#{name.dasherize} [due to #{due_to}]"
199+
if @implied_options_conflicts.include?(name)
200+
say " ERROR: Conflicts with --no-#{name.dasherize}", :red
201+
end
202+
end
203+
say ""
204+
205+
raise "Cannot proceed due to conflicting options" if @implied_options_conflicts.any?
206+
end
207+
143208
def create_root # :doc:
144209
valid_const?
145210

@@ -190,18 +255,16 @@ def asset_pipeline_gemfile_entry
190255
end
191256

192257
def include_all_railties? # :doc:
193-
[
194-
options.values_at(
195-
:skip_active_record,
196-
:skip_test,
197-
:skip_action_cable,
198-
:skip_active_job
199-
),
200-
skip_active_storage?,
201-
skip_action_mailer?,
202-
skip_action_mailbox?,
203-
skip_action_text?
204-
].flatten.none?
258+
options.values_at(
259+
:skip_action_cable,
260+
:skip_action_mailbox,
261+
:skip_action_mailer,
262+
:skip_action_text,
263+
:skip_active_job,
264+
:skip_active_record,
265+
:skip_active_storage,
266+
:skip_test,
267+
).none?
205268
end
206269

207270
def comment_if(value) # :doc:
@@ -226,19 +289,19 @@ def sqlite3? # :doc:
226289
end
227290

228291
def skip_active_storage? # :doc:
229-
options[:skip_active_storage] || options[:skip_active_record] || options[:skip_active_job]
292+
options[:skip_active_storage]
230293
end
231294

232295
def skip_action_mailer? # :doc:
233-
options[:skip_action_mailer] || options[:skip_active_job]
296+
options[:skip_action_mailer]
234297
end
235298

236299
def skip_action_mailbox? # :doc:
237-
options[:skip_action_mailbox] || skip_active_storage?
300+
options[:skip_action_mailbox]
238301
end
239302

240303
def skip_action_text? # :doc:
241-
options[:skip_action_text] || skip_active_storage?
304+
options[:skip_action_text]
242305
end
243306

244307
def skip_sprockets?
@@ -333,7 +396,7 @@ def javascript_gemfile_entry
333396
end
334397

335398
def hotwire_gemfile_entry
336-
return if options[:skip_javascript] || options[:skip_hotwire]
399+
return if options[:skip_hotwire]
337400

338401
turbo_rails_entry =
339402
GemfileEntry.floats "turbo-rails", "Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]"
@@ -457,7 +520,7 @@ def run_javascript
457520
end
458521

459522
def run_hotwire
460-
return if options[:skip_javascript] || options[:skip_hotwire] || !bundle_install?
523+
return if options[:skip_hotwire] || !bundle_install?
461524

462525
rails_command "turbo:install stimulus:install"
463526
end

railties/lib/rails/generators/rails/app/app_generator.rb

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -265,41 +265,42 @@ class AppGenerator < AppBase
265265
class_option :minimal, type: :boolean, desc: "Preconfigure a minimal rails app"
266266
class_option :javascript, type: :string, aliases: ["-j", "--js"], default: "importmap", desc: "Choose JavaScript approach [options: importmap (default), webpack, esbuild, rollup]"
267267
class_option :css, type: :string, aliases: "-c", desc: "Choose CSS processor [options: tailwind, bootstrap, bulma, postcss, sass... check https://github.com/rails/cssbundling-rails]"
268-
class_option :skip_bundle, type: :boolean, aliases: "-B", default: false, desc: "Don't run bundle install"
269-
class_option :skip_decrypted_diffs, type: :boolean, default: false, desc: "Don't configure git to show decrypted diffs of encrypted credentials"
268+
class_option :skip_bundle, type: :boolean, aliases: "-B", default: nil, desc: "Don't run bundle install"
269+
class_option :skip_decrypted_diffs, type: :boolean, default: nil, desc: "Don't configure git to show decrypted diffs of encrypted credentials"
270270

271271
def initialize(*args)
272272
super
273273

274+
imply_options({
275+
**OPTION_IMPLICATIONS,
276+
minimal: [
277+
:skip_action_cable,
278+
:skip_action_mailbox,
279+
:skip_action_mailer,
280+
:skip_action_text,
281+
:skip_active_job,
282+
:skip_active_storage,
283+
:skip_bootsnap,
284+
:skip_dev_gems,
285+
:skip_hotwire,
286+
:skip_javascript,
287+
:skip_jbuilder,
288+
:skip_system_test,
289+
],
290+
api: [
291+
:skip_asset_pipeline,
292+
:skip_javascript,
293+
],
294+
}, meta_options: [:minimal])
295+
274296
if !options[:skip_active_record] && !DATABASES.include?(options[:database])
275297
raise Error, "Invalid value for --database option. Supported preconfigurations are: #{DATABASES.join(", ")}."
276298
end
277299

278-
# Force sprockets and JavaScript to be skipped when generating API only apps.
279-
# Can't modify options hash as it's frozen by default.
280-
if options[:api]
281-
self.options = options.merge(skip_asset_pipeline: true, skip_javascript: true).freeze
282-
end
283-
284-
if options[:minimal]
285-
self.options = options.merge(
286-
skip_action_cable: true,
287-
skip_action_mailer: true,
288-
skip_action_mailbox: true,
289-
skip_action_text: true,
290-
skip_active_job: true,
291-
skip_active_storage: true,
292-
skip_bootsnap: true,
293-
skip_dev_gems: true,
294-
skip_javascript: true,
295-
skip_jbuilder: true,
296-
skip_system_test: true,
297-
skip_hotwire: true).freeze
298-
end
299-
300300
@after_bundle_callbacks = []
301301
end
302302

303+
public_task :report_implied_options
303304
public_task :set_default_accessors!
304305
public_task :create_root
305306
public_task :target_rails_prerelease

railties/lib/rails/generators/rails/plugin/plugin_generator.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,12 +218,14 @@ class PluginGenerator < AppBase # :nodoc:
218218
def initialize(*args)
219219
@dummy_path = nil
220220
super
221+
imply_options
221222

222223
if !engine? || !with_dummy_app?
223224
self.options = options.merge(skip_asset_pipeline: true).freeze
224225
end
225226
end
226227

228+
public_task :report_implied_options
227229
public_task :set_default_accessors!
228230
public_task :create_root
229231

0 commit comments

Comments
 (0)