Current section
Files
Jump to
Current section
Files
lib/mix/tasks/phia.install.ex
defmodule Mix.Tasks.Phia.Install do
@moduledoc """
Sets up PhiaUI in the current Phoenix project.
This is the first command to run when integrating PhiaUI into an existing
Phoenix application. It performs three idempotent setup steps:
1. **Injects the theme CSS** — Prepends the PhiaUI TailwindCSS v4 `@theme`
block (color tokens, radius, typography, etc.) into `assets/css/app.css`.
2. **Creates the hooks entry point** — Writes
`assets/js/phia_hooks/index.js` with an empty `PhiaHooks` export that
you can add individual component hooks to.
3. **Wires hooks into app.js** — Prepends an import statement and the
`PhiaHooks` registration snippet to `assets/js/app.js`.
## Usage
mix phia.install
Run this command once from the root of your Phoenix project. It is safe to
run multiple times — each step is idempotent and skips silently when already
applied.
## What gets modified
assets/css/app.css — PhiaUI @theme block prepended
assets/js/phia_hooks/ — directory created
assets/js/phia_hooks/index.js — PhiaHooks export created
assets/js/app.js — import + hook registration prepended
## Typical workflow
# 1. Install PhiaUI setup
$ mix phia.install
# 2. Eject individual components as editable source files
$ mix phia.add button
$ mix phia.add dialog
# 3. Optionally install theme presets for runtime colour switching
$ mix phia.theme install
## Idempotency markers
The task uses sentinel strings to detect prior installation:
- CSS: `/* PhiaUI Theme — do not remove this line */`
- JS: `// phia_hooks_registered`
Do not remove these markers, as they prevent double-injection.
## Options
--help Print this help message
"""
use Mix.Task
@shortdoc "Installs PhiaUI theme tokens, hooks, and app.js wiring"
# Sentinel string written at the top of app.css to detect prior installation.
@css_marker "/* PhiaUI Theme — do not remove this line */"
# Sentinel string written at the top of app.js to detect prior installation.
@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 atom.
## 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`.
The theme block is read from `priv/templates/theme/theme.css` and prepended
to the existing file contents, separated by the idempotency marker comment.
Skips silently if the marker is already present, so it is safe to call this
function multiple times.
## Return values
- `:ok` — theme block was injected.
- `:already_installed` — marker was already present; file was not changed.
"""
@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 an empty
`PhiaHooks` export.
Uses `Mix.Generator.create_file/3` for interactive conflict handling: if the
file already exists, the user is prompted before any overwrite.
The generated file exports an empty object that you populate by importing
individual component hook files, for example:
import PhiaDialog from "./dialog.js"
const PhiaHooks = { PhiaDialog }
export default PhiaHooks
"""
@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 a PhiaHooks import and registration snippet to `assets/js/app.js`
under `root`.
The snippet is prepended to the file so the hooks are available before
any LiveSocket initialisation code.
Skips if the idempotency marker is already present.
## Return values
- `:ok` — snippet was prepended to app.js.
- `:already_installed` — marker was already present; file was not changed.
- `:not_found` — `assets/js/app.js` does not exist.
"""
@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
# ---------------------------------------------------------------------------
# Reads the theme CSS template and wraps it with the idempotency marker.
# The marker must appear at the very top so `String.contains?/2` finds it
# even if the file has been reformatted.
defp build_theme_block do
template_path = template_path()
theme_css = File.read!(template_path)
@css_marker <> "\n" <> theme_css <> "\n"
end
# Resolves the theme CSS template path.
# Prefers a local priv/ path (useful when developing phia_ui itself) and
# falls back to the installed application's priv directory.
defp template_path do
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