@v4/token-parser
The v4-tokens CLI that turns a Token Studio export into typed tokens, CSS and icons, plus the Figma comparison scripts.
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
- Parse (
parse.ts) — walks Token Studio sets (skips$themes/$metadata), supportsvalue/typeand$value/$typeleaves, typed composites (typography, boxShadow). - 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 anunsupported-modifierdiagnostic. The maths, and why grepping the export for a hex proves nothing, are on The modifier maths. - 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. - Normalise (
normalise.ts) — collapses redundantP/S/CSwrappers, camelCases segments, maps spellings per the build plan (ColorSet-1→colourSet1,Base-1→base1), detects post-normalisation collisions. - Emit (
emit.ts) — typedas constTypeScript (tokens.ts+Tokenstype) and flat--ph-*CSS custom properties from the same resolved graph. - 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 nobadgenamespace), and component themes bind them throughg.components.*on the global contract. - Drift check — done two ways: CI regenerates and
git diff --exit-codes the committed output, andv4-tokens driftreports design-side staleness. /generate-token-contracts— shipped as a skill that drives this loop with themodifytraps surfaced.satisfies ClubTokenOverridesscaffolding — not built; club packages (here@v4/tokens-euro-league) feedcreateThemethrough an adapter instead.