padrino-gen 0.16.2 → 0.18.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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/README.rdoc +8 -8
  3. data/lib/padrino-gen/command.rb +1 -1
  4. data/lib/padrino-gen/generators/actions.rb +233 -18
  5. data/lib/padrino-gen/generators/app/app.rb.tt +4 -1
  6. data/lib/padrino-gen/generators/app.rb +2 -2
  7. data/lib/padrino-gen/generators/cli.rb +17 -7
  8. data/lib/padrino-gen/generators/component.rb +20 -13
  9. data/lib/padrino-gen/generators/components/actions.rb +7 -6
  10. data/lib/padrino-gen/generators/components/mocks/mocha.rb +19 -4
  11. data/lib/padrino-gen/generators/components/mocks/rr.rb +24 -6
  12. data/lib/padrino-gen/generators/components/orms/activerecord.rb +73 -35
  13. data/lib/padrino-gen/generators/components/orms/mongoid.rb +37 -2
  14. data/lib/padrino-gen/generators/components/orms/sequel.rb +88 -37
  15. data/lib/padrino-gen/generators/components/renderers/liquid.rb +1 -2
  16. data/lib/padrino-gen/generators/components/scripts/dojo.rb +3 -3
  17. data/lib/padrino-gen/generators/components/scripts/jquery.rb +3 -3
  18. data/lib/padrino-gen/generators/components/scripts/mootools.rb +3 -3
  19. data/lib/padrino-gen/generators/components/scripts/prototype.rb +4 -4
  20. data/lib/padrino-gen/generators/components/scripts/vite.rb +91 -0
  21. data/lib/padrino-gen/generators/components/stylesheets/sass.rb +1 -1
  22. data/lib/padrino-gen/generators/components/stylesheets/scss.rb +1 -1
  23. data/lib/padrino-gen/generators/components/stylesheets/tailwind.rb +78 -0
  24. data/lib/padrino-gen/generators/components/tests/bacon.rb +4 -2
  25. data/lib/padrino-gen/generators/components/tests/cucumber.rb +14 -9
  26. data/lib/padrino-gen/generators/components/tests/minitest.rb +3 -3
  27. data/lib/padrino-gen/generators/components/tests/rspec.rb +14 -16
  28. data/lib/padrino-gen/generators/components/tests/shoulda.rb +5 -5
  29. data/lib/padrino-gen/generators/components/tests/testunit.rb +5 -5
  30. data/lib/padrino-gen/generators/controller.rb +6 -1
  31. data/lib/padrino-gen/generators/helper.rb +1 -1
  32. data/lib/padrino-gen/generators/mailer.rb +1 -1
  33. data/lib/padrino-gen/generators/migration.rb +2 -2
  34. data/lib/padrino-gen/generators/model.rb +4 -13
  35. data/lib/padrino-gen/generators/plugin.rb +1 -2
  36. data/lib/padrino-gen/generators/project/config/apps.rb.tt +7 -2
  37. data/lib/padrino-gen/generators/project/config/boot.rb +7 -0
  38. data/lib/padrino-gen/generators/project.rb +24 -8
  39. data/lib/padrino-gen/generators/runner.rb +37 -10
  40. data/lib/padrino-gen/generators/task.rb +1 -1
  41. data/lib/padrino-gen/generators/templates/Gemfile.tt +3 -1
  42. data/lib/padrino-gen/generators/templates/static/js/jquery.js +2 -4
  43. data/lib/padrino-gen/generators/templates/static/ujs/jquery.js +6 -7
  44. data/lib/padrino-gen/padrino-tasks/activerecord.rb +79 -26
  45. data/lib/padrino-gen/padrino-tasks/mongoid.rb +5 -1
  46. data/lib/padrino-gen/padrino-tasks/sequel.rb +37 -16
  47. data/lib/padrino-gen/padrino-tasks/sql-helpers.rb +172 -44
  48. data/test/helper.rb +2 -3
  49. data/test/test_activerecord_tasks.rb +241 -0
  50. data/test/test_app_generator.rb +18 -5
  51. data/test/test_cli.rb +152 -2
  52. data/test/test_component_generator.rb +547 -2
  53. data/test/test_controller_generator.rb +79 -21
  54. data/test/test_generator.rb +24 -0
  55. data/test/test_helper_generator.rb +4 -3
  56. data/test/test_mailer_generator.rb +5 -4
  57. data/test/test_migration_generator.rb +146 -39
  58. data/test/test_model_generator.rb +124 -203
  59. data/test/test_mongoid_tasks.rb +74 -0
  60. data/test/test_plugin_generator.rb +199 -8
  61. data/test/test_project_generator.rb +348 -132
  62. data/test/test_sequel_tasks.rb +156 -0
  63. data/test/test_sql_helpers.rb +139 -64
  64. data/test/test_sql_helpers_dump.rb +328 -0
  65. data/test/test_task_generator.rb +9 -5
  66. metadata +16 -46
  67. data/lib/padrino-gen/generators/components/orms/couchrest.rb +0 -55
  68. data/lib/padrino-gen/generators/components/orms/datamapper.rb +0 -170
  69. data/lib/padrino-gen/generators/components/orms/dynamoid.rb +0 -67
  70. data/lib/padrino-gen/generators/components/orms/minirecord.rb +0 -175
  71. data/lib/padrino-gen/generators/components/orms/mongomapper.rb +0 -44
  72. data/lib/padrino-gen/generators/components/orms/mongomatic.rb +0 -88
  73. data/lib/padrino-gen/generators/components/orms/ohm.rb +0 -67
  74. data/lib/padrino-gen/generators/components/orms/ripple.rb +0 -75
  75. data/lib/padrino-gen/generators/components/scripts/extcore.rb +0 -10
  76. data/lib/padrino-gen/generators/components/stylesheets/compass/application.scss +0 -43
  77. data/lib/padrino-gen/generators/components/stylesheets/compass/partials/_base.scss +0 -12
  78. data/lib/padrino-gen/generators/components/stylesheets/compass.rb +0 -28
  79. data/lib/padrino-gen/generators/components/stylesheets/less.rb +0 -25
  80. data/lib/padrino-gen/padrino-tasks/datamapper.rb +0 -96
  81. data/lib/padrino-gen/padrino-tasks/minirecord.rb +0 -19
  82. data/lib/padrino-gen/padrino-tasks/mongomapper.rb +0 -59
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f23d1c3fcf2f3fb73ac268383f3ad934b9cb3a19044fb26e5a1768fb0057034
4
- data.tar.gz: 3f8c4eed5f141cdcff42e93236666fe67765ed906a1eeee3e4d77181b4b6a472
3
+ metadata.gz: '039917b81dc2bfbb846774da7452682d6f29af8bc1f4d76609b2ec10d8392912'
4
+ data.tar.gz: 451d8a83d39747bf6648d0b434f211a4571e5694f5cc70b45e9f9844ef7bfa7c
5
5
  SHA512:
6
- metadata.gz: 123166a6d9ef6c9c27d0b12d464380a53ec631e1ac06379f7a6685d6710d834f56f63cb5f044747186d82359aea9370caa45b29e52fd514d6eb82d5794c91270
7
- data.tar.gz: 0c9b6c6c90a123b81afdd15311ceba558afd10519506aa416abcddb52c83e0146338946d79a8ba5651f93e23d2d59c593e50d25cdf4e696fd73e3809e165aba4
6
+ metadata.gz: c0769c35ccfe2cff70e99aa0c137b825e285e831649945d18e07e5ea72b44efcc1214f3fb3ebc63661d0b0c8371e84c78dcb5fff6b7472471c83176e3dd1b40e
7
+ data.tar.gz: 45b9f905e803023d7932ceb487ade75bbcd5df395754f820cc86626176de272bb422ef5696d3665027b80f7a8f1d34436f266b02c8bb27d9938fbbcce410baf1
data/README.rdoc CHANGED
@@ -24,13 +24,13 @@ The simplest possible command to generate a base application would be:
24
24
 
25
25
  $ padrino-gen project demo_project
26
26
 
27
- This would construct a Padrino application DemoApp (which extends from Padrino::Application)
27
+ This would construct a Padrino application DemoProject::App (which extends from Padrino::Application)
28
28
  inside the folder 'demo_project' at our current path. Inside the application there would be configuration and
29
29
  setup performed for the default components.
30
30
 
31
31
  You can define specific components to be used:
32
32
 
33
- $ padrino-gen project demo_project -t rspec -r haml -m rr -s jquery -d datamapper
33
+ $ padrino-gen project demo_project -t rspec -e haml -m rr -s jquery -d sequel
34
34
 
35
35
  You can also instruct the generator to skip a certain component to avoid using one at all (or to use your own):
36
36
 
@@ -39,11 +39,11 @@ You can also instruct the generator to skip a certain component to avoid using o
39
39
  The available components and their default options are listed below:
40
40
 
41
41
  test:: none (default), bacon, shoulda, cucumber, rspec, minitest, testunit
42
- renderer:: none (default), slim, erb, erubis, liquid, haml
43
- stylesheet:: none (default), less, compass, sass, scss
42
+ renderer:: none (default), slim, erb, liquid, haml
43
+ stylesheet:: none (default), tailwind, sass, scss
44
44
  mock:: none (default), mocha, rr
45
- script:: none (default), jquery, prototype, mootools, extcore, dojo
46
- orm:: none (default), datamapper, mongomapper, mongoid, activerecord, minirecord, sequel, couchrest, ohm, mongomatic, ripple, dynamoid
45
+ script:: none (default), jquery, prototype, mootools, dojo, vite
46
+ orm:: none (default), mongoid, activerecord, sequel
47
47
 
48
48
  In addition, you can generate projects based on existing templates:
49
49
 
@@ -56,9 +56,9 @@ To learn more about the project generator, check out the guide to
56
56
 
57
57
  Padrino provides support for plugins to be executed within your application. For example:
58
58
 
59
- $ padrino-gen plugin hoptoad
59
+ $ padrino-gen plugin bulma
60
60
 
61
- would install the hoptoad middleware into your application automatically.
61
+ would download the Bulma stylesheet into your application's public/stylesheets.
62
62
 
63
63
  To learn more about the plugin system, check out the guide to
64
64
  {Padrino Generators}[http://padrinorb.com/guides/generators/plugins/].
@@ -13,6 +13,6 @@ module Padrino
13
13
  #
14
14
  def self.bin_gen(*args)
15
15
  @_padrino_gen_bin ||= [Padrino.ruby_command, File.expand_path('../../bin/padrino-gen', __dir__)]
16
- system(*@_padrino_gen_bin, *args)
16
+ system(*@_padrino_gen_bin, *args.map { |arg| arg.is_a?(Symbol) ? arg.to_s : arg })
17
17
  end
18
18
  end
@@ -10,6 +10,12 @@ module Padrino
10
10
  #
11
11
  DEFAULT_HELPER_NAME = 'Helper'.freeze
12
12
 
13
+ ##
14
+ # The SQL adapters for --adapter, and the ORM components that set up their database with one.
15
+ #
16
+ SQL_ADAPTERS = %w[mysql mysql2 mysql-gem postgres sqlite].freeze
17
+ SQL_ADAPTER_ORMS = %w[activerecord sequel].freeze
18
+
13
19
  ##
14
20
  # Common actions needed to support project and component generation.
15
21
  #
@@ -59,9 +65,19 @@ module Padrino
59
65
  def apply_component_for(choice, component)
60
66
  # I need to override Thor#apply because for unknown reason verbose: false break tasks.
61
67
  path = File.expand_path(__dir__ + "/components/#{component.to_s.pluralize}/#{choice}.rb")
68
+ unless File.exist?(path)
69
+ # An existing project's .components can name a component that has since been removed.
70
+ available = Dir["#{__dir__}/components/#{component.to_s.pluralize}/*.rb"].map { |lib| File.basename(lib, '.rb') }.sort
71
+ if choice.to_s.empty?
72
+ say "The project's .components sets no #{component} component (available: #{available.join(', ')})."
73
+ else
74
+ say "The #{choice} #{component} component is no longer available in Padrino (available: #{available.join(', ')})."
75
+ end
76
+ exit 1
77
+ end
62
78
  say_status :apply, "#{component.to_s.pluralize}/#{choice}"
63
79
  shell.padding += 1
64
- instance_eval(File.read(path))
80
+ instance_eval(File.read(path, encoding: 'UTF-8'))
65
81
  shell.padding -= 1
66
82
  end
67
83
 
@@ -152,11 +168,130 @@ module Padrino
152
168
  choice = options[component]
153
169
  until valid_choice?(component, choice)
154
170
  say("Option for --#{component} '#{choice}' is not available.", :red)
155
- choice = ask("Please enter a valid option for #{component}:", limited_to: choices)
171
+ choice = ask_valid_option(component, choices, "Please enter a valid option for #{component}:")
156
172
  end
157
173
  choice
158
174
  end
159
175
 
176
+ ##
177
+ # Prompts the user if necessary until options[:adapter] or the answer is a valid SQL adapter, and keeps it.
178
+ #
179
+ # @return [String] Name of the SQL adapter.
180
+ #
181
+ # @example
182
+ # resolve_valid_adapter # => 'sqlite'
183
+ #
184
+ def resolve_valid_adapter
185
+ @_adapter ||= begin
186
+ adapter = options[:adapter] || stored_adapter || 'sqlite'
187
+ until SQL_ADAPTERS.include?(adapter)
188
+ say("Option for --adapter '#{adapter}' is not available.", :red)
189
+ adapter = ask_valid_option(:adapter, SQL_ADAPTERS, 'Please enter a valid option for adapter:')
190
+ end
191
+ adapter
192
+ end
193
+ end
194
+
195
+ ##
196
+ # Returns whether `padrino g component` is applying a component the project already has,
197
+ # rather than adding it or switching to it. Files the project may have edited stay as they are then.
198
+ #
199
+ # @param [Symbol] component
200
+ # The type of component module.
201
+ #
202
+ # @return [Boolean] Whether the chosen component is the one the project has.
203
+ #
204
+ def reapplying_component?(component)
205
+ !@_stored_components.nil? && @_stored_components[component].to_s == @_components[component].to_s
206
+ end
207
+
208
+ ##
209
+ # Removes the lines the project's mock component added to the test helper, when `padrino g component`
210
+ # switches to another mock. Each mock file defines mock_setup_lines(test), the lines its setup_mock adds;
211
+ # the previous mock's file is evaluated on a separate object, so its methods don't replace the new mock's.
212
+ # Only lines that are still exactly as written (indentation aside) are removed; other edits stay.
213
+ #
214
+ # @param [String] test
215
+ # The test component the new mock is set up for.
216
+ #
217
+ def remove_previous_mock_setup(test)
218
+ previous = @_stored_components && @_stored_components[:mock].to_s
219
+ return if previous.nil? || previous.empty? || previous == 'none' || reapplying_component?(:mock)
220
+ source = File.expand_path("components/mocks/#{previous}.rb", __dir__)
221
+ return unless File.exist?(source)
222
+
223
+ probe = Object.new
224
+ probe.instance_eval(File.read(source, encoding: 'UTF-8'), source)
225
+ setup = probe.respond_to?(:mock_setup_lines) && probe.mock_setup_lines(test)
226
+ return unless setup && File.exist?(destination_root(setup[:path]))
227
+
228
+ content = File.binread(destination_root(setup[:path]))
229
+ # The mocks insert after the same line; if the project edited it, the new mock can't be added, so keep the old one.
230
+ return unless content.include?(setup[:after].b)
231
+
232
+ lines = setup[:text].each_line.map(&:strip).reject(&:empty?).uniq
233
+ pattern = /^[ \t]*(?:#{lines.map { |line| Regexp.escape(line) }.join('|')})[ \t]*(?:\n|\z)/
234
+ gsub_file(setup[:path], pattern, '') if content.match?(pattern)
235
+ end
236
+
237
+ ##
238
+ # Returns whether config/database.rb stays: the orm is the project's, and so is any --adapter given.
239
+ #
240
+ # @return [Boolean] Whether to keep an existing database config.
241
+ #
242
+ def keep_database_config?
243
+ # An --adapter corrected at the prompt counts as the corrected one, which resolve_valid_adapter keeps in @_adapter.
244
+ given = options[:adapter] && (@_adapter || options[:adapter])
245
+ reapplying_component?(:orm) && [nil, stored_adapter].include?(given)
246
+ end
247
+
248
+ ##
249
+ # Returns whether the SQL adapter of the project is unknown: it is applying the orm the project
250
+ # has, without --adapter, in a project that has a database config but no stored adapter
251
+ # (generated by an older version). The config stays and no driver is added then.
252
+ #
253
+ # @return [Boolean] Whether the adapter is unknown.
254
+ #
255
+ def adapter_unknown?
256
+ return @_adapter_unknown if defined?(@_adapter_unknown)
257
+
258
+ @_adapter_unknown = reapplying_component?(:orm) && options[:adapter].nil? && stored_adapter.nil? &&
259
+ File.exist?(destination_root('config/database.rb'))
260
+ end
261
+
262
+ ##
263
+ # Returns the SQL adapter stored for the project, if any. Projects generated by older versions have none.
264
+ #
265
+ # @return [String, nil] Name of the SQL adapter.
266
+ #
267
+ def stored_adapter
268
+ @_stored_components && @_stored_components[:adapter]
269
+ end
270
+
271
+ ##
272
+ # Asks once for one of the valid choices of an option; callers re-ask while the answer is invalid.
273
+ # Thor's limited_to: would repeat the prompt forever at the end of input (no terminal, empty pipe),
274
+ # so that case raises a Thor::Error instead: the generator's start prints its message and exits with status 1.
275
+ #
276
+ # @param [Symbol] option
277
+ # The name of the option.
278
+ # @param [Array<String>] choices
279
+ # The valid choices for the option.
280
+ # @param [String] statement
281
+ # The prompt to show.
282
+ #
283
+ # @return [String] The answer entered.
284
+ #
285
+ # @example
286
+ # ask_valid_option(:adapter, %w[mysql sqlite], 'Please, choose a proper adapter:')
287
+ #
288
+ def ask_valid_option(option, choices, statement)
289
+ answer = ask("#{statement} [#{choices.join(', ')}]")
290
+ raise Thor::Error, "No input left to choose a valid --#{option}; pass one of: #{choices.join(', ')}" if answer.nil?
291
+
292
+ answer
293
+ end
294
+
160
295
  ##
161
296
  # Returns true if the option passed is a valid choice for component.
162
297
  #
@@ -215,12 +350,24 @@ module Padrino
215
350
  File.exist?(destination_root('config/boot.rb'))
216
351
  end
217
352
 
353
+ ##
354
+ # Says that the generator must run at the root of a Padrino application and exits with status 1.
355
+ #
356
+ def exit_outside_app_root!
357
+ say 'You are not at the root of a Padrino application! (config/boot.rb not found)'
358
+ exit 1
359
+ end
360
+
218
361
  ##
219
362
  # Returns true if constant name already exists.
220
363
  #
221
364
  def already_exists?(name, project_name = nil)
222
365
  project_name = project_name ? (Object.const_get(project_name) rescue nil) : nil
223
366
  Object.const_defined?(name) || project_name&.const_defined?(name)
367
+ rescue ::NameError => e
368
+ # Only Ruby's rejection of the name itself; a NameError from code that autoloading runs is a real error.
369
+ raise unless e.instance_of?(::NameError) && e.message.start_with?('wrong constant name')
370
+ raise Thor::Error, "#{name} is not a valid constant name"
224
371
  end
225
372
 
226
373
  ##
@@ -315,6 +462,18 @@ module Padrino
315
462
  gem_names.reverse_each { |lib| insert_into_gemfile(lib, options) }
316
463
  end
317
464
 
465
+ ##
466
+ # Returns true if the project runs on Falcon, which serves each request in
467
+ # a fiber. Uses the --server option when generating a project, and the
468
+ # project's Gemfile when adding a component later.
469
+ #
470
+ def falcon_server?
471
+ return options[:server] == 'falcon' if options[:server]
472
+
473
+ gemfile = destination_root('Gemfile')
474
+ File.exist?(gemfile) && File.read(gemfile).match?(/^\s*gem ['"]falcon['"]/)
475
+ end
476
+
318
477
  ##
319
478
  # Inserts a required gem into the Gemfile to add the bundler dependency.
320
479
  #
@@ -329,9 +488,18 @@ module Padrino
329
488
  # insert_into_gemfile(name, group: 'test', version: ">1.2.3")
330
489
  #
331
490
  def insert_into_gemfile(name, options = {})
332
- after_pattern = options[:group] ? "#{options[:group].to_s.capitalize} requirements\n" : "Component requirements\n"
333
- version = options.delete(:version)
334
- gem_options = options.map { |k, v| k.to_s == 'require' && [true, false].include?(v) ? "#{k}: #{v}" : "#{k}: '#{v}'" }.join(', ')
491
+ # Generated Gemfiles only have a "Test requirements" section; other groups go with the components.
492
+ group_pattern = "#{options[:group].to_s.capitalize} requirements\n" if options[:group]
493
+ gemfile = destination_root('Gemfile')
494
+ after_pattern = group_pattern if group_pattern && File.exist?(gemfile) && File.read(gemfile).include?(group_pattern)
495
+ after_pattern ||= "Component requirements\n"
496
+ version = options[:version]
497
+ gem_options = options.reject { |k, _| k == :version }.map do |k, v|
498
+ if v.is_a?(Array) then "#{k}: #{v.inspect}"
499
+ elsif k.to_s == 'require' && [true, false].include?(v) then "#{k}: #{v}"
500
+ else "#{k}: '#{v}'"
501
+ end
502
+ end.join(', ')
335
503
  write_option = gem_options.empty? ? '' : ", #{gem_options}"
336
504
  write_version = version ? ", '#{version}'" : ''
337
505
  include_text = "gem '#{name}'" << write_version << write_option << "\n"
@@ -347,7 +515,7 @@ module Padrino
347
515
  # method hook to call from Padrino, i.e :after_load, :before_load.
348
516
  #
349
517
  # @example
350
- # insert_hook("DataMapper.finalize", :after_load)
518
+ # insert_hook("SomeORM.finalize", :after_load)
351
519
  #
352
520
  def insert_hook(include_text, where)
353
521
  inject_into_file('config/boot.rb', " #{include_text}\n", after: "Padrino.#{where} do\n")
@@ -374,16 +542,18 @@ module Padrino
374
542
  # Name of the initializer.
375
543
  # @param [String] data
376
544
  # Text to generate into the initializer file.
545
+ # @param [Boolean] skip
546
+ # Whether to keep an existing initializer file.
377
547
  #
378
548
  # @example
379
549
  # initializer(:test, "some stuff here")
380
550
  # #=> generates 'lib/test_init.rb'
381
551
  #
382
- def initializer(name, data = nil)
552
+ def initializer(name, data = nil, skip: false)
383
553
  @_init_name, @_init_data = name, data
384
554
  register = data ? " register #{name.to_s.underscore.camelize}Initializer\n" : " register #{name}\n"
385
555
  inject_into_file destination_root('/app/app.rb'), register, after: "Padrino::Application\n"
386
- template 'templates/initializer.rb.tt', destination_root("/config/initializers/#{name}.rb") if data
556
+ template 'templates/initializer.rb.tt', destination_root("/config/initializers/#{name}.rb"), skip: skip if data
387
557
  end
388
558
 
389
559
  ##
@@ -392,13 +562,15 @@ module Padrino
392
562
  # Name of the middleware.
393
563
  # @param [String] source
394
564
  # Text to generate into the middleware file.
565
+ # @param [Boolean] skip
566
+ # Whether to keep an existing middleware file.
395
567
  #
396
568
  # @example
397
569
  # middleware(:hello, "class Hello\nend")
398
570
  # #=> generates 'lib/hello_middleware.rb'
399
571
  #
400
- def middleware(name, source)
401
- create_file destination_root("lib/#{name}_middleware.rb"), source
572
+ def middleware(name, source, skip: false)
573
+ create_file destination_root("lib/#{name}_middleware.rb"), source, skip: skip
402
574
  insert_middleware name.to_s.underscore.camelize
403
575
  end
404
576
 
@@ -436,11 +608,46 @@ module Padrino
436
608
  #
437
609
  def run_bundler
438
610
  say 'Bundling application dependencies using bundler...', :yellow
439
- in_root { run 'bundle install --binstubs' }
611
+ in_root do
612
+ with_project_bundle_env do
613
+ run 'bundle install'
614
+ run 'bundle binstubs --all'
615
+ end
616
+ end
440
617
  end
441
618
 
619
+ # The variables Bundler sets up for its children (Bundler::EnvironmentPreserver::BUNDLER_KEYS).
620
+ BUNDLER_ENV_KEYS = %w[BUNDLE_BIN_PATH BUNDLE_GEMFILE BUNDLE_LOCKFILE BUNDLER_VERSION BUNDLER_SETUP GEM_HOME
621
+ GEM_PATH MANPATH PATH RB_USER_INSTALL RUBYLIB RUBYOPT].freeze
622
+
442
623
  ##
443
- # Raise SystemExit if the app does not exist.
624
+ # Runs the block with the environment for a command run on the project: the project's Gemfile
625
+ # and lockfile, and none of the Bundler setup (RUBYOPT=-rbundler/setup, BUNDLE_GEMFILE...) that a
626
+ # caller's `bundle exec` or binstub exported. Other variables are passed on as they are. With
627
+ # +setup+, the command loads the project's bundle first, as `bundle exec` would in the project.
628
+ #
629
+ # @param [Boolean] setup
630
+ # Whether the command requires bundler/setup.
631
+ #
632
+ def with_project_bundle_env(setup: false)
633
+ saved = ENV.to_h
634
+ if defined?(Bundler) && Bundler.respond_to?(:original_env)
635
+ original = Bundler.original_env
636
+ # ceiling: a template's own changes to Bundler's keys (PATH, RUBYOPT, GEM_PATH...) made after Bundler loaded
637
+ # don't reach its steps; upgrade: re-apply the diff from a snapshot taken when the runner starts.
638
+ BUNDLER_ENV_KEYS.each { |key| original.key?(key) ? ENV[key] = original[key] : ENV.delete(key) }
639
+ ENV.keys.grep(/\ABUNDLER_ORIG_/).each { |key| ENV.delete(key) }
640
+ end
641
+ ENV['BUNDLE_GEMFILE'] = destination_root('Gemfile')
642
+ ENV['BUNDLE_LOCKFILE'] = destination_root('Gemfile.lock')
643
+ ENV['RUBYOPT'] = ['-rbundler/setup', ENV['RUBYOPT']].compact.join(' ') if setup
644
+ yield
645
+ ensure
646
+ ENV.replace(saved) if saved
647
+ end
648
+
649
+ ##
650
+ # Exits with status 1 if the app does not exist.
444
651
  #
445
652
  # @param [String] app
446
653
  # Directory name of application.
@@ -456,7 +663,7 @@ module Padrino
456
663
  say "Unable to locate '#{app.underscore.camelize}' application "
457
664
  say '================================================================='
458
665
  say
459
- raise SystemExit
666
+ exit 1
460
667
  end
461
668
 
462
669
  ##
@@ -488,7 +695,8 @@ module Padrino
488
695
  end
489
696
 
490
697
  ##
491
- # Ensures that project name is valid, else raise an NameError.
698
+ # Ensures that project name is valid, else raises a Thor::Error: the generator's start prints its
699
+ # message and exits with status 1.
492
700
  #
493
701
  # @param [String] name
494
702
  # Name of project.
@@ -501,9 +709,9 @@ module Padrino
501
709
  #
502
710
  def valid_constant?(name)
503
711
  if name =~ /^\d/
504
- raise ::NameError, "Constant name #{name} cannot start with numbers"
712
+ raise Thor::Error, "Constant name #{name} cannot start with numbers"
505
713
  elsif name =~ /^\W/
506
- raise ::NameError, "Constant name #{name} cannot start with non-word character"
714
+ raise Thor::Error, "Constant name #{name} cannot start with non-word character"
507
715
  end
508
716
  end
509
717
 
@@ -519,7 +727,7 @@ module Padrino
519
727
  #
520
728
  def validate_namespace(name)
521
729
  valid_constant? name
522
- name.match(/^[[:alnum:]_]+$/) || raise(::NameError, "Namespace '#{name}' must consist only of alphanumeric characters or '_'")
730
+ name.match(/^[[:alnum:]_]+$/) || raise(Thor::Error, "Namespace '#{name}' must consist only of alphanumeric characters or '_'")
523
731
  end
524
732
 
525
733
  ##
@@ -580,7 +788,7 @@ module Padrino
580
788
  [:stylesheet, 'stylesheet engine', { aliases: '-c', default: :none }]
581
789
  ].each do |name, caption, opts|
582
790
  opts[:default] = '' if options[:default] == false
583
- component_option name, caption, opts.merge(choices: Dir["#{__dir__}/components/#{name.to_s.pluralize}/*.rb"].map { |lib| File.basename(lib, '.rb').to_sym })
791
+ component_option name, caption, opts.merge(choices: Dir["#{__dir__}/components/#{name.to_s.pluralize}/*.rb"].map { |lib| File.basename(lib, '.rb') }.sort.map(&:to_sym))
584
792
  end
585
793
  end
586
794
 
@@ -598,6 +806,13 @@ module Padrino
598
806
  @require_arguments
599
807
  end
600
808
 
809
+ ##
810
+ # Tells Thor to exit with status 1 when it rejects the arguments, e.g. a missing required argument.
811
+ #
812
+ def exit_on_failure?
813
+ true
814
+ end
815
+
601
816
  ##
602
817
  # Returns the compiled list of component types which can be specified.
603
818
  #
@@ -4,7 +4,10 @@ module <%= @project_name %>
4
4
  register Padrino::Mailer
5
5
  register Padrino::Helpers
6
6
  <% end -%>
7
- enable :sessions
7
+ # The session cookie is Secure (sent over HTTPS only) in production. Over plain HTTP
8
+ # the app then sends no session cookie, so logins and CSRF-protected forms fail. Behind
9
+ # a proxy that terminates TLS, have it send X-Forwarded-Proto: https, or set secure: false.
10
+ set :sessions, same_site: :lax, secure: Padrino.env == :production
8
11
 
9
12
  ##
10
13
  # Caching support.
@@ -43,7 +43,7 @@ module Padrino
43
43
  elsif !options[:force]
44
44
  say "#{@app_name} already exists."
45
45
  say 'Please, change the name.'
46
- return
46
+ exit 1
47
47
  end
48
48
  end
49
49
 
@@ -67,7 +67,7 @@ module Padrino
67
67
  say "This application has been mounted to /#{@app_name.downcase}"
68
68
  say "You can configure a different path by editing 'config/apps.rb'"
69
69
  else
70
- say 'You are not at the root of a Padrino application! (config/boot.rb not found)'
70
+ exit_outside_app_root!
71
71
  end
72
72
  end
73
73
  end
@@ -17,11 +17,18 @@ module Padrino
17
17
  # custom generators, so is necessary know who are.
18
18
  #
19
19
  def load_boot
20
+ saved_env = { 'BUNDLE_GEMFILE' => ENV['BUNDLE_GEMFILE'], 'PADRINO_LOG_LEVEL' => ENV['PADRINO_LOG_LEVEL'] }
20
21
  ENV['PADRINO_LOG_LEVEL'] ||= 'test'
21
- ENV['BUNDLE_GEMFILE'] = File.join(options[:root], 'Gemfile') if options[:root]
22
- boot = options[:root] ? File.join(options[:root], 'config/boot.rb') : 'config/boot.rb'
22
+ root = File.expand_path(options[:root] || '.')
23
+ ENV['BUNDLE_GEMFILE'] = File.join(root, 'Gemfile') if options[:root]
24
+ boot = File.join(root, 'config/boot.rb')
23
25
  if File.exist?(boot)
24
- require File.expand_path(boot)
26
+ # Boot from the project root, as `padrino start`, rake and the binstubs do, so that its relative paths
27
+ # (such as a sqlite database in config/database.rb) don't resolve against the caller's directory.
28
+ # ceiling: only the boot runs there; a path the project opens lazily after boot (an ActiveRecord connection
29
+ # to a relative path) still resolves against the caller's directory. Running the whole generator from the
30
+ # root would cover it.
31
+ Dir.chdir(root) { require boot }
25
32
  else
26
33
  require 'padrino-support'
27
34
  end
@@ -29,8 +36,7 @@ module Padrino
29
36
  puts "=> Problem loading #{boot}"
30
37
  puts ["=> #{e.message}", *e.backtrace].join("\n ")
31
38
  ensure
32
- ENV.delete('BUNDLE_GEMFILE')
33
- ENV.delete('PADRINO_LOG_LEVEL')
39
+ ENV.update(saved_env) # restores the caller's values, or removes the ones it didn't have
34
40
  end
35
41
 
36
42
  ##
@@ -43,10 +49,14 @@ module Padrino
43
49
  generator_class = Padrino::Generators.mappings[generator_kind]
44
50
 
45
51
  if generator_class
46
- args = ARGV.empty? && generator_class.require_arguments? ? ['-h'] : ARGV
47
- generator_class.start(args)
52
+ if ARGV.empty? && generator_class.require_arguments?
53
+ generator_class.start(['-h'])
54
+ exit 1
55
+ end
56
+ generator_class.start(ARGV)
48
57
  else
49
58
  puts "Please specify generator to use (#{Padrino::Generators.mappings.keys.join(', ')})"
59
+ exit 1 unless generator_kind == :help
50
60
  end
51
61
  end
52
62
  end
@@ -17,7 +17,7 @@ module Padrino
17
17
 
18
18
  class_option :app, desc: 'The application name', aliases: '-n', default: nil, type: :string
19
19
  class_option :root, desc: 'The root destination', aliases: '-r', default: '.', type: :string
20
- class_option :adapter, desc: 'SQL adapter for ORM (sqlite, mysql, mysql2, mysql-gem, postgres)', aliases: '-a', default: 'sqlite', type: :string
20
+ class_option :adapter, desc: 'SQL adapter for ORM (sqlite, mysql, mysql2, mysql-gem, postgres)', aliases: '-a', default: nil, type: :string
21
21
 
22
22
  defines_component_options default: false
23
23
 
@@ -28,6 +28,7 @@ module Padrino
28
28
  self.destination_root = options[:root]
29
29
  if in_app_root?
30
30
  @_components = options.class.new(options.slice(*self.class.component_types))
31
+ @_stored_components = retrieve_component_config(destination_root('.components'))
31
32
  @app_name = (options[:app] || 'App').gsub(/\W/, '_').camelize
32
33
  if @_components.values.delete_if(&:empty?).empty?
33
34
  self.class.start(['-h'])
@@ -41,30 +42,36 @@ module Padrino
41
42
  exit
42
43
  end
43
44
 
44
- self.class.component_types.each do |comp|
45
- next if @_components[comp].empty?
46
-
47
- choice = @_components[comp] = resolve_valid_choice(comp)
45
+ # Settle every choice, switch and check before applying any, so a failure leaves the project unchanged.
46
+ chosen = self.class.component_types.reject { |comp| @_components[comp].empty? }
47
+ chosen.each { |comp| @_components[comp] = resolve_valid_choice(comp) }
48
+ chosen.select! do |comp|
49
+ choice = @_components[comp]
48
50
  existing = fetch_component_choice(comp)
49
51
  ask = existing != 'none' && existing != choice
50
- next if ask && !yes?("Switch #{comp} to '#{choice}' from '#{existing}' ?[yes/no]:")
52
+ !ask || yes?("Switch #{comp} to '#{choice}' from '#{existing}' ?[yes/no]:")
53
+ end
51
54
 
52
- @project_name = fetch_component_choice(:namespace)
53
- if comp.to_s == 'test' && !already_exists?(@app_name, @project_name)
54
- say "#{@project_name}::#{@app_name} does not exist."
55
- say 'Please, change app name.'
56
- next
57
- end
55
+ @project_name = fetch_component_choice(:namespace)
56
+ if chosen.include?(:test) && !already_exists?(@app_name, @project_name)
57
+ say "#{@project_name}::#{@app_name} does not exist."
58
+ say 'Please, change app name.'
59
+ exit 1
60
+ end
61
+ resolve_valid_adapter if chosen.include?(:orm) && SQL_ADAPTER_ORMS.include?(@_components[:orm])
58
62
 
63
+ chosen.each do |comp|
64
+ choice = @_components[comp]
59
65
  execute_component_setup(comp, choice)
60
66
  store_component_choice(comp, choice)
67
+ store_component_choice(:adapter, resolve_valid_adapter) if comp.to_s == 'orm' && SQL_ADAPTER_ORMS.include?(choice) && !adapter_unknown?
61
68
  if comp.to_s == 'orm' && choice.to_s != 'none'
62
69
  inject_into_file destination_root('Rakefile'), "PadrinoTasks.use(:database)\n", before: 'PadrinoTasks.init'
63
70
  inject_into_file destination_root('Rakefile'), "PadrinoTasks.use(#{choice.to_sym.inspect})\n", before: 'PadrinoTasks.init'
64
71
  end
65
72
  end
66
73
  else
67
- say 'You are not at the root of a Padrino application! (config/boot.rb not found)'
74
+ exit_outside_app_root!
68
75
  end
69
76
  end
70
77
  end
@@ -28,8 +28,8 @@ module Padrino
28
28
  return if migration_exist?(filename)
29
29
  model_name = name.to_s.pluralize
30
30
  field_tuples = columns.map { |value| value.split(':') }
31
- field_tuples.map! { |field, kind| kind =~ /datetime/i ? [field, 'DateTime'] : [field, kind] }
32
- column_declarations = field_tuples.map(&options[:column_format]).join("\n ")
31
+ field_tuples.map! { |field, kind, *modifiers| [field, kind =~ /datetime/i ? 'DateTime' : kind, *modifiers] }
32
+ column_declarations = field_tuples.flat_map(&options[:column_format]).join("\n ")
33
33
  contents = options[:base].dup.gsub(/\s{4}!UP!\n/m, options[:up]).gsub(/!DOWN!\n/m, options[:down])
34
34
  contents = contents.gsub(/!NAME!/, model_name.underscore.camelize).gsub(/!TABLE!/, model_name.underscore)
35
35
  contents = contents.gsub(/!FILENAME!/, filename.underscore).gsub(/!FILECLASS!/, filename.underscore.camelize)
@@ -70,9 +70,9 @@ module Padrino
70
70
  migration_scan = filename.underscore.camelize.scan(/(Add|Remove).*?(?:(?:To|From).+?)*(?:To|From)((?:To|From)?.*?)$/).flatten
71
71
  direction, table_name = migration_scan[0].downcase, migration_scan[1].downcase.pluralize if migration_scan.any?
72
72
  tuples = direction ? columns.map { |value| value.split(':') } : []
73
- tuples.map! { |field, kind| kind =~ /datetime/i ? [field, 'DateTime'] : [field, kind] }
74
- add_columns = tuples.map(&options[:add]).join("\n ")
75
- remove_columns = tuples.map(&options[:remove]).join("\n ")
73
+ tuples.map! { |field, kind, *modifiers| [field, kind =~ /datetime/i ? 'DateTime' : kind, *modifiers] }
74
+ add_columns = tuples.flat_map(&options[:add]).join("\n ")
75
+ remove_columns = tuples.flat_map(&options[:remove]).join("\n ")
76
76
  forward_text = change_format.gsub(/!TABLE!/, table_name).gsub(/!COLUMNS!/, add_columns) if tuples.any?
77
77
  back_text = change_format.gsub(/!TABLE!/, table_name).gsub(/!COLUMNS!/, remove_columns) if tuples.any?
78
78
  contents = options[:base].dup.gsub(/\s{4}!UP!\n/m, (direction == 'add' ? forward_text.to_s : back_text.to_s))
@@ -145,7 +145,8 @@ module Padrino
145
145
  #
146
146
  def insert_test_suite_setup(suite_text, options = {})
147
147
  options = { path: 'test/test_config.rb' }.update(options)
148
- create_file(options[:path], suite_text.gsub(/CLASS_NAME/, "#{@project_name}::#{@app_name}"))
148
+ # --app points the setup at another app, so that run rewrites it.
149
+ create_file(options[:path], suite_text.gsub(/CLASS_NAME/, "#{@project_name}::#{@app_name}"), skip: reapplying_component?(:test) && !self.options[:app])
149
150
  end
150
151
 
151
152
  ##