Packages

Compile-time design token library for Elixir

Current section

Files

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

lib/mix/tasks/jetons.figma_refs.ex

defmodule Mix.Tasks.Jetons.FigmaRefs do
@moduledoc """
Rewrites Figma `aliasData` literals into Jetons `{ref}` references.
Figma DTCG exports bake a fully-resolved colour into every `$value` and keep
the alias only under `$extensions["com.figma.aliasData"]`. This task walks all
`*.tokens.json` files under DIR and replaces each aliased token's `$value`
with a `{dot.path}` reference (e.g. `hue/300` -> `{hue.300}`), so the CSS
transformer can emit cascading `var()` references and re-resolve tokens across
modifier contexts (hue/tone) rather than freezing whatever value was active at
export time.
Tokens without `aliasData` (raw primitives, alpha=0 variants, opacity-derived
values) are left untouched. The task is idempotent and only rewrites files it
actually changes, so re-exported sources can be re-run safely.
## Usage
mix jetons.figma_refs path/to/tokens
mix jetons.figma_refs path/to/tokens --check
## Options
* `--check` - report what would change without writing; exits non-zero if any
conversions are still needed (handy in CI / pre-commit)
"""
use Mix.Task
alias Jetons.Figma
@shortdoc "Rewrite Figma aliasData literals into {ref} references"
@switches [check: :boolean]
@impl true
def run(args) do
{opts, paths} = OptionParser.parse!(args, strict: @switches)
dir = Path.expand(List.first(paths) || ".")
check? = Keyword.get(opts, :check, false)
files = Path.wildcard(Path.join(dir, "**/*.tokens.json"))
if files == [] do
raise Mix.Error, "No *.tokens.json files found under #{dir}"
end
{total, touched} = Enum.reduce(files, {0, 0}, &process(&1, &2, dir, check?))
summary(total, touched, check?)
end
defp process(file, {total, touched}, dir, check?) do
{converted, count} = file |> File.read!() |> Jason.decode!() |> Figma.to_refs()
cond do
count == 0 ->
{total, touched}
check? ->
Mix.shell().info("#{relative(file, dir)}: #{count} alias(es) to convert")
{total + count, touched + 1}
true ->
File.write!(file, Jason.encode!(converted, pretty: true) <> "\n")
Mix.shell().info("#{relative(file, dir)}: converted #{count} alias(es)")
{total + count, touched + 1}
end
end
defp relative(file, dir), do: Path.relative_to(file, Path.dirname(dir))
defp summary(total, touched, true) do
Mix.shell().info("\n#{total} alias(es) across #{touched} file(s) need converting")
if total > 0, do: exit({:shutdown, 1})
end
defp summary(total, touched, false) do
Mix.shell().info("\nDone: #{total} alias(es) converted across #{touched} file(s)")
end
end