How DESIGN.md Token Reference Resolution Works: The `{path.to.token}` Syntax Explained
DESIGN.md resolves token references using a three-phase algorithm that first parses primitive values into a symbol table, then recursively resolves chained references with cycle detection and depth limits, and finally substitutes concrete values into component definitions.
The google-labs-code/design.md CLI processes design tokens defined in YAML front-matter, where values can reference other tokens using the {path.to.token} syntax. This resolution mechanism allows design systems to create reusable, chained token values while preventing infinite loops and handling unresolved references.
The Three Phases of Token Resolution
The resolver operates sequentially through three distinct phases implemented in packages/cli/src/linter/model/handler.ts.
Phase 1: Initial Primitive Parsing
During the first pass, the ModelHandler.execute method (lines 55-74) parses all non-reference values and populates a symbol table. Primitive values such as colors, typography, and dimensions are stored as resolved objects, while raw reference strings remain unprocessed.
// From handler.ts lines 55-74
symbolTable.set(`colors.${name}`, resolved); // primitive color stored as object
symbolTable.set(`rounded.${name}`, raw); // raw reference string stored as-is
This symbol table serves as the lookup registry for all subsequent resolution steps.
Phase 2: Chained Reference Resolution
After primitive parsing, the system resolves raw reference strings using the resolveReference function (lines 73-96 of handler.ts). This recursive algorithm handles chained references—where one token points to another—until a concrete value is reached.
function resolveReference(symbolTable, path, visited, depth = 0) {
if (depth > MAX_REFERENCE_DEPTH) return null; // depth limit guard
if (visited.has(path)) return null; // circular reference guard
visited.add(path);
const value = symbolTable.get(path);
if (typeof value === 'string' && isTokenReference(value)) {
const innerPath = value.slice(1, -1); // strip curly braces
return resolveReference(symbolTable, innerPath, visited, depth + 1);
}
return value;
}
The resolver implements two critical safety mechanisms:
- Depth limiting via
MAX_REFERENCE_DEPTHto prevent infinite recursion - Cycle detection using a
visitedSet to catch circular token references
Phase 3: Component Construction
In the final phase (lines 84-101 of handler.ts), component definitions are processed. Each property containing a token reference is evaluated using resolveReference. If resolution fails, the reference is recorded for error reporting.
// From handler.ts lines 84-101
if (isTokenReference(rawValue)) {
const refPath = rawValue.slice(1, -1);
const resolved = resolveReference(symbolTable, refPath, new Set());
if (resolved !== null) properties.set(propName, resolved);
else unresolvedRefs.push(rawValue); // track unresolvable references
}
Recursive Resolution Algorithm and Safety Guards
The resolveReference function in packages/cli/src/linter/model/handler.ts follows a strict recursive pattern to traverse token chains. When encountering a reference string like {colors.primary-60}, the algorithm strips the curly braces and queries the symbol table for the inner path. If the retrieved value is another reference string, the function calls itself with an incremented depth counter.
The circular guard uses a visited Set that tracks all paths encountered during the current resolution chain. If a path appears twice in the same chain, the function returns null immediately. Similarly, exceeding MAX_REFERENCE_DEPTH triggers an early termination, ensuring the CLI cannot enter infinite loops through malformed token definitions.
Practical Example: Resolving Component Tokens
Consider this DESIGN.md front-matter excerpt:
---
colors:
primary-60: "#0D47A1"
primary-20: "#1976D2"
typography:
label-md:
fontFamily: Public Sans
fontSize: 14px
components:
button-primary:
backgroundColor: "{colors.primary-60}"
textColor: "{colors.primary-20}"
typography: "{typography.label-md}"
---
The resolution process follows this sequence:
- Parse primitives:
colors.primary-60,colors.primary-20, andtypography.label-mdare parsed into concrete objects and stored in the symbol table. - Resolve references:
button-primary.backgroundColortriggersresolveReference(symbolTable, "colors.primary-60", new Set()), which returns the color object#0D47A1. - Construct model: The component object is populated with fully resolved values, ready for export to Tailwind CSS or DTCG format.
Implementation Files and Specification Reference
The token reference behavior is formally defined in two key locations:
docs/spec.md(lines 86-87): Specifies that token references must be wrapped in curly braces and point to primitive values, except within thecomponentssection where composite values are permitted.packages/cli/src/linter/model/handler.ts: Contains the complete resolution implementation, includingModelHandler.execute, theresolveReferencerecursive function, and the component construction logic.
According to the specification in docs/spec.md, the {path.to.token} syntax requires the inner path to resolve to a primitive design token value, ensuring type consistency across the design system.
Summary
- Three-phase resolution: Primitive parsing populates the symbol table, chained references are resolved recursively, and components are constructed with concrete values.
- Safety guards: The
resolveReferencefunction usesMAX_REFERENCE_DEPTHand avisitedSet to prevent infinite loops and detect circular dependencies. - Unresolvable tracking: Failed references are collected in
unresolvedRefsfor diagnostic reporting during the CLI linting process. - Spec compliance: The official specification in
docs/spec.mddefines the syntax constraints and value requirements for token references.
Frequently Asked Questions
What is the maximum depth for token reference chains in DESIGN.md?
The resolution algorithm enforces a maximum depth limit through the MAX_REFERENCE_DEPTH constant defined in packages/cli/src/linter/model/handler.ts. While the exact numeric value is implementation-dependent, the guard prevents infinite recursion by terminating resolution attempts that exceed this threshold, returning null for excessively deep chains.
How does DESIGN.md detect circular token references?
Circular references are detected using a visited Set that tracks all paths encountered during the current resolution chain. In the resolveReference function (lines 73-96 of handler.ts), if a path already exists in the visited Set, the function immediately returns null, breaking the cycle and preventing infinite recursion.
Where is the official token reference syntax defined?
The official specification for the {path.to.token} syntax appears in docs/spec.md at lines 86-87. This document specifies that references must be wrapped in curly braces and typically resolve to primitive values, with the exception of component definitions where composite values are permitted.
What happens when a token reference cannot be resolved?
When resolveReference returns null for a reference—either due to a missing token, circular dependency, or depth limit violation—the unresolved reference string is pushed to the unresolvedRefs array (lines 84-101 of handler.ts). The CLI subsequently reports these unresolved references to help developers identify missing or invalid token definitions.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →