How DESIGN.md Token Reference Resolution Works: A Deep Dive into the google-labs-code/design.md CLI
DESIGN.md token references use the syntax {path.to.token} and are resolved through a three-phase process involving primitive parsing, chained reference resolution with cycle detection, and component property binding, all orchestrated by the ModelHandler class.
The google-labs-code/design.md repository implements a declarative design token system where YAML front-matter can reference previously defined values using curly-brace notation. Understanding this resolution mechanism is essential for debugging circular dependencies and extending the CLI linter. The entire pipeline lives in the TypeScript source code and enforces strict validation rules to prevent infinite recursion.
The Three-Phase Resolution Pipeline
The ModelHandler class in packages/cli/src/linter/model/handler.ts processes token references through a sequential pipeline that transforms raw strings into resolved objects.
Phase 1: Primitive Token Parsing
The handler first iterates over top-level token groups—colors, typography, rounded, and spacing—to build an initial symbol table. Primitive values such as hex colors or dimensions are parsed using dedicated functions like parseColor and parseDimension, then inserted as resolved objects. Reference strings wrapped in curly braces are inserted as-is for subsequent processing.
This initial build occurs in lines 54-124 of handler.ts, where the system populates both the symbol table and type-specific maps (colors, spacing, etc.).
Phase 2: Chained Reference Resolution
Once primitives are stored, the system invokes resolveReference(symbolTable, path, visited, depth) to dereference any remaining token strings. This recursive function:
- Validates that
depthdoes not exceedMAX_REFERENCE_DEPTH(default 10) - Detects circular references using a
visitedSet - Looks up the current path in the symbol table
- Recurses if the looked-up value is another reference
- Returns the final resolved object or
nullon failure
The resolution logic spans lines 73-96 in handler.ts. When a chain resolves successfully, lines 128-176 update the symbol table and token maps with the concrete values (e.g., ResolvedColor or ResolvedDimension).
Phase 3: Component Property Resolution
Component definitions reference tokens in their property values. The handler walks each component and checks if property values match the reference pattern using isTokenReference(). Numbers and booleans pass through unchanged, while valid references trigger resolveReference() against the fully populated symbol table.
Failures during this phase are recorded in the unresolvedRefs array for reporting, while successful resolutions produce ComponentDef objects containing ready-to-use token instances.
Configuration Limits and Safety Guards
The system prevents runaway recursion through configurable constants defined in packages/cli/src/linter/spec-config.ts (lines 24-27) and loaded via getSpecConfig().
Reference Depth Limits
MAX_REFERENCE_DEPTH caps reference chains at 10 hops by default. If resolveReference() exceeds this depth, it returns null and the model records a finding with severity 'error'.
Token Nesting Constraints
MAX_TOKEN_NESTING_DEPTH (default 20) limits how deeply objects can nest in the YAML tree. The forEachLeaf utility enforces this constraint during the initial parsing phase to maintain predictable traversal performance.
Token Reference Syntax and Constraints
A valid reference must follow the exact pattern {path.to.token} where the path uses dot notation to traverse the token hierarchy. For example:
colors:
primary: "#1A1C1E"
accent: "{colors.primary}"
The resolver expects terminal paths to point at primitive tokens. Composite values may only be referenced within the components section according to the specification documented in docs/spec.md (lines 86-88).
Practical Resolution Examples
Resolving Simple Color References
Given a DESIGN.md file with mutual references:
colors:
primary: "#1A1C1E"
accent: "{colors.primary}"
The CLI executes the following resolution:
// Phase 1: Primitive parsing
symbolTable.set('colors.primary', resolvedPrimary); // ResolvedColor object
symbolTable.set('colors.accent', '{colors.primary}'); // Raw reference string
// Phase 2: Chained resolution
const resolved = resolveReference(symbolTable, 'colors.primary', new Set());
// Returns the ResolvedColor object for "#1A1C1E"
symbolTable.set('colors.accent', resolved);
The accent key now points to the same ResolvedColor instance as primary, enabling memory-efficient token reuse.
Detecting Circular Reference Chains
Circular dependencies cause immediate resolution failures:
colors:
a: "{colors.b}"
b: "{colors.c}"
c: "{colors.a}" # Creates a cycle
The resolver detects the cycle when resolveReference() encounters 'colors.a' in the visited Set during recursion. It returns null and generates a finding:
{
"severity": "error",
"path": "colors.a",
"message": "Reference could not be resolved (missing or circular)."
}
Binding Tokens to Component Properties
Components resolve references during the final phase:
components:
card:
backgroundColor: "{colors.primary}"
padding: "{spacing.md}"
The handler processes these properties:
if (isTokenReference(rawValue)) {
const refPath = rawValue.slice(1, -1); // Extract "colors.primary"
const resolved = resolveReference(symbolTable, refPath, new Set());
if (resolved !== null) {
properties.set(propName, resolved);
} else {
unresolvedRefs.push(rawValue);
}
}
The resulting ComponentDef contains fully resolved objects rather than raw strings, ready for downstream generators like Tailwind config exporters.
Summary
- DESIGN.md uses
{path.to.token}syntax for references, resolved by theModelHandlerclass inpackages/cli/src/linter/model/handler.ts. - Resolution follows three phases: primitive parsing, chained reference resolution with cycle detection, and component property binding.
- Safety limits:
MAX_REFERENCE_DEPTH(default 10) prevents infinite recursion, whileMAX_TOKEN_NESTING_DEPTH(default 20) restricts YAML complexity. - The
resolveReference()function uses avisitedSet to detect cycles and returnsnullfor broken chains, which are recorded in thefindingsarray. - Component properties undergo the same resolution logic against the fully populated symbol table, producing
ComponentDefobjects with concrete token values.
Frequently Asked Questions
What is the maximum depth for token references in DESIGN.md?
The default maximum reference depth is 10 hops, controlled by the MAX_REFERENCE_DEPTH constant in spec-config.ts. Exceeding this limit causes the resolver to return null and emit an error finding, preventing stack overflow errors from deeply chained references.
How does the resolver detect circular references?
The resolveReference() function accepts a visited Set that tracks all paths encountered during the current resolution chain. Before recursing into a new reference, it checks if the target path already exists in the Set. If found, the function returns null immediately and reports a circular dependency error.
Can component properties reference tokens from any group?
Yes, component properties can reference any token previously loaded into the symbol table, including colors, typography, spacing, and rounded tokens. The resolution occurs during Phase 3 after all token groups have been processed, ensuring the full symbol table is available for lookups.
What happens when a token reference cannot be resolved?
Unresolved references—whether from missing definitions, circular dependencies, or depth limit violations—cause resolveReference() to return null. During component processing, these failures are added to the unresolvedRefs array. The ModelHandler aggregates all such errors into the findings result, which the CLI linter surfaces as validation errors with specific path information.
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 →