# The Skua installer runs as a full Igniter task when Igniter is available — so
# `mix igniter.install skua` and `mix igniter.new my_app --with phx.new --install
# skua` work, with diff preview, `--yes`, and idempotent re-runs. Without Igniter
# it falls back to a self-contained Mix task, so `mix skua.install` still works
# and Skua compiles cleanly on apps that don't carry Igniter. Both paths share
# the pure transforms in `Skua.Install.Patches`.
if Code.ensure_loaded?(Igniter) do
defmodule Mix.Tasks.Skua.Install do
@shortdoc "Wire Skua into a Phoenix app and scaffold the starter home page"
@moduledoc """
Installs Skua into the current Phoenix project.
mix igniter.install skua
mix skua.install
Wires `assets/css/skua.css` and the JS hooks bundle, imports Skua's
components into `lib/<app>_web.ex` (Skua wins for `button`/`input`/`header`/
`table`/`list`), routes flashes through Skua toasts, strips the default
Phoenix navbar, and scaffolds an editable starter `HomeLive` at `/`.
Every step is idempotent — re-run any time after a Skua upgrade and it only
adds what's missing. Steps that don't match the default Phoenix layout are
reported as a manual instruction rather than corrupting a file.
"""
use Igniter.Mix.Task
alias Skua.Install.Patches, as: P
@impl Igniter.Mix.Task
def info(_argv, _composing_task) do
%Igniter.Mix.Task.Info{
example: "mix igniter.install skua --theme greenfield --strip-daisy",
schema: [strip_daisy: :boolean, theme: :string]
}
end
@impl Igniter.Mix.Task
def igniter(igniter) do
case P.check_requirements() do
{:error, msg} ->
# Refuse cleanly — adding an issue aborts the run with no partial apply.
Igniter.add_issue(igniter, msg)
:ok ->
{css_ref, js_ref} = P.asset_refs()
{web_mod, web_dir, web_ex} = P.web_context()
layouts = Path.join([web_dir, "components", "layouts.ex"])
# nil (flag omitted) means "auto" — strip when daisyUI is present; the
# Patches transforms self-skip when there's nothing to strip.
strip_daisy = Keyword.get(igniter.args.options, :strip_daisy, :auto)
theme = Keyword.get(igniter.args.options, :theme)
igniter
|> patch("assets/css/app.css", &P.css(&1, css_ref))
|> patch("assets/js/app.js", &P.app_js(&1, js_ref))
|> patch(web_ex, &P.web_imports(&1, web_mod))
|> patch(layouts, &P.layout_flash/1)
|> patch(layouts, &P.strip_chrome/1)
|> gen_home(web_mod, web_dir)
|> patch(Path.join(web_dir, "router.ex"), &P.router/1)
|> patch(
Path.join([web_dir, "components", "layouts", "root.html.heex"]),
&P.root_layout/1
)
|> maybe_strip_daisy(strip_daisy, layouts)
|> maybe_apply_theme(theme)
|> Igniter.add_notice("Skua installed. Run `mix phx.server` and open `/`.")
end
end
defp maybe_apply_theme(igniter, nil), do: igniter
defp maybe_apply_theme(igniter, name) do
if Skua.Themes.exists?(name) do
igniter
|> patch("assets/css/app.css", &P.theme_css(&1, Skua.Themes.css(name)))
|> Igniter.add_notice(
"Applied the \"#{name}\" theme to assets/css/app.css — edit the tokens there, " <>
"or re-run with a different --theme."
)
else
Igniter.add_issue(
igniter,
"Unknown --theme #{inspect(name)}. Run `mix skua.themes` to list all " <>
"#{length(Skua.Themes.names())}, or omit --theme to keep the default look."
)
end
end
defp maybe_strip_daisy(igniter, false, _layouts), do: igniter
defp maybe_strip_daisy(igniter, _strip, layouts) do
igniter
|> patch("assets/css/app.css", &P.strip_daisy_css/1)
|> patch(layouts, &P.strip_daisy_theme_toggle/1)
|> rm_if_exists(P.daisy_vendor_files())
|> Igniter.add_notice(
"daisyUI removed where present — vendored files deleted; base-*/primary/error " <>
"bridged to Skua tokens. Pass --no-strip-daisy to keep daisyUI."
)
end
defp rm_if_exists(igniter, paths) do
Enum.reduce(paths, igniter, fn path, acc ->
if Igniter.exists?(acc, path), do: Igniter.rm(acc, path), else: acc
end)
end
# Apply a `Skua.Install.Patches` transform to a file through Igniter's
# rewrite pipeline. `:skip` is a no-op, `{:manual, msg}` degrades to a
# warning instead of corrupting the file.
defp patch(igniter, path, transform) do
if Igniter.exists?(igniter, path) do
Igniter.update_file(igniter, path, fn source ->
case transform.(Rewrite.Source.get(source, :content)) do
:skip -> source
{:ok, new} -> Rewrite.Source.update(source, :content, new)
{:manual, msg} -> {:warning, msg}
end
end)
else
Igniter.add_warning(igniter, "#{path} not found — apply the Skua step here manually.")
end
end
defp gen_home(igniter, web_mod, web_dir) do
# Igniter derives an Elixir source's path from its module name
# (FooWeb.HomeLive -> lib/foo_web/home_live.ex), so we generate there.
# Idempotent across both that path and the legacy live/ path Skua 0.1.0
# used, so re-running never produces a duplicate module.
dest = Path.join(web_dir, "home_live.ex")
legacy = Path.join([web_dir, "live", "home_live.ex"])
cond do
Igniter.exists?(igniter, dest) or Igniter.exists?(igniter, legacy) ->
igniter
true ->
case P.home_body(web_mod) do
{:ok, body} -> Igniter.create_new_file(igniter, dest, body)
{:manual, msg} -> Igniter.add_warning(igniter, msg)
end
end
end
end
else
defmodule Mix.Tasks.Skua.Install do
@shortdoc "Wire Skua into a Phoenix app and scaffold the starter home page"
@moduledoc """
Installs Skua into the current Phoenix project.
mix skua.install
Wires `assets/css/skua.css` and the JS hooks bundle, imports Skua's
components into `lib/<app>_web.ex`, routes flashes through Skua toasts,
strips the default Phoenix navbar, and scaffolds an editable starter
`HomeLive` at `/`. Every step is idempotent.
Add `{:igniter, "~> 0.8", only: [:dev, :test]}` (or run `mix igniter.install
skua`) to get the one-command Igniter installer with diff preview.
"""
use Mix.Task
alias Skua.Install.Patches, as: P
@requirements ["app.config"]
@impl Mix.Task
def run(args) do
# Refuse cleanly on an incompatible host (no partial apply).
with {:error, msg} <- P.check_requirements(), do: Mix.raise(msg)
{opts, _, _} = OptionParser.parse(args, switches: [strip_daisy: :boolean, theme: :string])
# nil (flag omitted) means "auto" — strip when daisyUI is present; the
# Patches transforms self-skip when there's nothing to strip.
strip_daisy = Keyword.get(opts, :strip_daisy, :auto)
theme = Keyword.get(opts, :theme)
if theme && not Skua.Themes.exists?(theme) do
Mix.raise(
"Unknown --theme #{inspect(theme)}. Run `mix skua.themes` to list all " <>
"#{length(Skua.Themes.names())}, or omit --theme to keep the default look."
)
end
app = Mix.Project.config()[:app]
{web_mod, web_dir, web_ex} = P.web_context()
layouts = Path.join([web_dir, "components", "layouts.ex"])
{css_ref, js_ref} = P.asset_refs()
Mix.shell().info([:cyan, "\n Installing Skua into #{app}…\n", :reset])
results =
[
file_patch("assets/css/app.css", &P.css(&1, css_ref), "add @import for skua.css"),
file_patch("assets/js/app.js", &P.app_js(&1, js_ref), "import + register Skua hooks"),
file_patch(
web_ex,
&P.web_imports(&1, web_mod),
"import Skua components (except button/input)"
),
file_patch(layouts, &P.layout_flash/1, "render flashes as Skua toasts"),
file_patch(layouts, &P.strip_chrome/1, "strip default Phoenix chrome"),
gen_home(web_mod, web_dir),
file_patch(
Path.join(web_dir, "router.ex"),
&P.router/1,
"route / to the Skua home LiveView"
),
file_patch(
Path.join([web_dir, "components", "layouts", "root.html.heex"]),
&P.root_layout/1,
"add pre-paint theme script"
)
] ++ strip_daisy_results(strip_daisy, layouts) ++ theme_results(theme)
print_summary(results)
Mix.shell().info([
:green,
"\n Done. ",
:reset,
"Run ",
:bright,
"mix phx.server",
:reset,
" and open ",
:bright,
"/",
:reset,
".\n"
])
end
defp file_patch(path, transform, action) do
cond do
not File.exists?(path) ->
{:manual, path, "file not found — apply this step manually"}
true ->
case transform.(File.read!(path)) do
:skip ->
{:skip, path, action}
{:ok, new} ->
File.write!(path, new)
{:ok, path, action}
{:manual, msg} ->
{:manual, path, msg}
end
end
end
defp gen_home(web_mod, web_dir) do
# Match the Igniter branch: module-convention path, idempotent across the
# legacy live/ path Skua 0.1.0 used.
dest = Path.join(web_dir, "home_live.ex")
legacy = Path.join([web_dir, "live", "home_live.ex"])
cond do
File.exists?(dest) or File.exists?(legacy) ->
{:skip, dest, "generate starter home LiveView"}
true ->
case P.home_body(web_mod) do
{:ok, body} ->
File.mkdir_p!(Path.dirname(dest))
File.write!(dest, body)
{:ok, dest, "generate starter home LiveView"}
{:manual, msg} ->
{:manual, dest, msg}
end
end
end
defp theme_results(nil), do: []
defp theme_results(name) do
[
file_patch(
"assets/css/app.css",
&P.theme_css(&1, Skua.Themes.css(name)),
"apply the #{name} theme"
)
]
end
defp strip_daisy_results(false, _layouts), do: []
defp strip_daisy_results(_strip, layouts) do
[
file_patch(
"assets/css/app.css",
&P.strip_daisy_css/1,
"strip daisyUI + add token bridge"
),
file_patch(layouts, &P.strip_daisy_theme_toggle/1, "swap theme toggle to Skua")
] ++ Enum.map(P.daisy_vendor_files(), &rm_vendor/1)
end
defp rm_vendor(path) do
if File.exists?(path) do
File.rm!(path)
{:ok, path, "remove vendored daisyUI"}
else
{:skip, path, "remove vendored daisyUI"}
end
end
defp print_summary(results) do
Enum.each(results, fn
{:ok, path, action} ->
Mix.shell().info([
:green,
" ✓ ",
:reset,
"#{action || "patched"} — ",
:bright,
path,
:reset
])
{:skip, path, action} ->
Mix.shell().info([:faint, " • already done — #{action || path}", :reset])
{:manual, path, msg} ->
Mix.shell().info([:yellow, " ! ", :reset, "#{path}: ", msg])
end)
end
end
end