Recovery for the mix phoenix_kit.* tasks that cannot run without igniter.
Igniter is an optional dependency (see mix.exs), and every task that drives
it is wrapped in if Code.ensure_loaded?(Igniter.Mix.Task). Without a
fallback the guard's else branch defines nothing at all, so the task simply
vanishes and Mix reports:
** (Mix) The task "phoenix_kit.update" could not be foundwhich names neither PhoenixKit nor igniter, and leaves a host stranded on an
old schema with nothing to search for. This module defines the stand-in task
that takes its place, and — since the dep line is the same every time —
offers to write it into the host's mix.exs instead of only describing it.
Why the dep is optional at all
A stock mix phx.new app declares {:igniter, "~> 0.6", only: [:dev, :test]}.
A non-optional dep here resolves for all environments, and Mix refuses to
converge the two — which broke mix igniter.install phoenix_kit on every
freshly generated project. Optional means the host's own declaration wins.
The cost is this case: a host that never declared igniter itself was getting it transitively, and an upgrade drops it. That is what this module covers.
Why the compile-time guard is not enough
Code.ensure_loaded?/1 runs when PhoenixKit is compiled into the host's
_build, and PhoenixKit is not recompiled when the host's own dependency
list changes. Either side of the guard can therefore outlive the fact that
chose it:
Compiled with igniter, igniter since dropped. The stale beam still takes the igniter branch, the
elsebranch was never compiled, and the host gets** (UndefinedFunctionError) function Igniter.Mix.Task.help_requested?/1 is undefinedout of therun/1generated byuse Igniter.Mix.Task. Every igniter-backed task callsensure_available!/3at the top of its ownrun/1to catch this.Compiled without igniter, igniter since added. The stand-in below is still what Mix loads, so following its own advice appears to change nothing.
stand_in_run/2detects that and asks for the one-line recompile instead of repeating the instructions.
What it offers to do
With igniter genuinely absent, ensure_available!/3 prints the situation and
asks to add {:igniter, "~> 0.7", only: [:dev, :test]} to the host's
deps/0 (auto-accepted under --yes/-y), then shells out to mix deps.get
— a fresh OS process, because the running one evaluated mix.exs at boot and
will not see the new dep. It stops there rather than continuing: PhoenixKit
itself has to be recompiled to pick the other side of the guard, which cannot
happen inside the run that is already executing the wrong side. Declining, a
MIX_ENV where only: [:dev, :test] would not help, an unparseable deps/0,
or a failed fetch all fall back to the manual instructions.
Summary
Functions
Body of the stand-in task. The caller supplies its own @moduledoc —
generating one from here would hide it from static analysis.
Inserts {:igniter, "~> 0.7", only: [:dev, :test]} at the top of a mix.exs deps/0 list.
Ensures igniter is loadable, or stops the task with the best available help.
The guidance printed when igniter is missing and cannot be added for us.
Body of the stand-in task, compiled in place of a task that needs igniter.
Types
Functions
Body of the stand-in task. The caller supplies its own @moduledoc —
generating one from here would hide it from static analysis.
Inserts {:igniter, "~> 0.7", only: [:dev, :test]} at the top of a mix.exs deps/0 list.
Returns the rewritten source, or {:error, :already_declared} when igniter
is already in the list (a different problem — see message/2) and
{:error, :deps_not_found} when the list is not in the shape the generators
produce.
Ensures igniter is loadable, or stops the task with the best available help.
Called at the top of run/1 in every igniter-backed task, and by the
stand-in task this module defines. Returns :ok only when igniter is really
there; every other outcome raises, because the caller's next move is code
that cannot run.
argv is the task's own arguments — --yes/-y skips the confirmation
prompt. igniter_module exists so the failing branch is testable; callers
pass the task name and argv only.
The guidance printed when igniter is missing and cannot be added for us.
reason names what stopped the automatic path, so the manual instructions
can lead with the part that is actually different for this host.
Body of the stand-in task, compiled in place of a task that needs igniter.
Splits the two ways of arriving here: igniter is still missing (offer to add it), or it has since been added and only this stale module remembers otherwise (ask for the recompile).