defmodule Mix.Tasks.Shadix.Add do
@shortdoc "Copies Shadix components into your app, rewriting module namespaces"
@moduledoc """
Copies one or more Shadix components into your Phoenix application,
rewriting `Shadix.*` module namespaces to match your app.
## Usage
mix shadix.add button card alert
## Options
* `--namespace` — target base module namespace
(default: `<AppModule>Web.Components.UI`)
* `--dir` — target directory to write files into
(default: `lib/<underscored_namespace>`)
* `--hooks-dir` — target directory for JS/TS LiveView hook files
(default: `assets/js/hooks`)
* `--force` — overwrite existing files (default: false, skip existing)
## What it does
1. Resolves transitive dependencies (e.g. `button` pulls in `cn`)
2. Rewrites `Shadix.Components` → `<namespace>` and `Shadix.Cn` → `<namespace>.Cn`
3. Writes files to the target directory
4. Prints a summary and a reminder to run `mix shadix.init` if not done yet
"""
use Mix.Task
@requirements ["app.config"]
@impl Mix.Task
def run(args) do
{opts, names, _invalid} =
OptionParser.parse(args,
strict: [namespace: :string, dir: :string, hooks_dir: :string, force: :boolean]
)
if names == [] do
Mix.raise("Usage: mix shadix.add <component> [component...] [options]")
end
namespace = Keyword.get(opts, :namespace) || Shadix.Generator.default_namespace()
dir = Keyword.get(opts, :dir) || Shadix.Generator.default_dir(namespace)
hooks_dir = Keyword.get(opts, :hooks_dir, "assets/js/hooks")
force = Keyword.get(opts, :force, false)
File.mkdir_p!(dir)
manifests = Shadix.Registry.resolve(names)
written =
Enum.flat_map(manifests, fn manifest ->
Enum.flat_map(manifest["files"], fn file ->
dest = Path.join(dir, file["path"])
content = Shadix.Generator.rewrite_namespace(file["content"], namespace)
maybe_write(dest, content, force)
end)
end)
# Hook sources are copied verbatim — they are NOT namespace-rewritten (TS, not Elixir).
hooks = Enum.flat_map(manifests, fn manifest -> manifest["hooks"] || [] end)
written_hooks =
if hooks == [] do
[]
else
File.mkdir_p!(hooks_dir)
Enum.flat_map(hooks, fn hook ->
dest = Path.join(hooks_dir, hook["path"])
maybe_write(dest, hook["content"], force)
end)
end
Mix.shell().info("")
if written == [] and written_hooks == [] do
Mix.shell().info("No files written (all already existed).")
else
if written != [] do
Mix.shell().info("#{length(written)} file(s) written to #{dir}/")
end
if written_hooks != [] do
Mix.shell().info("#{length(written_hooks)} hook file(s) written to #{hooks_dir}/")
end
end
Mix.shell().info("""
Run `mix shadix.init` if you haven't already to install the theme CSS
and configure Tailwind.
""")
maybe_print_hook_notes(hooks)
npm_deps =
manifests
|> Enum.flat_map(fn manifest -> manifest["npm_deps"] || [] end)
|> Enum.uniq()
maybe_print_npm_notes(npm_deps)
end
# Writes `content` to `dest`, skipping pre-existing files unless `force`.
# Returns `[dest]` if written, `[]` otherwise (for flat_map accumulation).
defp maybe_write(dest, content, force) do
if File.exists?(dest) and not force do
Mix.shell().info(" skip #{dest} (already exists; use --force to overwrite)")
[]
else
File.write!(dest, content)
Mix.shell().info(" write #{dest}")
[dest]
end
end
defp maybe_print_hook_notes([]), do: :ok
defp maybe_print_hook_notes(hooks) do
names = hooks |> Enum.map(& &1["name"]) |> Enum.uniq()
Mix.shell().info("""
This component ships LiveView hook(s): #{Enum.join(names, ", ")}.
Import and register them in your LiveSocket `hooks:` map, e.g.:
import { #{Enum.join(names, ", ")} } from "./hooks/dialog"
const liveSocket = new LiveSocket("/live", Socket, {
hooks: { #{Enum.join(names, ", ")} },
// ...
})
""")
end
defp maybe_print_npm_notes([]), do: :ok
defp maybe_print_npm_notes(deps) do
Mix.shell().info("""
This component depends on npm package(s). Install them in your assets dir:
npm install #{Enum.join(deps, " ")}
""")
end
end