../work

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.

PrimitreeRecorded CLI runs
Layer rule

meaningbase

Semantic tokens must reference the base layer.

Source tokentokens.json
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
semantic.action contains #33CCFF directly, without referencing color.cyan.
primitree buildexit 1
Build stopped
PT1003semantic.action

Layer 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.

primitree.config.ts
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"
        ]
      }
    }
  }
}
tokens.json · Literal color
{
  "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"
      }
    }
  }
}
primitree check --format json · exit 1
{
  "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
  }
}
primitree build · exit 1
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.

Output recorded from the real CLI. The browser switches between those two results.

Primitree 1.0.0-next.1
My role
Creator & Maintainer
Timeline
2025 - Present
Built with
TypeScriptDTCG 2025.10Node.js 24TurborepoFigma Variables APIMCPReact 19
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
typescript
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-breaking exits 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

The configured pipeline produces four output 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:

  • 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 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