Packages

Compile-time design token library for Elixir

Current section

Files

Jump to
jetons lib mix tasks jetons.namespace.ex
Raw

lib/mix/tasks/jetons.namespace.ex

defmodule Mix.Tasks.Jetons.Namespace do
@moduledoc """
Hoists the `--keys` token groups under `--group` across every `*.tokens.json`
under DIR and rewrites the `{ref}`s pointing at them, so the tokens emit
`--group-*` CSS properties (`hue.300` -> `color.hue.300` -> `--color-hue-300`).
Idempotent; only rewrites files it changes. See `Jetons.Namespace`.
## Usage
mix jetons.namespace path/to/tokens --group color --keys accent,hue,hue-t,utility
mix jetons.namespace path/to/tokens --group color --keys accent,hue,hue-t,utility --check
## Options
* `--group` - parent group to nest under (required)
* `--keys` - comma-separated top-level keys to move and re-reference (required)
* `--unnest` - reverse: lift `--keys` out of `--group` and strip the prefix
* `--check` - report changes without writing; exits non-zero if any are needed
## Example
Given `palette.tokens.json` (`--group color --keys hue`):
{ "hue": { "300": { "$value": "#3b82f6" } } }
the task rewrites it in place to:
{ "color": { "hue": { "300": { "$value": "#3b82f6" } } } }
and prints `palette.tokens.json: nested 1 group(s), rewrote refs`.
"""
use Mix.Task
alias Jetons.Namespace
@shortdoc "Nest top-level token groups under a parent (e.g. hue -> color.hue)"
@switches [group: :string, keys: :string, unnest: :boolean, check: :boolean]
@impl true
def run(args) do
{opts, paths} = OptionParser.parse!(args, strict: @switches)
dir = Path.expand(List.first(paths) || ".")
group = Keyword.fetch(opts, :group) |> ok_or_raise("--group is required")
keys_csv = Keyword.fetch(opts, :keys) |> ok_or_raise("--keys is required")
keys = keys_csv |> String.split(",", trim: true) |> Enum.map(&String.trim/1)
mode = if Keyword.get(opts, :check, false), do: :check, else: :write
direction = if Keyword.get(opts, :unnest, false), do: :unnest, else: :nest
config = %{dir: dir, group: group, keys: keys, mode: mode, direction: direction}
files = Path.wildcard(Path.join(dir, "**/*.tokens.json"))
if files == [] do
raise Mix.Error, "No *.tokens.json files found under #{dir}"
end
{moved, touched} = Enum.reduce(files, {0, 0}, &process(&1, &2, config))
summary(moved, touched, mode)
end
defp ok_or_raise({:ok, value}, _message), do: value
defp ok_or_raise(:error, message), do: raise(Mix.Error, message)
defp process(file, {moved, touched}, %{dir: dir, group: group, keys: keys} = config) do
decoded = file |> File.read!() |> Jason.decode!()
{converted, count} = transform(config.direction, decoded, group, keys)
if converted == decoded do
{moved, touched}
else
write_or_report(file, converted, count, dir, config.mode, config.direction)
{moved + count, touched + 1}
end
end
defp transform(:nest, map, group, keys), do: Namespace.nest(map, group, keys)
defp transform(:unnest, map, group, keys), do: Namespace.unnest(map, group, keys)
defp verb(:nest), do: "nest"
defp verb(:unnest), do: "lift"
defp write_or_report(file, _converted, count, dir, :check, direction) do
Mix.shell().info(
"#{relative(file, dir)}: #{count} group(s) to #{verb(direction)}, refs to rewrite"
)
end
defp write_or_report(file, converted, count, dir, :write, direction) do
File.write!(file, Jason.encode!(converted, pretty: true) <> "\n")
Mix.shell().info(
"#{relative(file, dir)}: #{verb(direction)}ed #{count} group(s), rewrote refs"
)
end
defp relative(file, dir), do: Path.relative_to(file, Path.dirname(dir))
defp summary(moved, touched, :check) do
Mix.shell().info("\n#{moved} group(s) across #{touched} file(s) need rewriting")
if touched > 0, do: exit({:shutdown, 1})
end
defp summary(moved, touched, :write) do
Mix.shell().info("\nDone: rewrote #{moved} group(s) across #{touched} file(s)")
end
end