Composable project starters for Phoenix applications.
Starter turns your post-phx.new ritual into a starter: an ordered,
flag-aware list of steps that lives in your project, built on
Igniter.
Concepts
Step — a single unit of setup work. Every step is an
Igniter.Mix.Task, so steps compose, preview their changes as diffs, and never blindly overwrite files. Starter ships a library of built-in steps in three kinds::add(install and wire up a package),:remove(undo aphx.newdefault), and:gen(generate configuration or code).Starter — a module that lists steps in order. Starters support optional steps behind flags, arbitrary Mix task composition, your own custom step modules, and even other starters.
Quick start
Generate your starter, then run it:
mix starter.new
mix starter.runThe generated starter documents every step in place — delete what you
don't want, reorder freely, and tag steps with if: :flag to make them
optional (mix starter.run --oban-pro).
Writing a starter
use Starter turns a module into a full Igniter.Mix.Task. Name it
under Mix.Tasks (for example Mix.Tasks.MyApp.Starter) and it becomes
runnable as mix my_app.starter — or via mix starter.run, which finds
it — with a --flag option for every optional step.
defmodule Mix.Tasks.MyApp.Starter do
use Starter
@impl Starter
def steps do
[
{:remove, :daisy_ui},
{:remove, :topbar},
{:gen, :gitignore},
{:add, :credo},
{:add, :oban, if: :oban},
{:task, "igniter.install", ["ash"]}
]
end
endRunning mix my_app.starter applies every unconditional step;
mix my_app.starter --oban also applies the Oban step.
Step forms
{:add, :credo}— get a package into the app: Starter's own step when it has one, otherwise the package is installed and its own installer runs. Which of the two applies is upstream's business, not something a starter file should encode.{:remove, :topbar}— run the built-in remove steptopbar{:gen, :gitignore}— run the built-in gen stepgitignore{:add, :oban, if: :oban}— only when the--obanflag is passed{:install, :ash}— force the package's own installer, skipping any built-in step of the same name. Rarely needed;{:add, :ash}already resolves this way when Starter ships no step.{:add, :"oban.oban_pro"}— a package from a private Hex repo or organization. Names resolve against public Hex unless qualified, sorepo.package(andorg/package) must be spelled out.{:task, "some.igniter.task"}— compose any Igniter-aware Mix task, with optional argv and anif:option as a fourth element; non-Igniter tasks are skipped with a warning{:queue, "starter.add", ["oban_pro"]}— queue any Mix task to run after the starter's changes apply; use this only for work that must see the applied project on disk{:starter, OtherStarter}— include another starter's stepsMyApp.Steps.Custom— any module implementingIgniter.Mix.Task, optionally as{MyApp.Steps.Custom, if: :flag}
Queued tasks run non-interactively with --yes appended — their
subprocesses have no stdin, so a prompt there could never be answered.
Confirming the starter's diff is the approval for everything it queues;
queued tasks must tolerate the --yes flag (every Igniter task does).
Composition
Starters compose as a tree, not just a flat list. A {:starter, Other}
step includes another starter's steps, an included starter can include
starters itself — to any depth — and an include can be flag-gated:
{:starter, Other, if: :flag}. Packages and custom steps are the leaves.
At run time the tree is flattened depth-first into one ordered step list,
so a nested starter's steps run exactly where its include appears.
Packages the whole tree installs are deduped and fetched up front, a
single diff covers the entire run, and flags declared anywhere in the
tree surface as --flag options on the task you invoke.
Ordering
Steps apply in list order, installers included, and every change lands in
one diff you confirm once. A step placed after one that installs a package
sees its effects, so ordering is just list position — e.g.
{:gen, :sort_deps} at the end sorts the deps the installers added.
The one exception is dependencies. Packages that resolve to an install are
added to mix.exs and fetched before any step runs, because their
installers have to be on disk to compose. That dependency change is
written and confirmed on its own, ahead of the run's main diff.
Queued steps still run last, in list order, after everything applies.
Each run prints a plan showing what every step resolved to — a built-in step, a package's installer, a plain dependency, or queued work.
Summary
Functions
Returns every flag referenced by if: options in the given steps,
including flags of nested starters.
Returns every flag used by the given starter module.
Types
@type step() :: {:add | :remove | :gen, atom()} | {:add | :remove | :gen, atom(), keyword()} | {:install, atom()} | {:install, atom(), keyword()} | {:queue, String.t()} | {:queue, String.t(), [String.t()]} | {:queue, String.t(), [String.t()], keyword()} | {:task, String.t()} | {:task, String.t(), [String.t()]} | {:task, String.t(), [String.t()], keyword()} | {:starter, module()} | {:starter, module(), keyword()} | module() | {module(), keyword()}
A single entry in a starter's steps/0 list.
Callbacks
@callback steps() :: [step()]
Returns the starter's steps, in the order they should run.