defmodule Mix.Tasks.Phia.Install do @moduledoc """ Sets up PhiaUI in the current Phoenix project. Injects the PhiaUI TailwindCSS v4 `@theme` block into `assets/css/app.css`, creates `assets/js/phia_hooks/index.js`, and registers PhiaUI hooks in `assets/js/app.js`. ## Usage mix phia.install ## Behaviour - Detects the current app name via `Mix.Project.config()[:app]`. - Prepends the `@theme { ... }` block from `priv/templates/theme/theme.css` into `assets/css/app.css`. - Creates `assets/js/phia_hooks/index.js` with PhiaHooks export. - Adds PhiaHooks import and registration to `assets/js/app.js`. - **Idempotent**: all steps are safe to run multiple times. - Prints confirmation messages via `Mix.shell().info/1`. ## Options --help Print this help message """ use Mix.Task @shortdoc "Installs PhiaUI theme tokens, hooks, and app.js wiring" @css_marker "/* PhiaUI Theme — do not remove this line */" @js_marker "// phia_hooks_registered" @hooks_content """ // PhiaUI JS Hooks — auto-generated by mix phia.install // Add your custom PhiaUI hooks here or import from sub-modules. const PhiaHooks = {} export default PhiaHooks """ @impl Mix.Task def run(["--help"]) do Mix.shell().info(@moduledoc) end def run(_args) do name = app_name() root = File.cwd!() css_path = Path.join([root, "assets/css/app.css"]) if File.exists?(css_path) do inject_theme(css_path) Mix.shell().info([:green, "* inject ", :reset, css_path]) else Mix.shell().error("Could not find #{css_path}. Run from your Phoenix project root.") end inject_hooks(root) Mix.shell().info([:green, "* create ", :reset, "assets/js/phia_hooks/index.js"]) inject_app_js(root) Mix.shell().info([:green, "* update ", :reset, "assets/js/app.js"]) Mix.shell().info([:green, "✓ PhiaUI installed for ", :reset, inspect(name)]) end @doc """ Returns the current Mix project's application name. ## Example iex> Mix.Tasks.Phia.Install.app_name() :phia_ui """ @spec app_name() :: atom() def app_name, do: Mix.Project.config()[:app] @doc """ Injects the PhiaUI theme block into the CSS file at `path`. Skips silently if the marker is already present (idempotent). """ @spec inject_theme(Path.t()) :: :ok | :already_installed def inject_theme(path) do current = File.read!(path) if String.contains?(current, @css_marker) do :already_installed else theme_block = build_theme_block() File.write!(path, theme_block <> current) :ok end end @doc """ Creates `assets/js/phia_hooks/index.js` under `root` with PhiaHooks export. Uses `Mix.Generator.create_file/3` for interactive conflict handling: if the file already exists, the user is prompted before any overwrite. """ @spec inject_hooks(Path.t()) :: :ok | nil def inject_hooks(root) do hooks_dir = Path.join([root, "assets", "js", "phia_hooks"]) hooks_path = Path.join(hooks_dir, "index.js") File.mkdir_p!(hooks_dir) Mix.Generator.create_file(hooks_path, @hooks_content) end @doc """ Adds PhiaHooks import and registration to `assets/js/app.js` under `root`. Skips if the idempotency marker is already present. """ @spec inject_app_js(Path.t()) :: :ok | :already_installed | :not_found def inject_app_js(root) do app_js = Path.join([root, "assets", "js", "app.js"]) if File.exists?(app_js) do current = File.read!(app_js) if String.contains?(current, @js_marker) do :already_installed else injection = "#{@js_marker}\n" <> "import PhiaHooks from './phia_hooks/index.js'\n\n" File.write!(app_js, injection <> current) :ok end else :not_found end end # --------------------------------------------------------------------------- # Private helpers # --------------------------------------------------------------------------- defp build_theme_block do template_path = template_path() theme_css = File.read!(template_path) @css_marker <> "\n" <> theme_css <> "\n" end defp template_path do # When running inside the phia_ui library itself, use priv/ directly. # When installed as a dep, use Application.app_dir/2. local = Path.join([File.cwd!(), "priv/templates/theme/theme.css"]) if File.exists?(local) do local else Application.app_dir(:phia_ui, "priv/templates/theme/theme.css") end end end