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