DTCG token pipeline that turns design system architecture into a checkable contract. Six packages covering a CLI, token graph, emitters for CSS, Tailwind and TypeScript, React hooks, and an MCP server.
CLI, token graph, DTCG emitters, React hooks, and an MCP server
Standards-compliant output with a documented boolean extension
Published to npm as a release candidate, approaching 1.0
Every design system of any size has rules about its tokens. Base tokens hold raw values. Semantic tokens reference base tokens and never hold a hex code of their own. Component tokens point at semantic tokens. These rules are what keep a token system from collapsing into a flat list of names.
Almost nowhere are those rules enforced. They live in a wiki page, a Figma cover frame, or one person's head.
Primitree grew out of @figma-vars/hooks, which solved reading Figma Variables into React. The hooks package is now one of six, because the harder problem turned out to be everything around the read: what the tokens are allowed to be, and what happens when they change.
Primitree makes token architecture a declared contract that a build can check, rather than a convention people are asked to remember.
A source declares its layers. Each layer names the roots it owns, whether it holds literals or references, and which layers it is allowed to point at:
export default defineConfig({
schemaVersion: 1,
sources: {
brand: {
type: 'dtcg',
file: './tokens.json',
architecture: {
layers: [
// holds raw values
{ id: 'base', roots: ['color'], values: 'literal' },
// must point at base, never hold a value
{
id: 'meaning',
roots: ['semantic'],
values: 'reference',
references: ['base'],
},
],
},
ownership: { default: ['design-systems'] },
outputs: {
directory: './generated',
formats: ['dtcg', 'css', 'typescript', 'tailwind'],
},
},
},
})A semantic token holding a hex value is now a build failure with a token path attached, not a code review someone has to catch.
Token diffs match on stable Figma IDs rather than names, so a rename stays a rename instead of appearing as an unrelated deletion and addition:
--fail-on-breaking exits with code 2 so CI can gate on itEvery build writes a manifest beside its output. The next build reads it and refuses to overwrite a listed file whose hash changed, or to touch any path the manifest does not list. Hand edits to generated files surface as an error instead of vanishing.
Running with --check reports missing, changed, and unexpected
files without writing anything, and exits 1 on any difference. That is the
CI shape: prove the committed tokens match what the source produces.
An MCP server exposes the token graph to any MCP client with
list_collections, get_token,
resolve_context, search_tokens, and
diff_tokens, so an assistant can answer questions about the
design system from the tokens themselves rather than from guesswork.
Output follows DTCG 2025.10. Where Figma carries information the specification has no home for, it goes into a namespaced extension rather than a private convention:
$extensions['com.primitree']A token build rewrites a directory of files, which is exactly the kind of operation that goes wrong halfway through. The writer is built to fail safely:
Every input has a stated ceiling instead of an implicit one: file sizes, token file counts, directory entries, nesting depth, path lengths in UTF-8 bytes, and path component counts after symlink resolution. Symbolic links and special files are rejected outright. A parser that reads whatever it is handed is a parser waiting to be handed something hostile.
Six published packages over a shared core, built with Turborepo on Node 24 and pnpm 11:
primitree for the unscoped CLI entry point@primitree/core for normalization, the token graph, diffing, and API access@primitree/dtcg for conversion and the CSS, Tailwind, and TypeScript emitters@primitree/cli for check, build, inspect, diff, and scaffolding@primitree/hooks for reading built tokens in React, or the live API with SWR@primitree/mcp for serving the graph to MCP clientsA Figma plugin exports local variables for teams without Enterprise API access, so the pipeline does not require a plan tier to start using.
Primitree is published to npm as 1.0.0-next and is a release
candidate rather than a finished 1.0. What is settled:
Architecture rules are checkable, so layer and ownership violations fail a build instead of surviving review
Token identity survives renames, which makes a diff report something a reviewer can trust
Generated output is guarded by a manifest, so builds cannot silently erase hand edits
One source produces four formats, removing the usual pile of bespoke conversion scripts
The graph is queryable over MCP, so coding agents can read the real design system
The work left before 1.0 is API stability rather than capability. Publishing under a prerelease tag keeps that promise honest while the surface settles.