@v4/token-parser

The v4-tokens CLI that turns a Token Studio export into typed tokens, CSS and icons, plus the Figma comparison scripts.

2026-11-07Status: Implemented

Overview

Implemented — packages/token-parser, exposing the v4-tokens CLI. It parses a whole Token Studio export — primitive, semantic and colour-set tokens, with full CS → S → P reference resolution — into a typed tokens.ts and a tokens.css, and fails the build on bad data: two token names that collide after normalisation (link-visited vs Link-Visited) stop it rather than silently overwriting each other.

CLI surface

v4-tokens build -i <tokens.json> -o <out-dir> [-n <name>] [--emit ts,css]
v4-tokens check -i <tokens.json>        # validate only; non-zero exit on errors (CI gate)
v4-tokens icons -k <file-key> -o <dir> [--page "- Icons"] [--filter <substr>]
               [--nodes "<Figma name>@<node-id>[,…]"]
v4-tokens drift -k <file-key> --registry <figma.json>   # design-drift report
v4-tokens coverage -c <components-dir> [-n <component>] [--write]

The CLI runs from the built dist/ (bin: ./dist/cli.js), so a change in src/ does nothing until pnpm --filter @v4/token-parser build. Regenerating @v4/tokens then needs that package built too before Storybook sees new values.

build also emits per-component contract modules — every components.<figmaFile>.<group> namespace becomes components/<figmaFile>/<group>.ts (emit-components.ts; 11 group modules across five namespaces — core, content, football, video, gamification — 17 files with their index barrels, importable as @v4/tokens/components/core/form etc.; Component/Hospitality is an empty set in the export, so it emits nothing) — and a tokens.flat.json sidecar used to print a contract diff (+added/−removed/~changed) against the previous build on every run. drift compares the Figma file's latest version against the version stamped in each component's figma.json bridge. Transforms include percentage letter-spacing → em (0% → 0) and lowercase size abbreviations (XS → xs).

icons discovers icon component masters via the published-components endpoint (variant children filtered out), exports SVGs, and emits React icon components (currentColor, aria-hidden, typed IconProps) with a regenerated alphabetical barrel — replacing token-builder 1.x's SVGR step. --nodes names components explicitly instead of discovering them on a page — needed when the icon a design composes is itself a variant (the ELB footer's social glyphs). Wired into @v4/tokens as pnpm sync-icons (output: packages/tokens/src/icons/, imported as @v4/tokens/icons).

coverage recomputes each component manifest's binding coverage from its tokens/ extraction files (coverage.ts): the basis is node × property pairs with INSTANCE subtrees and hidden nodes excluded (coverageBasis: "node-property-pairs@v2", with hiddenNodePairsExcluded and hiddenSubtrees reported), a manifest's coverage.delegatedThemes points it at a shared _internal layout's theme, and the block also lists gaps, rollUps, evidence and the theme leaves no CSS reads (cssConsumption.unread). --write updates every tokens/_manifest.json. The rules that depend on it are in .claude/rules/frontend/component-architecture.md.

Pipeline

  1. Parse (parse.ts) — walks Token Studio sets (skips $themes/$metadata), supports value/type and $value/$type leaves, typed composites (typography, boxShadow).
  2. Resolve (resolve.ts) — cross-set reference index (paths without set names, matching Token Studio semantics), recursive resolution with cycle detection, token arithmetic ({P.Size.BaseUnit}*1.5 → computed value, calc() fallback for non-numeric bases), embedded references inside longer strings (gradients). Modifiers (modify.ts) — the brand Tints and Shades are a reference plus $extensions["studio.tokens"].modify (darken, lighten, alpha), applied with Token Studio's own maths so the output matches Figma's rendered pixels; an unsupported modifier or colour space fails the build with an unsupported-modifier diagnostic. The maths, and why grepping the export for a hex proves nothing, are on The modifier maths.
  3. Transform (transform.ts) — px suffixing for size-ish types (ported from token-builder 1.x), opacity % → decimal, font-weight aliases (SemiBold → 600), boxShadow layers → CSS strings (multi-layer + inset), typography composites → transformed objects.
  4. Normalise (normalise.ts) — collapses redundant P/S/CS wrappers, camelCases segments, maps spellings per the build plan (ColorSet-1 → colourSet1, Base-1 → base1), detects post-normalisation collisions.
  5. Emit (emit.ts) — typed as const TypeScript (tokens.ts + Tokens type) and flat --ph-* CSS custom properties from the same resolved graph.
  6. Report (report.ts) — diagnostics (unresolved refs, cycles, collisions, unknown types, suspicious names) + summary; errors fail the exit code.

Tested with vitest: pipeline.test.ts (23 tests over a purpose-built fixture), coverage.test.ts (25), modify.test.ts (17, including golden pairs against Figma's pixels), icons.test.ts and resolve-math.test.ts.

Figma scripts (the fork-refactor toolkit)

packages/token-parser/scripts/ holds the Node scripts that compare components with their Figma files. They read FIGMA_PAT from the root .env (read-env.mjs) — except check-bridges, which is offline.

Script What it does
check-bridges.mjs Offline gate (pnpm --filter @v4/token-parser check-bridges, run by scripts/verify.mjs): every component bridge names a file this repo is built from (.claude/manifest.json figma.files / figma.clientFiles), and every cached extraction agrees with its bridge
design-status.mjs --file <clientFileKey> [--ready] [--unbuilt] — the design and development status the client's Figma file records per component; --unbuilt lists finished designs with no component
fork-diff.mjs --upstream <ghostFileKey> --client <clientFileKey> [--only <component>] — diffs a component's Token Studio bindings between upstream Ghost and the client fork, ranked by measured difference
design-drift.mjs [--only <component>] [--gap 3000] [--state <file>] — has the client design moved since each extraction was cached? Re-extracts with the real extractor; reports UNMEASURED and NOT-COMPARABLE separately
extract-figma-bindings.mjs Walks a component's Figma tree and writes the extraction shape coverage reads, recording the fileKey and hidden nodes
extract-colour-ways.mjs Extracts a component's colour-way variants
export-figma-screenshots.mjs Exports reference PNGs into figma-screenshots/
verify-against-figma.mjs, verify-bindings.mjs Check a component's bindings and render against Figma

Why they exist is on Building against ELB's Figma forks; how to use them on a fork is Refactor against ELB's fork (rule: .claude/rules/frontend/fork-refactor.md). /fork-status runs design-status and fork-diff together.

Status of the original roadmap

  • Per-component contract slicing — done: components.* namespaces emit typed modules under @v4/tokens/components/*. They are per design-system namespace, not per UI component (there is no badge namespace), and component themes bind them through g.components.* on the global contract.
  • Drift check — done two ways: CI regenerates and git diff --exit-codes the committed output, and v4-tokens drift reports design-side staleness.
  • /generate-token-contracts — shipped as a skill that drives this loop with the modify traps surfaced.
  • satisfies ClubTokenOverrides scaffolding — not built; club packages (here @v4/tokens-euro-league) feed createTheme through an adapter instead.