Class: CreateRailsApp::Wizard

Inherits:
Object
  • Object
show all
Defined in:
lib/create_rails_app/wizard.rb

Overview

Step-by-step interactive prompt loop for choosing rails new options.

Walks through each supported option in Options::Catalog::ORDER, presenting only the options supported by the detected Rails version. Supports back-navigation via CtrlB+ and smart filtering via SKIP_RULES.

See Also:

  • CLI#run_interactive_wizard!

Constant Summary collapse

BACK =

Sentinel returned by the prompter when the user presses Ctrl+B.

Object.new.tap { |o| o.define_singleton_method(:inspect) { '#<BACK>' } }.freeze
LABELS =

Human-readable labels for each option key.

{
  api: 'API-only mode',
  active_record: 'Active Record (ORM)',
  database: 'Database',
  javascript: 'JavaScript approach',
  css: 'CSS framework',
  asset_pipeline: 'Asset pipeline',
  hotwire: 'Hotwire (Turbo + Stimulus)',
  jbuilder: 'Jbuilder (JSON templates)',
  action_mailer: 'Action Mailer',
  action_mailbox: 'Action Mailbox',
  action_text: 'Action Text (rich text)',
  active_job: 'Active Job',
  active_storage: 'Active Storage (file uploads)',
  action_cable: 'Action Cable (WebSockets)',
  test: 'Tests',
  system_test: 'System tests',
  brakeman: 'Brakeman (security scanner)',
  bundler_audit: 'Bundler Audit (dependency checker)',
  rubocop: 'RuboCop (linter)',
  ci: 'CI files',
  docker: 'Dockerfile',
  kamal: 'Kamal (deployment)',
  thruster: 'Thruster (HTTP/2 proxy)',
  solid: 'Solid (Cache/Queue/Cable)',
  devcontainer: 'Dev Container',
  bootsnap: 'Bootsnap (boot speedup)',
  dev_gems: 'Dev gems',
  keeps: 'Source control .keep files',
  decrypted_diffs: 'Decrypted diffs',
  git: 'Initialize git',
  bundle: 'Run bundle install'
}.freeze
HELP_TEXT =

Short explanations shown below each wizard step.

{
  api: 'Generates a slimmed-down app optimized for API backends.',
  active_record: 'Database ORM layer. Skipping also skips the database choice.',
  database: 'Which database adapter to configure.',
  javascript: 'How JavaScript is managed in the asset pipeline.',
  css: 'Which CSS framework to pre-install.',
  asset_pipeline: 'Which asset pipeline to use for JS/CSS bundling.',
  hotwire: 'Turbo + Stimulus for SPA-like behavior over HTML.',
  jbuilder: 'DSL for building JSON views.',
  action_mailer: 'Framework for sending emails.',
  action_mailbox: 'Routes inbound emails to controller-like mailboxes.',
  action_text: 'Rich text content and editing with Trix.',
  active_job: 'Framework for declaring and running background jobs.',
  active_storage: 'Upload files to cloud services like S3 or GCS.',
  action_cable: 'WebSocket framework for real-time features.',
  test: 'Generates test directory and helpers.',
  system_test: 'Browser-based integration tests via Capybara.',
  brakeman: 'Static analysis for security vulnerabilities.',
  bundler_audit: 'Checks dependencies for known vulnerabilities.',
  rubocop: 'Ruby style and lint checking.',
  ci: 'Generates CI workflow configuration.',
  docker: 'Generates Dockerfile for containerized deployment.',
  kamal: 'Generates Kamal deploy configuration.',
  thruster: 'HTTP/2 proxy with asset caching and X-Sendfile.',
  solid: 'Solid Cache, Solid Queue, and Solid Cable adapters.',
  devcontainer: 'Generates VS Code dev container configuration.',
  bootsnap: 'Speeds up boot times with caching.',
  dev_gems: 'Development gems like web-console.',
  keeps: 'Empty directories preserved via .keep files.',
  decrypted_diffs: 'Show decrypted diffs of encrypted credentials in git.',
  git: 'Initializes a git repository for the new app.',
  bundle: 'Runs bundle install after generating the app.'
}.freeze
CHOICE_HELP =

Per-choice hints displayed next to enum choices.

{
  database: {
    'sqlite3' => 'simple file-based, great for development',
    'postgresql' => 'full-featured, most popular for production',
    'mysql' => 'widely used relational database',
    'trilogy' => 'modern MySQL-compatible client',
    'mariadb-mysql' => 'MariaDB with mysql2 adapter',
    'mariadb-trilogy' => 'MariaDB with Trilogy adapter'
  },
  javascript: {
    'importmap' => 'no bundler, uses browser-native import maps',
    'bun' => 'fast all-in-one JS runtime and bundler',
    'webpack' => 'established full-featured bundler',
    'esbuild' => 'extremely fast JS bundler',
    'rollup' => 'ES module-focused bundler',
    'none' => 'no JavaScript setup'
  },
  asset_pipeline: {
    'propshaft' => 'modern, lightweight asset pipeline',
    'sprockets' => 'classic asset pipeline with preprocessing',
    'none' => 'no asset pipeline'
  },
  css: {
    'tailwind' => 'utility-first CSS framework',
    'bootstrap' => 'popular component-based framework',
    'bulma' => 'modern CSS-only framework',
    'postcss' => 'CSS transformations via plugins',
    'sass' => 'CSS with variables, nesting, and mixins',
    'none' => 'no CSS framework'
  }
}.freeze
SKIP_RULES =

Rules that determine when a wizard step should be silently skipped. Each lambda receives the current values hash and returns true to skip.

{
  database: ->(values) { values[:active_record] == false },
  javascript: ->(values) { values[:api] == true },
  css: ->(values) { values[:api] == true },
  asset_pipeline: ->(values) { values[:api] == true },
  hotwire: ->(values) { values[:api] == true },
  jbuilder: ->(values) { values[:api] == true },
  action_mailbox: ->(values) { values[:active_record] == false },
  action_text: ->(values) { values[:api] == true || values[:active_record] == false },
  active_storage: ->(values) { values[:active_record] == false },
  system_test: ->(values) { values[:test] == false || values[:api] == true }
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(compatibility_entry:, defaults:, prompter:) ⇒ Wizard



146
147
148
149
150
151
152
# File 'lib/create_rails_app/wizard.rb', line 146

def initialize(compatibility_entry:, defaults:, prompter:)
  @compatibility_entry = compatibility_entry
  @prompter = prompter
  @values = sanitize_defaults(defaults)
  @stashed = {}
  @last_presented_index = 0
end

Instance Attribute Details

#last_presented_indexObject (readonly)

Returns the value of attribute last_presented_index.



141
142
143
# File 'lib/create_rails_app/wizard.rb', line 141

def last_presented_index
  @last_presented_index
end

Instance Method Details

#run(start_index: 0) ⇒ Hash{Symbol => Object}

Runs the wizard and returns the selected options.



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/create_rails_app/wizard.rb', line 158

def run(start_index: 0)
  keys = Options::Catalog::ORDER.select { |key| @compatibility_entry.supports_option?(key) }
  index = [start_index, keys.length - 1].min
  while index < keys.length
    key = keys[index]

    if skip_step?(key)
      @stashed[key] = @values.delete(key) if @values.key?(key)
      index += 1
      next
    end

    @values[key] = @stashed.delete(key) if @stashed.key?(key) && !@values.key?(key)

    @last_presented_index = index
    answer = ask_for(key, index:, total: keys.length)
    case answer
    when BACK
      index = find_previous_unskipped(keys, index)
    else
      assign_value(key, answer)
      index += 1
    end
  end
  @values.dup
end