How to Debug Why a Token Reference Isn't Resolving in the Design MD CLI
To debug an unresolved token reference in the Design MD CLI, verify the syntax with isTokenReference, inspect the symbolTable for the target key, and trace the resolveReference function in packages/cli/src/linter/model/handler.ts to detect missing keys, circular references, or depth limit violations.
The Design MD CLI from the google-labs-code/design.md repository processes design-token references like {colors.brand} through a multi-phase resolution pipeline. When a token reference isn't resolving, the issue typically stems from syntax errors, missing symbol table entries, or circular dependencies in the token chain. Understanding how the CLI resolves these references allows you to pinpoint exactly where the resolution breaks down.
Understanding the Three-Phase Resolution Pipeline
The CLI resolves design-token references in three distinct phases:
- Primitive token parsing – Raw values are stored in a
symbolTablealongside any already-resolved values. - Chained reference resolution – The CLI walks the symbol table, replacing token-reference strings with concrete values using the
resolveReferencefunction (lines 73-98 inpackages/cli/src/linter/model/handler.ts). - Component construction – Component properties are populated with resolved values, and any reference that could not be resolved is recorded in the
unresolvedRefsarray (lines 84-101).
Common Root Causes of Unresolved Token References
When resolveReference returns null, it typically indicates one of these five issues:
Invalid token syntax – If isTokenReference returns false, the string lacks proper curly braces {} and is stored unchanged in the symbol table, bypassing resolution entirely.
Non-existent target entry – When symbolTable.get(path) returns null inside resolveReference, the reference path does not exist in the symbol table, causing the token to be added to unresolvedRefs.
Circular reference – The cycle detection mechanism (visited.has(path)) identifies when a token references itself through a chain, returning null and leaving the original reference unresolved.
Depth limit exceeded – The MAX_REFERENCE_DEPTH guard (defined in packages/cli/src/linter/spec-config.ts at line 31) aborts resolution when chained references exceed the default limit of 20 levels.
Type mismatch – After resolution, the caller verifies resolved.type (e.g., lines 133-137 for color validation). If the resolved token type does not match the expected property type, the value is ignored and the reference remains unresolved.
Step-by-Step Debugging Guide
Follow these steps to isolate why a specific token reference is failing:
- Confirm the token syntax
Verify that your token uses proper curly-brace notation:
import { isTokenReference } from './packages/cli/src/linter/model/spec.js';
const token = '{colors.brand}';
console.log(isTokenReference(token)); // Must return true
If isTokenReference returns false, the CLI will never attempt resolution.
- Inspect the symbol table after Phase 1
Insert a temporary debug statement after the primitive parsing loops (around line 74 in handler.ts):
console.log('Symbol Table:', symbolTable);
Look for entries like colors.brand → '{colors.base}' (indicating a chained reference) or a concrete value.
- Check that the target key exists
Extract the path and verify its presence:
const path = token.slice(1, -1); // "colors.brand"
console.log(symbolTable.has(path)); // Must return true
- Run the resolver manually
Test the resolution logic directly:
import { resolveReference } from './packages/cli/src/linter/model/handler.js';
const resolved = resolveReference(symbolTable, 'colors.brand', new Set());
console.log('Resolved:', resolved);
If resolved is null, the key is missing, a cycle was detected, or the depth limit was exceeded.
- Detect cycles or deep chains
Monitor the visited Set while stepping through resolveReference. For depth issues, check if your chain exceeds MAX_REFERENCE_DEPTH (20 levels by default). Increase this constant in spec-config.ts only after confirming the design intent.
- Verify the final type
Ensure the resolved token type matches the consumer's expectation. If you expect a color but receive a dimension, the handler will skip the value (see lines 133-137 in handler.ts).
- Read the unresolvedRefs array
After model construction, inspect the unresolvedRefs array populated at line 98:
console.log('Unresolved:', component.unresolvedRefs);
This array contains all references that survived the resolution pipeline.
- Run the unit tests
Execute the test suite in packages/cli/src/linter/model/handler.test.ts (specifically lines 225-282) to verify resolution behavior against known examples:
npm test
Typical Pitfalls to Avoid
Watch for these common mistakes that prevent token resolution:
- Whitespace or extra characters – The
isTokenReferencefunction requires exact{...}syntax without surrounding spaces. - Case-sensitivity – Keys are case-sensitive;
colors.Branddoes not matchcolors.brand. - Missing section prefix – References must include the full path including section prefixes like
colors.orspacing..
Quick Sanity Check Script
Run this standalone script inside the repository root to verify token resolution:
import { readFileSync } from 'fs';
import { isTokenReference } from './packages/cli/src/linter/model/spec.js';
import { resolveReference } from './packages/cli/src/linter/model/handler.js';
// Assuming symbolTable is built from your DESIGN.md or token JSON
const token = '{colors.brand}';
if (!isTokenReference(token)) {
console.error('Token syntax invalid');
} else {
const path = token.slice(1, -1);
const resolved = resolveReference(symbolTable, path, new Set());
console.log('Resolved value:', resolved);
}
Key Source Files for Reference
Understanding these files accelerates debugging:
packages/cli/src/linter/model/handler.ts– Contains theresolveReferencefunction andunresolvedRefslogic.packages/cli/src/linter/model/spec.ts– DefinesisTokenReferenceand validation helpers.packages/cli/src/linter/model/spec.test.ts– Tests for token reference detection.packages/cli/src/linter/model/handler.test.ts– Comprehensive tests for direct, chained, and circular references (lines 225-282).packages/cli/src/linter/spec-config.ts– HoldsMAX_REFERENCE_DEPTHand resolution limits.
Summary
- The Design MD CLI resolves tokens through primitive parsing, chained resolution via
resolveReference, and component construction phases. - Unresolved references typically result from invalid syntax, missing symbol table entries, circular dependencies, depth limit violations, or type mismatches.
- Debug by verifying
isTokenReferencereturns true, checkingsymbolTable.has(path), and tracingresolveReferencefor null returns. - Inspect
unresolvedRefson components and consulthandler.test.tsfor expected resolution behavior. - Reference
spec-config.tsto adjustMAX_REFERENCE_DEPTHwhen dealing with deeply nested token chains.
Frequently Asked Questions
Why does my token reference remain as plain text in the output?
If a token appears as literal text like {colors.brand} instead of resolving to a value, isTokenReference likely returned false due to malformed syntax or extra whitespace. The CLI stores the string unchanged in the symbol table when it fails the curly-brace validation check defined in packages/cli/src/linter/model/spec.ts.
How do I fix a circular reference error?
Circular references occur when token A references token B, which eventually references token A again. The resolveReference function detects this using a visited Set and returns null. Break the cycle by editing your DESIGN.md file to remove the recursive dependency, ensuring tokens form a directed acyclic graph.
Can I increase the maximum depth for chained token references?
Yes. The default limit of 20 chained references is defined as MAX_REFERENCE_DEPTH in packages/cli/src/linter/spec-config.ts at line 31. Increase this constant only after confirming that your design system intentionally requires deeper nesting, as excessive chaining can indicate architectural issues.
Where does the CLI report which references failed to resolve?
The CLI populates the unresolvedRefs array on each component during the construction phase (lines 84-101 in packages/cli/src/linter/model/handler.ts). Inspect this array in your component output or test results to see exactly which token paths could not be resolved.
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 →