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 +Ctrl+B+ 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.

Returns:

  • (Hash{Symbol => String})
{
  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.

Returns:

  • (Hash{Symbol => String})
{
  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.

Returns:

  • (Hash{Symbol => Hash{String => String}})
{
  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.

Returns:

  • (Hash{Symbol => Proc})
{
  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

Returns a new instance of Wizard.

Parameters:



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_index ⇒ Object (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.

Parameters:

  • start_index (Integer) (defaults to: 0) —

    step index to resume from

Returns:

  • (Hash{Symbol => Object})


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