PhoenixKit.Install.MissingIgniter (phoenix_kit v2.17.0)

Copy Markdown View Source

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 found

which 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 else branch was never compiled, and the host gets ** (UndefinedFunctionError) function Igniter.Mix.Task.help_requested?/1 is undefined out of the run/1 generated by use Igniter.Mix.Task. Every igniter-backed task calls ensure_available!/3 at the top of its own run/1 to 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/2 detects 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

Types

Why the dep line could not be added automatically.

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

failure()

@type failure() ::
  :declined
  | :already_declared
  | :deps_not_found
  | :no_mix_exs
  | :fetch_failed
  | :unsupported_env

Why the dep line could not be added automatically.

Functions

__using__(opts)

(macro)

Body of the stand-in task. The caller supplies its own @moduledoc — generating one from here would hide it from static analysis.

add_igniter_dep(source)

@spec add_igniter_dep(String.t()) :: {:ok, String.t()} | {:error, failure()}

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.

ensure_available!(task, argv \\ [], igniter_module \\ Igniter.Mix.Task)

@spec ensure_available!(String.t(), [String.t()], module()) :: :ok

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.

message(task, reason \\ nil)

@spec message(String.t(), failure() | nil) :: String.t()

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.

stand_in_run(task, argv)

@spec stand_in_run(String.t(), [String.t()]) :: no_return()

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).