Primitree
A design token pipeline that checks architecture rules and generates CSS, Tailwind, and TypeScript.
When a semantic token holds a literal color
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.
When a semantic layer requires references, a token holding a literal color produces a build failure that identifies the token path.
meaningbase
Semantic tokens must reference the base layer.
semantic.action{
"$type": "color",
"$value": {
"colorSpace": "srgb",
"components": [
0.2,
0.8,
1
],
"hex": "#33CCFF"
}
}- Semantic token
semantic.action- Not referenced
color.cyan- Color value
#33CCFF
primitree buildexit 1semantic.actionLayer meaning does not allow this token value form.
No generated files were written.
Inspect configuration and full output
The two runs use the same configuration. Only semantic.action.$value changes.
export default {
"schemaVersion": 1,
"sources": {
"brand": {
"type": "dtcg",
"file": "./tokens.json",
"architecture": {
"layers": [
{
"id": "base",
"roots": [
"color"
],
"values": "literal"
},
{
"id": "meaning",
"roots": [
"semantic"
],
"values": "reference",
"references": [
"base"
]
}
]
},
"ownership": {
"default": [
"design-systems"
]
},
"outputs": {
"directory": "./generated",
"formats": [
"css"
]
}
}
}
}
{
"color": {
"cyan": {
"$type": "color",
"$value": {
"colorSpace": "srgb",
"components": [
0.2,
0.8,
1
],
"hex": "#33CCFF"
}
}
},
"semantic": {
"action": {
"$type": "color",
"$value": {
"colorSpace": "srgb",
"components": [
0.2,
0.8,
1
],
"hex": "#33CCFF"
}
}
}
}
{
"schemaVersion": 1,
"command": "check",
"source": "brand",
"findings": [
{
"findingId": "PT1003:source%3Abrand%2Ftoken%3Asemantic.action",
"ruleId": "PT1003",
"tokenId": "source:brand/token:semantic.action",
"path": [
"semantic",
"action"
],
"message": "Layer meaning does not allow this token value form.",
"owners": [
"design-systems"
],
"disposition": "active",
"layerId": "meaning"
}
],
"summary": {
"active": 1,
"baseline": 0
}
}PT1003 semantic.action: Layer meaning does not allow this token value form.
Build stopped with 1 active finding for source "brand".
Literal color: Build stopped. PT1003: semantic.action must use a reference. No generated files were written.
In this case study
From Figma Variables to build rules
Token rules need to survive changes to the system: which layers hold raw values, which must use references, who owns each token, and which aliases a change affects. I created Primitree to put those rules in configuration that a build can check.
Primitree grew out of @figma-vars/hooks, which solved reading Figma Variables into React. The pipeline now checks architecture rules, reports changes, and generates output across six packages.
Configuration and change checks
Configuration for four output formats
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'],
},
},
},
})Figma-export diffs match on stable Figma IDs rather than names, so a rename stays a rename instead of appearing as an unrelated deletion and addition.
Configured DTCG diffs report changed tokens, affected aliases, and policy findings.
- Renames, moves, removals, and type changes are classified as breaking
--fail-on-breakingexits with code 2 so CI can gate on it- Output as Markdown for review, or JSON for tooling
Running with --check reports missing, changed, and unexpected
files without writing anything, and exits 1 on any difference. That is the
CI check for whether committed tokens match what the source produces.
Generated files need an ownership record
Configured DTCG builds write a manifest beside their 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.
Configured DTCG builds stage output before replacing files and retain recovery information when a run is interrupted.
Output follows DTCG 2025.10. Aliases stay token references instead of being flattened to values, and Figma modes map to Resolver contexts. Figma-specific information that the specification does not represent goes into a namespaced extension.
Figma IDs, scopes, and code syntax live under
$extensions['com.primitree']. Booleans use a documented
extension, since DTCG has no boolean type.
Output formats and file-safety checks
Generated formats
- DTCG 2025.10 token files plus a resolver document for contexts
- CSS custom properties, with contexts as selector axes
- Tailwind theme output
- Typed TypeScript token exports
Transactional writes
- Output staged before it replaces anything installed
- An interrupted run leaves its backup and cleanup sidecars in place, reports every retained path, and stops rather than writing over them
- The output directory must sit under the config directory and cannot contain the source file
- File identity is recorded before a read and compared after, so a path swapped mid-read is rejected instead of trusted
- The output directory and its ancestors are rechecked during a scan, which stops if one changes underneath
Input limits
Input limits cover 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.
Six packages, still a prerelease
Primitree is published to npm as 1.0.0-next.1, a release
candidate. API stability remains a focus before 1.0.
A Figma plugin exports local variables for teams without Enterprise API access, so the pipeline does not require a plan tier to start using.
Six packages are published as prereleases over a shared core, built with Turborepo on Node 24 and pnpm 11:
primitreefor the unscoped CLI entry point@primitree/corefor normalization, the token graph, diffing, and API access@primitree/dtcgfor conversion and the CSS, Tailwind, and TypeScript emitters@primitree/clifor check, build, inspect, diff, and scaffolding@primitree/hooksfor reading built tokens in React, or the live API with SWR@primitree/mcpfor serving the graph to MCP clients
An MCP server exposes the token graph through
list_collections, get_token,
resolve_context, search_tokens, and
diff_tokens. MCP clients can read token data, resolve
contexts, and inspect changes through those tools.
Project notes
6 packages
CLI, token graph, DTCG emitters, React hooks, and an MCP server
DTCG 2025.10
Standards-compliant output with a documented boolean extension
1.0.0-next.1
Published npm prerelease; API stability remains a focus before 1.0