function
<Component />
return
const
interface
{ }
color.primary
TS
() => {}
spacing.xl
dev
tokens
[ ]
code
<T>
type
async
font.sans
JS
TypeScript
export
[React, ...skills]
components
import
border.radius
props
default
?.
tokens
shadow.lg
await
&&
class
<Props>
accessibility
variables
transition
JSX
</>
theme
motion
( )
hooks
state
utils
wcag
responsive
grid
flex
scale
{ml}
Back to work
DEVELOPER TOOLS

Primitree

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.

Role
Creator & Maintainer
Timeline
2025 - Present
Technologies
TypeScriptDTCG 2025.10Node.js 24TurborepoFigma Variables APIMCPReact 19

Impact

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

Published to npm as a release candidate, approaching 1.0

The Problem

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.

  • A semantic token quietly gains a literal value and nothing objects
  • A rename in Figma reaches code as a deletion plus an addition, losing the history
  • Generated token files get hand-edited, then overwritten by the next build
  • Nobody can answer which components break if a base token changes
  • Ownership of a token is a Slack thread rather than a property of the token

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.

The Solution

Primitree makes token architecture a declared contract that a build can check, rather than a convention people are asked to remember.

Architecture as Configuration

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.

Diffs That Preserve Identity

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:

  • Renames, moves, removals, and type changes are classified as breaking
  • --fail-on-breaking exits with code 2 so CI can gate on it
  • Reports name the affected aliases, not just the changed token
  • Output as Markdown for review, or JSON for tooling

Generated Files That Defend Themselves

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

One Source, Four Outputs

  • 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

Tokens an Agent Can Query

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.

Technical Implementation

Standards Output, Documented Extensions

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:

  • Aliases stay token references instead of being flattened to values
  • Figma modes map to Resolver contexts
  • Figma IDs, scopes, and code syntax live under $extensions['com.primitree']
  • Booleans use a documented extension, since DTCG has no boolean type

Writes Treated as Transactions

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:

  • 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

Bounded Inputs

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.

Monorepo Shape

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 clients

A Figma plugin exports local variables for teams without Enterprise API access, so the pipeline does not require a plan tier to start using.

Status & Direction

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.

© 2026 Mark Learst.Crafted with precision
privacy
v2026.1.0