Architecture
Table of contents
The shape
The gem is a Thor CLI (exe/opinionated_rails, lib/opinionated_rails/cli.rb). new runs rails new, then a set of steps: small classes, each owning one option, that prompt, edit the generated app, and record what they did.
lib/opinionated_rails/
cli.rb the commands and the run's orchestration
answer_shell.rb answers prompts from flags/config; validates values
context.rb shared state for one run
step.rb, step_graph.rb the step contract and dependency ordering
prerequisites.rb tools the chosen options need (yarn, bun, node)
rails_version_guard.rb Rails 8.1+ check
kamal_config.rb builds config/deploy.yml and .kamal/secrets
pins.rb inventory and registry check of pinned versions
generators/ rails_new_invoker.rb, file_writer.rb
steps/core/ steps asked before `rails new`
steps/addons/ steps that run afterwards
templates/ static files copied into generated apps
One run of new
CLI#new in order:
RailsVersionGuard.check_installed!readsrails --versionand rejects anything but 8.1+ within 8.x.- Answers.
preset_answersmerges--config(YAML), then--databaseand--javascript; theAPP_NAMEargument overridesapp_name.AnswerShellwraps the Thor shell with them. Its constructor validates every key and value (LABELS,CHOICES), so mistakes fail before anything is generated. - Core prompts run through
prompt_and_generate:AppNameStep,DatabaseStep,AppTypeStep, thenTestingStepandFrontendStep(prompt only). The directory must not already exist. Prerequisites.check!verifies yarn or bun for React.RailsNewInvokerrunsrails newwith flags built from the answers (--database,--api,--skip-test,--javascript, and always--skip-docker --skip-kamal --skip-ci, because the matching steps write better files).StepGraphsortsCLI.generated_stepsby theirneeds; each step is prompted (if it has not been) and generated byrun_step. An exception is wrapped with the step name and the app path.finish_app: finalbundle install, JavaScript install and builds, Tailwind build, RuboCop autocorrect.- Unused config keys are reported.
Key classes
Step (step.rb): the base class. Class methods id, tier, needs, summary, description, prompts; instance methods if_condition(context), prompt(context), generate(context), outputs(context). See Writing a step.
Context (context.rb): state for one run. answer(key, value) / get(key) hold what the user chose; set_output(step_id, hash) / output(step_id, key) hold what steps produced, which later steps read (for example context.output(:database, :database)). It also carries app_path, shell and skip_bundle_install.
StepGraph (step_graph.rb): a topological sort (Kahn’s algorithm) over needs. It raises MissingDependencyError if a needed step is not registered, and CycleError for cycles, naming the steps involved.
AnswerShell (answer_shell.rb): the object steps call ask on. It answers by matching the prompt’s label (the text before (default) to a config key, returns "" (the default) under --yes, and otherwise delegates to the real shell. If a step re-asks a preset question (meaning it rejected the value), it raises instead of looping. LABELS and CHOICES are the single list of config keys and accepted values; answer_choices_spec.rb proves every choice is accepted by the step that asks for it.
KamalConfig (kamal_config.rb): pure functions that return the text of config/deploy.yml and .kamal/secrets for a database and stack. DockerKamalStep writes them after kamal init.
Prerequisites and RailsVersionGuard: preflight checks that fail before anything is created.
Pins (pins.rb): finds every pinned gem and npm version in the steps and checks each exists on its registry (rake pins:check). A new *_VERSION constant must be mapped to a gem name in Pins::GEM_NAMES or the spec fails.
Core versus add-on
Steps::Core (app_name, database, app_type, testing, frontend) exists because CLI#new must know these before rails new runs: they become rails new flags. A step belongs in core only if something structural depends on it running before rails new. Everything else is an add-on that declares needs.
Why the finishing steps exist
Steps append gems to the Gemfile and packages to package.json after the last install that Rails or a generator ran. Without a final install and build the app would not boot, or the first page would fail on missing assets. The finishing steps are not cosmetic; the real-app matrix covers each.
Editing Rails’ generated files
Steps patch text Rails generates (production.rb, cable.yml, …). Substituting a string that is absent silently does nothing, which leaves an app half-configured with no sign of it. The rule: warn through context.shell.say("Warning: ...") and include the exact setting to apply by hand. See solid_stack_step.rb and mailer_step.rb for the pattern.