rails-metro 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +17 -0
- data/LICENSE +21 -0
- data/README.md +267 -0
- data/exe/metro +3 -0
- data/lib/rails/metro/cli.rb +162 -0
- data/lib/rails/metro/config.rb +40 -0
- data/lib/rails/metro/feature_pack.rb +59 -0
- data/lib/rails/metro/feature_registry.rb +147 -0
- data/lib/rails/metro/packs/aasm_pack.rb +29 -0
- data/lib/rails/metro/packs/ab_testing_pack.rb +39 -0
- data/lib/rails/metro/packs/action_text_pack.rb +29 -0
- data/lib/rails/metro/packs/activeadmin_pack.rb +32 -0
- data/lib/rails/metro/packs/activity_feed_pack.rb +31 -0
- data/lib/rails/metro/packs/acts_as_list_pack.rb +28 -0
- data/lib/rails/metro/packs/acts_as_votable_pack.rb +32 -0
- data/lib/rails/metro/packs/administrate_pack.rb +31 -0
- data/lib/rails/metro/packs/after_party_pack.rb +31 -0
- data/lib/rails/metro/packs/ahoy_pack.rb +31 -0
- data/lib/rails/metro/packs/amplitude_pack.rb +45 -0
- data/lib/rails/metro/packs/annotate_pack.rb +30 -0
- data/lib/rails/metro/packs/api_cors_pack.rb +41 -0
- data/lib/rails/metro/packs/api_docs_pack.rb +34 -0
- data/lib/rails/metro/packs/api_guard_pack.rb +31 -0
- data/lib/rails/metro/packs/api_serialization_pack.rb +38 -0
- data/lib/rails/metro/packs/app_linting_pack.rb +35 -0
- data/lib/rails/metro/packs/audit_trail_pack.rb +31 -0
- data/lib/rails/metro/packs/authentication_pack.rb +31 -0
- data/lib/rails/metro/packs/authorization_pack.rb +31 -0
- data/lib/rails/metro/packs/avo_pack.rb +32 -0
- data/lib/rails/metro/packs/aws_ses_pack.rb +45 -0
- data/lib/rails/metro/packs/background_jobs_pack.rb +34 -0
- data/lib/rails/metro/packs/blazer_pack.rb +62 -0
- data/lib/rails/metro/packs/breadcrumbs_pack.rb +31 -0
- data/lib/rails/metro/packs/caching_pack.rb +30 -0
- data/lib/rails/metro/packs/charting_pack.rb +67 -0
- data/lib/rails/metro/packs/circuit_breaker_pack.rb +34 -0
- data/lib/rails/metro/packs/clicky_pack.rb +37 -0
- data/lib/rails/metro/packs/cloudinary_pack.rb +42 -0
- data/lib/rails/metro/packs/components_pack.rb +30 -0
- data/lib/rails/metro/packs/counter_culture_pack.rb +29 -0
- data/lib/rails/metro/packs/data_migrate_pack.rb +29 -0
- data/lib/rails/metro/packs/datadog_pack.rb +41 -0
- data/lib/rails/metro/packs/deployment_pack.rb +30 -0
- data/lib/rails/metro/packs/devise_pack.rb +33 -0
- data/lib/rails/metro/packs/doorkeeper_pack.rb +32 -0
- data/lib/rails/metro/packs/dotenv_pack.rb +36 -0
- data/lib/rails/metro/packs/elasticsearch_pack.rb +31 -0
- data/lib/rails/metro/packs/encryption_pack.rb +37 -0
- data/lib/rails/metro/packs/error_tracking_pack.rb +40 -0
- data/lib/rails/metro/packs/event_sourcing_pack.rb +31 -0
- data/lib/rails/metro/packs/faraday_pack.rb +42 -0
- data/lib/rails/metro/packs/fathom_pack.rb +36 -0
- data/lib/rails/metro/packs/feature_flags_pack.rb +34 -0
- data/lib/rails/metro/packs/friendly_id_pack.rb +31 -0
- data/lib/rails/metro/packs/geocoder_pack.rb +38 -0
- data/lib/rails/metro/packs/good_job_pack.rb +34 -0
- data/lib/rails/metro/packs/google_analytics_pack.rb +42 -0
- data/lib/rails/metro/packs/google_tag_manager_pack.rb +51 -0
- data/lib/rails/metro/packs/graphql_pack.rb +31 -0
- data/lib/rails/metro/packs/health_check_pack.rb +34 -0
- data/lib/rails/metro/packs/heap_pack.rb +39 -0
- data/lib/rails/metro/packs/honeybadger_pack.rb +37 -0
- data/lib/rails/metro/packs/hotwire_livereload_pack.rb +28 -0
- data/lib/rails/metro/packs/icons_pack.rb +28 -0
- data/lib/rails/metro/packs/invisible_captcha_pack.rb +35 -0
- data/lib/rails/metro/packs/kredis_pack.rb +31 -0
- data/lib/rails/metro/packs/lemon_squeezy_pack.rb +36 -0
- data/lib/rails/metro/packs/letter_opener_pack.rb +31 -0
- data/lib/rails/metro/packs/letter_opener_web_pack.rb +32 -0
- data/lib/rails/metro/packs/logging_pack.rb +36 -0
- data/lib/rails/metro/packs/madmin_pack.rb +32 -0
- data/lib/rails/metro/packs/mailgun_pack.rb +44 -0
- data/lib/rails/metro/packs/maintenance_mode_pack.rb +30 -0
- data/lib/rails/metro/packs/maintenance_tasks_pack.rb +32 -0
- data/lib/rails/metro/packs/markdown_pack.rb +48 -0
- data/lib/rails/metro/packs/matomo_pack.rb +48 -0
- data/lib/rails/metro/packs/meilisearch_pack.rb +37 -0
- data/lib/rails/metro/packs/mixpanel_pack.rb +47 -0
- data/lib/rails/metro/packs/mobility_pack.rb +32 -0
- data/lib/rails/metro/packs/mollie_pack.rb +36 -0
- data/lib/rails/metro/packs/multitenancy_pack.rb +35 -0
- data/lib/rails/metro/packs/newrelic_pack.rb +52 -0
- data/lib/rails/metro/packs/notifications_pack.rb +31 -0
- data/lib/rails/metro/packs/omniauth_pack.rb +49 -0
- data/lib/rails/metro/packs/paddle_pack.rb +37 -0
- data/lib/rails/metro/packs/pagination_pack.rb +44 -0
- data/lib/rails/metro/packs/passwordless_pack.rb +32 -0
- data/lib/rails/metro/packs/payments_pack.rb +35 -0
- data/lib/rails/metro/packs/paypal_pack.rb +35 -0
- data/lib/rails/metro/packs/pdf_pack.rb +45 -0
- data/lib/rails/metro/packs/performance_pack.rb +37 -0
- data/lib/rails/metro/packs/pghero_pack.rb +31 -0
- data/lib/rails/metro/packs/plausible_pack.rb +36 -0
- data/lib/rails/metro/packs/posthog_pack.rb +40 -0
- data/lib/rails/metro/packs/postmark_pack.rb +39 -0
- data/lib/rails/metro/packs/pretender_pack.rb +33 -0
- data/lib/rails/metro/packs/profiling_pack.rb +30 -0
- data/lib/rails/metro/packs/pundit_pack.rb +50 -0
- data/lib/rails/metro/packs/qr_code_pack.rb +37 -0
- data/lib/rails/metro/packs/r2_storage_pack.rb +48 -0
- data/lib/rails/metro/packs/rack_timeout_pack.rb +34 -0
- data/lib/rails/metro/packs/rails_i18n_pack.rb +31 -0
- data/lib/rails/metro/packs/ransack_pack.rb +29 -0
- data/lib/rails/metro/packs/rate_limiting_pack.rb +39 -0
- data/lib/rails/metro/packs/recaptcha_pack.rb +38 -0
- data/lib/rails/metro/packs/resend_pack.rb +37 -0
- data/lib/rails/metro/packs/revenuecat_pack.rb +36 -0
- data/lib/rails/metro/packs/rolify_pack.rb +32 -0
- data/lib/rails/metro/packs/rollbar_pack.rb +38 -0
- data/lib/rails/metro/packs/s3_storage_pack.rb +45 -0
- data/lib/rails/metro/packs/scenic_pack.rb +28 -0
- data/lib/rails/metro/packs/scheduling_pack.rb +46 -0
- data/lib/rails/metro/packs/search_pack.rb +32 -0
- data/lib/rails/metro/packs/security_pack.rb +30 -0
- data/lib/rails/metro/packs/sendgrid_pack.rb +44 -0
- data/lib/rails/metro/packs/sent_dm_pack.rb +36 -0
- data/lib/rails/metro/packs/seo_pack.rb +40 -0
- data/lib/rails/metro/packs/shoulda_matchers_pack.rb +37 -0
- data/lib/rails/metro/packs/sidekiq_pack.rb +42 -0
- data/lib/rails/metro/packs/simple_calendar_pack.rb +29 -0
- data/lib/rails/metro/packs/simple_form_pack.rb +30 -0
- data/lib/rails/metro/packs/simplecov_pack.rb +38 -0
- data/lib/rails/metro/packs/slack_notifier_pack.rb +36 -0
- data/lib/rails/metro/packs/soft_deletes_pack.rb +30 -0
- data/lib/rails/metro/packs/solidus_pack.rb +31 -0
- data/lib/rails/metro/packs/spreadsheets_pack.rb +29 -0
- data/lib/rails/metro/packs/statcounter_pack.rb +41 -0
- data/lib/rails/metro/packs/stimulus_components_pack.rb +33 -0
- data/lib/rails/metro/packs/storage_validations_pack.rb +28 -0
- data/lib/rails/metro/packs/strong_migrations_pack.rb +30 -0
- data/lib/rails/metro/packs/tagging_pack.rb +31 -0
- data/lib/rails/metro/packs/test_mocking_pack.rb +41 -0
- data/lib/rails/metro/packs/testing_pack.rb +39 -0
- data/lib/rails/metro/packs/twilio_pack.rb +38 -0
- data/lib/rails/metro/packs/vite_rails_pack.rb +31 -0
- data/lib/rails/metro/packs/web_push_pack.rb +39 -0
- data/lib/rails/metro/packs/webhooks_pack.rb +45 -0
- data/lib/rails/metro/packs/websockets_pack.rb +29 -0
- data/lib/rails/metro/template_compiler.rb +85 -0
- data/lib/rails/metro/tui/app.rb +119 -0
- data/lib/rails/metro/tui/steps/app_name_step.rb +57 -0
- data/lib/rails/metro/tui/steps/database_step.rb +53 -0
- data/lib/rails/metro/tui/steps/pack_step.rb +248 -0
- data/lib/rails/metro/tui/steps/review_step.rb +94 -0
- data/lib/rails/metro/tui/styles.rb +47 -0
- data/lib/rails/metro/tui.rb +37 -0
- data/lib/rails/metro/version.rb +5 -0
- data/lib/rails/metro.rb +9 -0
- metadata +206 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Metro
|
|
3
|
+
module Packs
|
|
4
|
+
class TestMockingPack < FeaturePack
|
|
5
|
+
pack_name "test_mocking"
|
|
6
|
+
description "VCR + WebMock for recording and replaying HTTP interactions in tests"
|
|
7
|
+
category "testing"
|
|
8
|
+
|
|
9
|
+
def gems
|
|
10
|
+
[
|
|
11
|
+
{name: "vcr", group: :test},
|
|
12
|
+
{name: "webmock", group: :test}
|
|
13
|
+
]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def template_lines
|
|
17
|
+
[
|
|
18
|
+
'create_file "test/support/vcr.rb", <<~RUBY',
|
|
19
|
+
' require "vcr"',
|
|
20
|
+
"",
|
|
21
|
+
" VCR.configure do |config|",
|
|
22
|
+
' config.cassette_library_dir = "test/cassettes"',
|
|
23
|
+
" config.hook_into :webmock",
|
|
24
|
+
" config.ignore_localhost = true",
|
|
25
|
+
" config.default_cassette_options = { record: :once }",
|
|
26
|
+
" end",
|
|
27
|
+
"RUBY"
|
|
28
|
+
]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def post_install_notes
|
|
32
|
+
[
|
|
33
|
+
"VCR: Wrap HTTP tests with `VCR.use_cassette(\"name\") { }` to record/replay",
|
|
34
|
+
"VCR: Cassettes stored in test/cassettes/",
|
|
35
|
+
"WebMock: All external HTTP is blocked in tests by default -- use VCR or stub explicitly"
|
|
36
|
+
]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Metro
|
|
3
|
+
module Packs
|
|
4
|
+
class TestingPack < FeaturePack
|
|
5
|
+
pack_name "testing"
|
|
6
|
+
description "FactoryBot + Faker + Capybara for testing"
|
|
7
|
+
category "testing"
|
|
8
|
+
|
|
9
|
+
def gems
|
|
10
|
+
[
|
|
11
|
+
{name: "factory_bot_rails", group: :test},
|
|
12
|
+
{name: "faker", group: :test},
|
|
13
|
+
{name: "capybara", group: :test},
|
|
14
|
+
{name: "selenium-webdriver", group: :test}
|
|
15
|
+
]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def template_lines
|
|
19
|
+
[
|
|
20
|
+
'create_file "test/support/factory_bot.rb", <<~RUBY',
|
|
21
|
+
" FactoryBot::SyntaxMethods",
|
|
22
|
+
"",
|
|
23
|
+
" class ActiveSupport::TestCase",
|
|
24
|
+
" include FactoryBot::Syntax::Methods",
|
|
25
|
+
" end",
|
|
26
|
+
"RUBY"
|
|
27
|
+
]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def post_install_notes
|
|
31
|
+
[
|
|
32
|
+
"Testing: Generate factories with `bin/rails g factory_bot:model User`",
|
|
33
|
+
"Testing: Use `build(:user)`, `create(:user)` in tests"
|
|
34
|
+
]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Metro
|
|
3
|
+
module Packs
|
|
4
|
+
class TwilioPack < FeaturePack
|
|
5
|
+
pack_name "twilio"
|
|
6
|
+
description "Twilio for SMS, voice, and WhatsApp messaging"
|
|
7
|
+
category "notifications"
|
|
8
|
+
|
|
9
|
+
def gems
|
|
10
|
+
[
|
|
11
|
+
{name: "twilio-ruby"}
|
|
12
|
+
]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def template_lines
|
|
16
|
+
[
|
|
17
|
+
'create_file "config/initializers/twilio.rb", <<~RUBY',
|
|
18
|
+
" TWILIO_CLIENT = Twilio::REST::Client.new(",
|
|
19
|
+
' Rails.application.credentials.dig(:twilio, :account_sid) || ENV["TWILIO_ACCOUNT_SID"],',
|
|
20
|
+
' Rails.application.credentials.dig(:twilio, :auth_token) || ENV["TWILIO_AUTH_TOKEN"]',
|
|
21
|
+
" )",
|
|
22
|
+
"RUBY"
|
|
23
|
+
]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def post_install_notes
|
|
27
|
+
[
|
|
28
|
+
"Twilio: Add your credentials with `bin/rails credentials:edit`:",
|
|
29
|
+
" twilio:",
|
|
30
|
+
" account_sid: your_account_sid",
|
|
31
|
+
" auth_token: your_auth_token",
|
|
32
|
+
" phone_number: \"+1234567890\""
|
|
33
|
+
]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Metro
|
|
3
|
+
module Packs
|
|
4
|
+
class ViteRailsPack < FeaturePack
|
|
5
|
+
pack_name "vite_rails"
|
|
6
|
+
description "Vite Rails for fast frontend bundling with HMR"
|
|
7
|
+
category "ui"
|
|
8
|
+
|
|
9
|
+
def gems
|
|
10
|
+
[
|
|
11
|
+
{name: "vite_rails"}
|
|
12
|
+
]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def template_lines
|
|
16
|
+
[
|
|
17
|
+
'run "bundle exec vite install"'
|
|
18
|
+
]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def post_install_notes
|
|
22
|
+
[
|
|
23
|
+
"Vite: Run `bin/vite dev` alongside `bin/rails s` for development",
|
|
24
|
+
"Vite: Hot Module Replacement (HMR) for instant updates",
|
|
25
|
+
"Vite: Build for production with `bin/vite build`"
|
|
26
|
+
]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Metro
|
|
3
|
+
module Packs
|
|
4
|
+
class WebPushPack < FeaturePack
|
|
5
|
+
pack_name "web_push"
|
|
6
|
+
description "Web Push for browser push notifications"
|
|
7
|
+
category "notifications"
|
|
8
|
+
|
|
9
|
+
def gems
|
|
10
|
+
[
|
|
11
|
+
{name: "web-push"}
|
|
12
|
+
]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def template_lines
|
|
16
|
+
[
|
|
17
|
+
'create_file "config/initializers/web_push.rb", <<~RUBY',
|
|
18
|
+
" WebPush.configure do |config|",
|
|
19
|
+
' config.vapid_public_key = Rails.application.credentials.dig(:vapid, :public_key) || ENV["VAPID_PUBLIC_KEY"]',
|
|
20
|
+
' config.vapid_private_key = Rails.application.credentials.dig(:vapid, :private_key) || ENV["VAPID_PRIVATE_KEY"]',
|
|
21
|
+
" end",
|
|
22
|
+
"RUBY"
|
|
23
|
+
]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def post_install_notes
|
|
27
|
+
[
|
|
28
|
+
"Web Push: Generate VAPID keys with `WebPush.generate_key`",
|
|
29
|
+
"Web Push: Add keys with `bin/rails credentials:edit`:",
|
|
30
|
+
" vapid:",
|
|
31
|
+
" public_key: your_public_key",
|
|
32
|
+
" private_key: your_private_key",
|
|
33
|
+
"Web Push: Register a service worker in your app for push subscriptions"
|
|
34
|
+
]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Metro
|
|
3
|
+
module Packs
|
|
4
|
+
class WebhooksPack < FeaturePack
|
|
5
|
+
pack_name "webhooks"
|
|
6
|
+
description "Incoming and outgoing webhook handling"
|
|
7
|
+
category "api"
|
|
8
|
+
|
|
9
|
+
def gems
|
|
10
|
+
[
|
|
11
|
+
{name: "webhook_system"}
|
|
12
|
+
]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def template_lines
|
|
16
|
+
[
|
|
17
|
+
'create_file "app/controllers/webhooks_controller.rb", <<~RUBY',
|
|
18
|
+
" class WebhooksController < ActionController::API",
|
|
19
|
+
" skip_before_action :verify_authenticity_token",
|
|
20
|
+
"",
|
|
21
|
+
" def receive",
|
|
22
|
+
" payload = JSON.parse(request.body.read)",
|
|
23
|
+
' signature = request.headers["X-Webhook-Signature"]',
|
|
24
|
+
"",
|
|
25
|
+
" # Verify and process webhook",
|
|
26
|
+
' Rails.logger.info "[Webhook] Received: \#{payload}"',
|
|
27
|
+
" head :ok",
|
|
28
|
+
" end",
|
|
29
|
+
" end",
|
|
30
|
+
"RUBY",
|
|
31
|
+
"",
|
|
32
|
+
'route "post \\"/webhooks/:provider\\", to: \\"webhooks#receive\\""'
|
|
33
|
+
]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def post_install_notes
|
|
37
|
+
[
|
|
38
|
+
"Webhooks: Incoming endpoint at POST /webhooks/:provider",
|
|
39
|
+
"Webhooks: Add signature verification per provider in WebhooksController"
|
|
40
|
+
]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Metro
|
|
3
|
+
module Packs
|
|
4
|
+
class WebsocketsPack < FeaturePack
|
|
5
|
+
pack_name "websockets"
|
|
6
|
+
description "Solid Cable (Rails 8 default, database-backed ActionCable)"
|
|
7
|
+
category "core"
|
|
8
|
+
|
|
9
|
+
def gems
|
|
10
|
+
[
|
|
11
|
+
{name: "solid_cable"}
|
|
12
|
+
]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def template_lines
|
|
16
|
+
[
|
|
17
|
+
'rails_command "solid_cable:install"'
|
|
18
|
+
]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def post_install_notes
|
|
22
|
+
[
|
|
23
|
+
"WebSockets: Solid Cable is configured as the ActionCable adapter"
|
|
24
|
+
]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require "tempfile"
|
|
2
|
+
|
|
3
|
+
module Rails
|
|
4
|
+
module Metro
|
|
5
|
+
class TemplateCompiler
|
|
6
|
+
attr_reader :config, :packs
|
|
7
|
+
|
|
8
|
+
def initialize(config:, packs:)
|
|
9
|
+
@config = config
|
|
10
|
+
@packs = packs
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def compile
|
|
14
|
+
lines = []
|
|
15
|
+
lines << header
|
|
16
|
+
lines << ""
|
|
17
|
+
lines << gem_declarations
|
|
18
|
+
lines << ""
|
|
19
|
+
lines << after_bundle_block
|
|
20
|
+
lines.flatten.join("\n")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def compile_to_file
|
|
24
|
+
content = compile
|
|
25
|
+
tmpfile = Tempfile.new(["rails-metro-template", ".rb"])
|
|
26
|
+
tmpfile.write(content)
|
|
27
|
+
tmpfile.close
|
|
28
|
+
tmpfile.path
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def post_install_notes
|
|
32
|
+
packs.flat_map { |p| p.new.post_install_notes }.compact
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def header
|
|
38
|
+
pack_names = packs.map(&:pack_name).join(", ")
|
|
39
|
+
[
|
|
40
|
+
"# Generated by rails-metro v#{VERSION}",
|
|
41
|
+
"# Packs: #{pack_names}",
|
|
42
|
+
"#",
|
|
43
|
+
"# Apply with: rails new APP_NAME -m this_template.rb",
|
|
44
|
+
"# Or: bin/rails app:template LOCATION=this_template.rb"
|
|
45
|
+
]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def gem_declarations
|
|
49
|
+
all_gems = packs.flat_map { |p| p.new.gems }.uniq { |g| g[:name] }
|
|
50
|
+
return [] if all_gems.empty?
|
|
51
|
+
|
|
52
|
+
grouped = all_gems.group_by { |g| g[:group] }
|
|
53
|
+
lines = []
|
|
54
|
+
|
|
55
|
+
ungrouped = grouped.delete(nil) || []
|
|
56
|
+
ungrouped.each do |g|
|
|
57
|
+
version = g[:version] ? ", \"#{g[:version]}\"" : ""
|
|
58
|
+
lines << "gem \"#{g[:name]}\"#{version}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
grouped.each do |group, gems|
|
|
62
|
+
lines << ""
|
|
63
|
+
lines << "gem_group :#{group} do"
|
|
64
|
+
gems.each do |g|
|
|
65
|
+
version = g[:version] ? ", \"#{g[:version]}\"" : ""
|
|
66
|
+
lines << " gem \"#{g[:name]}\"#{version}"
|
|
67
|
+
end
|
|
68
|
+
lines << "end"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
lines
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def after_bundle_block
|
|
75
|
+
template_lines = packs.flat_map { |p| p.new.template_lines }
|
|
76
|
+
return [] if template_lines.empty?
|
|
77
|
+
|
|
78
|
+
lines = ["", "after_bundle do"]
|
|
79
|
+
template_lines.each { |line| lines << " #{line}" }
|
|
80
|
+
lines << "end"
|
|
81
|
+
lines
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Metro
|
|
3
|
+
module Tui
|
|
4
|
+
class App
|
|
5
|
+
include Bubbletea::Model
|
|
6
|
+
|
|
7
|
+
STEP_ORDER = %i[app_name database packs review].freeze
|
|
8
|
+
|
|
9
|
+
attr_reader :step_index, :config, :steps
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@config = Config.new
|
|
13
|
+
@registry = FeatureRegistry.default
|
|
14
|
+
@step_index = 0
|
|
15
|
+
@steps = {
|
|
16
|
+
app_name: Steps::AppNameStep.new,
|
|
17
|
+
database: Steps::DatabaseStep.new,
|
|
18
|
+
packs: Steps::PackStep.new(registry: @registry),
|
|
19
|
+
review: nil
|
|
20
|
+
}
|
|
21
|
+
@quitting = false
|
|
22
|
+
@final_action = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def init
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def update(msg)
|
|
30
|
+
case msg
|
|
31
|
+
when Bubbletea::KeyMessage
|
|
32
|
+
if msg.esc? || (msg.ctrl? && msg.char == "c")
|
|
33
|
+
@quitting = true
|
|
34
|
+
return self, Bubbletea::QuitCommand.new
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
step = current_step
|
|
39
|
+
step, action = step.update(msg)
|
|
40
|
+
@steps[current_step_name] = step
|
|
41
|
+
|
|
42
|
+
case action
|
|
43
|
+
when :next
|
|
44
|
+
advance_step
|
|
45
|
+
when :back
|
|
46
|
+
go_back
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
[self, nil]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def view
|
|
53
|
+
return "" if @quitting
|
|
54
|
+
|
|
55
|
+
lines = []
|
|
56
|
+
lines << header
|
|
57
|
+
lines << ""
|
|
58
|
+
lines << current_step.view
|
|
59
|
+
lines.join("\n")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def current_step_name
|
|
63
|
+
STEP_ORDER[@step_index]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def current_step
|
|
67
|
+
@steps[current_step_name]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
attr_reader :final_action
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def header
|
|
75
|
+
progress = STEP_ORDER.each_with_index.map { |name, i|
|
|
76
|
+
label = name.to_s.tr("_", " ").capitalize
|
|
77
|
+
if i < @step_index
|
|
78
|
+
Styles.success.render("✓ #{label}")
|
|
79
|
+
elsif i == @step_index
|
|
80
|
+
Styles.selected.render("● #{label}")
|
|
81
|
+
else
|
|
82
|
+
Styles.dimmed.render("○ #{label}")
|
|
83
|
+
end
|
|
84
|
+
}.join(" ")
|
|
85
|
+
" #{progress}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def advance_step
|
|
89
|
+
sync_config
|
|
90
|
+
|
|
91
|
+
if @step_index >= STEP_ORDER.length - 1
|
|
92
|
+
@final_action = @steps[:review]&.action
|
|
93
|
+
@quitting = true
|
|
94
|
+
return self, Bubbletea::QuitCommand.new
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
@step_index += 1
|
|
98
|
+
|
|
99
|
+
if current_step_name == :review
|
|
100
|
+
@steps[:review] = Steps::ReviewStep.new(config: @config, registry: @registry)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
[self, nil]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def go_back
|
|
107
|
+
@step_index = [@step_index - 1, 0].max
|
|
108
|
+
[self, nil]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def sync_config
|
|
112
|
+
@config.app_name = @steps[:app_name].app_name
|
|
113
|
+
@config.database = @steps[:database].selected
|
|
114
|
+
@config.selected_packs = @steps[:packs].selected_packs.to_a.sort
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Metro
|
|
3
|
+
module Tui
|
|
4
|
+
module Steps
|
|
5
|
+
class AppNameStep
|
|
6
|
+
attr_reader :app_name, :error
|
|
7
|
+
|
|
8
|
+
def initialize(app_name: "")
|
|
9
|
+
@app_name = app_name
|
|
10
|
+
@error = nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def update(msg)
|
|
14
|
+
case msg
|
|
15
|
+
when Bubbletea::KeyMessage
|
|
16
|
+
if msg.enter?
|
|
17
|
+
if @app_name.strip.empty?
|
|
18
|
+
@error = "App name cannot be empty"
|
|
19
|
+
elsif @app_name.match?(/[^a-zA-Z0-9_-]/)
|
|
20
|
+
@error = "App name can only contain letters, numbers, hyphens, and underscores"
|
|
21
|
+
else
|
|
22
|
+
@error = nil
|
|
23
|
+
return self, :next
|
|
24
|
+
end
|
|
25
|
+
elsif msg.backspace?
|
|
26
|
+
@app_name = @app_name[0...-1]
|
|
27
|
+
@error = nil
|
|
28
|
+
elsif msg.runes?
|
|
29
|
+
@app_name += msg.to_s
|
|
30
|
+
@error = nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
[self, nil]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def view
|
|
37
|
+
lines = []
|
|
38
|
+
lines << Styles.title.render(" Create a new Rails app")
|
|
39
|
+
lines << ""
|
|
40
|
+
lines << " App name: #{@app_name}█"
|
|
41
|
+
lines << ""
|
|
42
|
+
if @error
|
|
43
|
+
lines << " #{Styles.error_style.render(@error)}"
|
|
44
|
+
lines << ""
|
|
45
|
+
end
|
|
46
|
+
lines << Styles.help.render(" Enter to continue • Ctrl+C to quit")
|
|
47
|
+
lines.join("\n")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def complete?
|
|
51
|
+
@app_name.strip.length > 0 && @error.nil?
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Metro
|
|
3
|
+
module Tui
|
|
4
|
+
module Steps
|
|
5
|
+
class DatabaseStep
|
|
6
|
+
DATABASES = %w[sqlite3 postgresql mysql2].freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :cursor, :selected
|
|
9
|
+
|
|
10
|
+
def initialize(selected: "sqlite3")
|
|
11
|
+
@cursor = DATABASES.index(selected) || 0
|
|
12
|
+
@selected = selected
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def update(msg)
|
|
16
|
+
case msg
|
|
17
|
+
when Bubbletea::KeyMessage
|
|
18
|
+
if msg.enter?
|
|
19
|
+
@selected = DATABASES[@cursor]
|
|
20
|
+
return self, :next
|
|
21
|
+
elsif msg.up? || msg.char == "k"
|
|
22
|
+
@cursor = (@cursor - 1) % DATABASES.length
|
|
23
|
+
elsif msg.down? || msg.char == "j"
|
|
24
|
+
@cursor = (@cursor + 1) % DATABASES.length
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
[self, nil]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def view
|
|
31
|
+
lines = []
|
|
32
|
+
lines << Styles.title.render(" Select database")
|
|
33
|
+
lines << ""
|
|
34
|
+
DATABASES.each_with_index do |db, i|
|
|
35
|
+
lines << if i == @cursor
|
|
36
|
+
Styles.selected.render(" > #{db}")
|
|
37
|
+
else
|
|
38
|
+
Styles.dimmed.render(" #{db}")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
lines << ""
|
|
42
|
+
lines << Styles.help.render(" j/k or arrows to move • Enter to select • Ctrl+C to quit")
|
|
43
|
+
lines.join("\n")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def complete?
|
|
47
|
+
DATABASES.include?(@selected)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|