panacea-rails 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.
data/exe/panacea ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ lib = File.expand_path("../lib", __dir__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+
7
+ require "panacea/rails"
8
+
9
+ Panacea::Rails.init
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "slop"
4
+ require "panacea/rails/version"
5
+ require "panacea/rails/runner"
6
+
7
+ module Panacea # :nodoc:
8
+ ###
9
+ # == Panacea::Rails
10
+ #
11
+ # This module is the access point to the configuration questions and the Panacea::Rails::Generator.
12
+ module Rails
13
+ class << self
14
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/LineLength, Metrics/BlockLength
15
+
16
+ ###
17
+ # This method uses Slop to handle the CLI command and its arguments.
18
+ # It accepts most of the default Rails arguments.
19
+ #
20
+ # If App's name is not passed as an argument we show the command help.
21
+ #
22
+ # When the App's name is passed, we start the Panacea::Rails::Runner
23
+ def init
24
+ opts = Slop.parse do |o|
25
+ o.banner = "usage: panacea your-app-name [options]"
26
+ o.separator ""
27
+ o.string "-d", "--database", "# Options (mysql/postgresql/sqlite3/oracle/frontbase/im_db/sqlserver/jdbcmysql/jdbcsqlite3/jdbcpostgresql/jdbc)", default: "postgresql"
28
+ o.bool "--skip-namespace", "# Skip namespace (affects only isolated applications)", default: false
29
+ o.bool "--skip-yarn", "# Don't use Yarn for managing JavaScript dependencies", default: false
30
+ o.bool "--skip-git", "# Skip .gitignore file", default: false
31
+ o.bool "--skip-keeps", "# Skip source control .keep files", default: false
32
+ o.bool "--skip-action-mailer", "# Skip Action Mailer files", default: false
33
+ o.bool "--skip-active-record", "# Skip Active Record files", default: false
34
+ o.bool "--skip-active-storage", "# Skip Active Storage files", default: false
35
+ o.bool "--skip-puma", "# Skip Puma related files", default: false
36
+ o.bool "--skip-action-cable", "# Skip Action Cable files", default: false
37
+ o.bool "--skip-sprockets", "# Skip Sprockets files", default: false
38
+ o.bool "--skip-spring", "# Don't install Spring application preloader", default: false
39
+ o.bool "--skip-listen", "# Don't generate configuration that depends on the listen gem", default: false
40
+ o.bool "--skip-coffee", "# Don't use CoffeeScript", default: false
41
+ o.bool "--skip-javascript", "# Skip JavaScript files", default: false
42
+ o.bool "--skip-turbolinks", "# Skip turbolinks gem", default: false
43
+ o.bool "--skip-test", "# Skip test files", default: false
44
+ o.bool "--skip-system-test", "# Skip system test files", default: false
45
+ o.bool "--skip-bootsnap", "# Skip bootsnap gem", default: false
46
+ o.bool "--dev", "# Setup the application with Gemfile pointing to your Rails checkout", default: false
47
+ o.bool "--edge", "# Setup the application with Gemfile pointing to Rails repository", default: false
48
+ o.string "--rc", "# Path to file containing extra configuration options for rails command", default: nil
49
+ o.bool "--no-rc", "# Skip loading of extra configuration options from .railsrc file", default: false
50
+ o.bool "--api", "# Preconfigure smaller stack for API only apps", default: false
51
+ o.bool "--skip-bundle", "# Don't run bundle install", default: false
52
+ o.separator ""
53
+ o.separator "Runtime options:"
54
+ o.bool "--force", "# Overwrite files that already exist", default: false
55
+ o.bool "--pretend", "# Run but do not make any changes", default: false
56
+ o.bool "--quiet", "# Suppress status output", default: false
57
+ o.bool "--skip", "# Skip files that already exist", default: false
58
+ o.separator ""
59
+ o.separator "Panacea options:"
60
+ o.on "-v", "--version" do
61
+ puts Panacea::Rails::VERSION
62
+ exit
63
+ end
64
+ o.on "-h", "--help" do
65
+ puts o
66
+ exit
67
+ end
68
+ end
69
+
70
+ return puts(opts) if opts.arguments.empty?
71
+
72
+ Runner.call(opts.arguments.first, opts.to_hash)
73
+ end
74
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/LineLength, Metrics/BlockLength
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panacea # :nodoc:
4
+ module Rails # :nodoc:
5
+ ###
6
+ # == Panacea::Rails::ArgumentsParser
7
+ #
8
+ # This module is in charge Parsing Slop Arguments.
9
+ module ArgumentsParser
10
+ ###
11
+ # This method builds an arguments String from the Slop args Hash.
12
+ #
13
+ # The string will be passed to the `rails new` command and it will be also
14
+ # tracked if the end user agrees to share Panacea's usage information
15
+ def parse_arguments(arguments)
16
+ arguments.each_with_object([]) do |arg, parsed_args|
17
+ case arg.last.class.to_s
18
+ when "String"
19
+ parsed_args << "--#{arg.first}=#{arg.last}"
20
+ when "TrueClass"
21
+ parsed_args << "--#{arg.first}"
22
+ end
23
+ end.join(" ")
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty/prompt"
4
+ require "yaml"
5
+ require_relative "stats"
6
+
7
+ module Panacea # :nodoc:
8
+ module Rails # :nodoc:
9
+ ###
10
+ # == Panacea::Rails::Customizer
11
+ #
12
+ # This class is in charge of asking the configuration questions.
13
+ # It saves the answers on the .panacea file.
14
+ class Customizer
15
+ # rubocop:disable Style/FormatStringToken
16
+
17
+ ###
18
+ # ASCII art displayed at the start of the Panacea command
19
+ WELCOME_MESSAGE = <<~MSG
20
+ _____________________________
21
+ | ..... // |
22
+ | _d^^^^^^^^^b_ // |
23
+ | .d'' `` |
24
+ | .p' /|
25
+ | .d' .//| Welcome to Panacea!
26
+ | .d' ----------- - `b. | You are about to boost your fresh
27
+ | :: --------------------- :: | %{app_name} Rails App
28
+ | :: --- P A N A C E A --- :: |
29
+ | :: --------------------- :: | Full documentation here: https://panacea.website
30
+ | `p. ------------------- .q' | Most of the defaults are false or disabled,
31
+ | `p. ----------------- .q' | if you want to enable a feature please answer yes
32
+ | `b. --------------- .d' |
33
+ | `q.. -------- ..p' |
34
+ | ^q........p^ |
35
+ |____________''''_____________|
36
+
37
+ MSG
38
+ # rubocop:enable Style/FormatStringToken
39
+
40
+ ###
41
+ # This method receives the App's name and the passed arguments.
42
+ #
43
+ # It creates a new instance of Panacea::Rails::Customizer class and
44
+ # executes the start instance method.
45
+ def self.start(app_name, passed_args)
46
+ new(app_name, passed_args).start
47
+ end
48
+
49
+ ###
50
+ # A Hash with the questions loaded from config/questions.yaml file
51
+ attr_reader :questions
52
+
53
+ ###
54
+ # A TTY::Prompt instance
55
+ attr_reader :prompt
56
+
57
+ ###
58
+ # A Hash where each question's answer is stored
59
+ attr_reader :answers
60
+
61
+ ###
62
+ # App's name
63
+ attr_reader :app_name
64
+
65
+ ###
66
+ # A String with the arguments passed to the Panacea command
67
+ attr_reader :passed_args
68
+
69
+ ###
70
+ # Panacea::Rails::Customizer initialize method
71
+ #
72
+ # A TTY::Prompt instance is used as default prompt.
73
+ def initialize(app_name, passed_args, prompt: TTY::Prompt.new)
74
+ @app_name = app_name
75
+ @passed_args = passed_args
76
+ @answers = {}
77
+ @prompt = prompt
78
+ end
79
+
80
+ ###
81
+ # This method shows the Welcome message.
82
+ # Then, it ask the questions using the default prompt.
83
+ # It also tracks the answers only if the end user agrees to.
84
+ # At the end, it saves the answers to the .panacea file.
85
+ def start
86
+ welcome_message
87
+ ask_questions
88
+ track_answers
89
+ save_answers
90
+ end
91
+
92
+ private
93
+
94
+ def welcome_message
95
+ message = format(WELCOME_MESSAGE, app_name: app_name.capitalize)
96
+ prompt.say(message, color: :blue)
97
+ end
98
+
99
+ def ask_questions(questions = load_questions)
100
+ questions.each do |key, question|
101
+ answer = ask_question(key, question)
102
+ subquestions = question.dig("subquestions")
103
+
104
+ ask_questions(subquestions) if !subquestions.nil? && answer
105
+ end
106
+ end
107
+
108
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Security/Eval
109
+ def ask_question(key, question)
110
+ title = question.dig("title")
111
+ default = question.dig("default")
112
+ condition = question.dig("condition")
113
+
114
+ unless condition.nil?
115
+ return unless eval(condition)
116
+ end
117
+
118
+ answer = nil
119
+
120
+ case question.dig("type")
121
+ when "boolean"
122
+ answer = prompt.yes?(title) { |q| q.default(default) }
123
+ when "range"
124
+ answer = prompt.ask(title, default: default) { |q| q.in(question.dig("range")) }
125
+ when "text"
126
+ answer = prompt.ask(title, default: default)
127
+ when "select"
128
+ answer = prompt.select(title, question.dig("options"))
129
+ else
130
+ raise StandardError, "Question type not supported."
131
+ end
132
+
133
+ update_answer(key, answer)
134
+ end
135
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Security/Eval
136
+
137
+ def update_answer(key, answer)
138
+ answers[key] = answer
139
+ end
140
+
141
+ def load_questions
142
+ questions_file = File.join(File.expand_path("../../../config", __dir__), "questions.yml")
143
+ @questions = YAML.safe_load(File.read(questions_file))
144
+ end
145
+
146
+ def save_answers
147
+ root_dir = File.expand_path("../../../", __dir__)
148
+ configurations_file = File.join(root_dir, ".panacea")
149
+
150
+ File.open(configurations_file, "w") { |f| f.write(answers.to_yaml) }
151
+ end
152
+
153
+ def track_answers
154
+ share_usage_info = answers.delete("share_usage_info")
155
+ return unless share_usage_info
156
+
157
+ params = answers.dup
158
+ params[:ruby_version] = RUBY_VERSION
159
+ params[:arguments] = passed_args
160
+ Stats.track(params)
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,316 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panacea # :nodoc:
4
+ module Rails # :nodoc:
5
+ # rubocop:disable Metrics/ClassLength
6
+
7
+ ###
8
+ # == Panacea::Rails::Generator
9
+ #
10
+ # This class is in charge of grouping all the actions to be executed via Panacea::Rails::Template
11
+ class Generator
12
+ ###
13
+ # The Rails::Generators::AppGenerator context
14
+ attr_reader :app_generator
15
+
16
+ ###
17
+ # A String with Panacea's installation directory
18
+ attr_reader :root_dir
19
+
20
+ ###
21
+ # The Panacea's Configuration Hash
22
+ attr_reader :config
23
+
24
+ ###
25
+ # This methods receive the context of the Rails::Generators::AppGenerator
26
+ # So it executes all methods against it.
27
+ #
28
+ # It also receives the Panacea's Config Hash and the Panacea's Gem Root dir
29
+ # in order to update Rails::Generators::AppGenerator source paths.
30
+ def initialize(app_generator, panacea_config, root_dir)
31
+ @app_generator = app_generator
32
+ @config = panacea_config
33
+ @app_generator.instance_variable_set :@panacea, @config
34
+ @root_dir = root_dir
35
+ end
36
+
37
+ ###
38
+ # Send any unknown method to the Rails::Generators::AppGenerator context.
39
+ # That context also know Thor actions.
40
+ def method_missing(method_name, *args, &block)
41
+ super unless app_generator.respond_to?(method_name)
42
+ app_generator.send(method_name, *args, &block)
43
+ end
44
+
45
+ ###
46
+ # Whitelist of Rails::Generator::AppGenerator and Thor methods.
47
+ #
48
+ # Add here any new method to be used from Thor / Rails Generator
49
+ def respond_to_missing?(method_name, include_private = false)
50
+ %i[
51
+ after_bundle generate rails_command template
52
+ run git source_paths empty_directory append_to_file
53
+ environment application say inject_into_class
54
+ ].include?(method_name) || super
55
+ end
56
+
57
+ ###
58
+ # All the methods passed via block are executed on the Rails::Generators::AppGenerator
59
+ # after_bundle hook.
60
+ def after_bundle_hook
61
+ after_bundle do
62
+ run "spring stop"
63
+ yield self
64
+ end
65
+ end
66
+
67
+ ###
68
+ # Update Rails::Generators::AppGenerator source paths, making Panacea's Templates under
69
+ # templates/ directory available.
70
+ def update_source_paths
71
+ source_paths.unshift(root_dir)
72
+ end
73
+
74
+ ###
75
+ # Update the Gemfile
76
+ def copy_gemfile
77
+ template "templates/Gemfile.tt", "Gemfile", force: true
78
+ end
79
+
80
+ ###
81
+ # Update the README.md
82
+ def copy_readme
83
+ template "templates/README.tt", "README.md", force: true
84
+ end
85
+
86
+ ###
87
+ # Creates the PANACEA.md file
88
+ def generate_panacea_document
89
+ template "templates/PANACEA.tt", "PANACEA.md"
90
+ end
91
+
92
+ ###
93
+ # Create .rubocop.yml in generated Rails app.
94
+ def setup_rubocop
95
+ template "templates/rubocop.tt", ".rubocop.yml"
96
+ end
97
+
98
+ ###
99
+ # Setup the test suite (rspec or minitest).
100
+ def setup_test_suite
101
+ return unless config.dig("test_suite") == "rspec"
102
+
103
+ generate "rspec:install"
104
+ run "rm -r test"
105
+ end
106
+
107
+ ###
108
+ # Setup Simplecov based on the chosen test suite.
109
+ def setup_simplecov
110
+ path = if config.dig("test_suite") == "minitest"
111
+ "test/support"
112
+ else
113
+ "spec/support"
114
+ end
115
+
116
+ template "templates/simplecov.tt", "#{path}/simplecov.rb"
117
+ append_to_file ".gitignore", "\n# Ignore Coverage files \n/coverage\n"
118
+ end
119
+
120
+ ###
121
+ # Override test helper based on the chosen test suite.
122
+ def override_test_helper
123
+ if config.dig("test_suite") == "minitest"
124
+ template "templates/minitest_test_helper.tt", "test/test_helper.rb", force: true
125
+ else
126
+ template "templates/rspec_test_helper.tt", "spec/rails_helper.rb", force: true
127
+ end
128
+ end
129
+
130
+ ###
131
+ # Setup Headless Chrome Driver based on chosen test suite.
132
+ def override_application_system_test
133
+ return unless config.dig("test_suite") == "minitest"
134
+ template "templates/application_system_test.tt", "test/application_system_test_case.rb", force: true
135
+ end
136
+
137
+ ###
138
+ # Setup OJ gem.
139
+ def setup_oj
140
+ template "templates/oj_initializer.tt", "config/initializers/oj.rb"
141
+ end
142
+
143
+ ###
144
+ # Setup Dotenv gem.
145
+ def setup_dotenv
146
+ template "templates/dotenv.tt", ".env"
147
+ append_to_file ".gitignore", "\n# Ignore .env file \n.env\n"
148
+ end
149
+
150
+ ###
151
+ # Setup chosen Background Job gem.
152
+ def setup_background_job
153
+ background_job = config.dig("background_job")
154
+
155
+ application nil do
156
+ <<~CONFS
157
+ # Default adapter queue
158
+ config.active_job.queue_adapter = :#{background_job}
159
+
160
+ CONFS
161
+ end
162
+
163
+ if background_job == "sidekiq"
164
+ route "mount Sidekiq::Web => '/sidekiq'"
165
+ route "require 'sidekiq/web'"
166
+ elsif background_job == "resque"
167
+ route "mount Resque::Server, at: '/jobs'"
168
+ route "require 'resque/server'"
169
+
170
+ template("templates/Rakefile.tt", "Rakefile", force: true)
171
+ end
172
+ end
173
+
174
+ ###
175
+ # Setup the application's timezone.
176
+ def setup_timezone
177
+ timezone = config.dig("timezone").split("-").first.chomp(" ")
178
+
179
+ application nil do
180
+ <<~CONFS
181
+ # Default timezone
182
+ config.time_zone = "#{timezone}"
183
+
184
+ CONFS
185
+ end
186
+ end
187
+
188
+ ###
189
+ # Setup the application's locale.
190
+ def setup_default_locale
191
+ locale = config.dig("locale").split("- ").last
192
+
193
+ application nil do
194
+ <<~CONFS
195
+ # Default i18n locale
196
+ config.i18n.default_locale = :#{locale}
197
+
198
+ CONFS
199
+ end
200
+
201
+ template "templates/default_locale.tt", "config/locales/#{locale}.yml" if locale != "en"
202
+ end
203
+
204
+ ###
205
+ # Setup Bullet gem
206
+ def setup_bullet
207
+ environment nil, env: "development" do
208
+ <<~CONFS
209
+ # Settings for Bullet gem
210
+ config.after_initialize do
211
+ Bullet.enable = true
212
+ Bullet.alert = true
213
+ Bullet.bullet_logger = true
214
+ Bullet.console = true
215
+ Bullet.rails_logger = true
216
+ Bullet.add_footer = true
217
+ end
218
+
219
+ CONFS
220
+ end
221
+ end
222
+
223
+ ###
224
+ # Setup letter_opener gem.
225
+ def setup_letter_opener
226
+ environment nil, env: "development" do
227
+ <<~CONFS
228
+ # Settings for Letter Opener
229
+ config.action_mailer.delivery_method = :letter_opener
230
+ config.action_mailer.perform_deliveries = true
231
+ config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
232
+
233
+ CONFS
234
+ end
235
+ end
236
+
237
+ ###
238
+ # Setup Devise gem.
239
+ def setup_devise
240
+ model_name = config.dig("devise_model_name").downcase
241
+ plural_model_name = model_name.downcase.pluralize
242
+
243
+ generate "devise:install"
244
+ generate "devise", model_name
245
+ generate "devise:views", plural_model_name if config.dig("devise_override_views")
246
+
247
+ rails_command "db:migrate"
248
+ end
249
+
250
+ ###
251
+ # Setup money_rails gem.
252
+ def setup_money_rails
253
+ generate "money_rails:initializer"
254
+ end
255
+
256
+ ###
257
+ # Setup Kaminari gem.
258
+ def setup_kaminari
259
+ generate "kaminari:config"
260
+ end
261
+
262
+ ###
263
+ # Setup webpacker gem.
264
+ def setup_webpack
265
+ rails_command "webpacker:install"
266
+ rails_command "webpacker:install:#{config.dig('webpack_type')}" if config.dig("webpack_type") != "none"
267
+ end
268
+
269
+ ###
270
+ # Setup foreman gem
271
+ def setup_foreman
272
+ run "gem install foreman" unless system("gem list -i foreman")
273
+
274
+ template("templates/Procfile.tt", "Procfile")
275
+ end
276
+
277
+ ###
278
+ # Setup Pundit gem
279
+ def setup_pundit
280
+ generate "pundit:install"
281
+ inject_into_class "app/controllers/application_controller.rb", "ApplicationController", " include Pundit\n"
282
+ end
283
+
284
+ ###
285
+ # Creates chosen Git hook.
286
+ def setup_githook
287
+ hook_file = ".git/hooks/#{config.dig('githook_type')}"
288
+ template "templates/githook.tt", hook_file
289
+ run "chmod ug+x #{hook_file}"
290
+ end
291
+
292
+ ###
293
+ # This needs to be run before commiting.
294
+ #
295
+ # Fix existing application's style offenses.
296
+ def fix_offenses!
297
+ run "rubocop -a --format=simple"
298
+ end
299
+
300
+ ###
301
+ # Commit only if end users want to.
302
+ def commit!
303
+ git add: "."
304
+ git commit: "-m '#{config.dig('commit_msg')}'"
305
+ end
306
+
307
+ ###
308
+ # Display good bye message.
309
+ def bye_message
310
+ message = "Panacea's work is done, enjoy!"
311
+ say "\n\n\e[34m#{message}\e[0m"
312
+ end
313
+ end
314
+ # rubocop:enable Metrics/ClassLength
315
+ end
316
+ end