What Happens When a Token Reference in DESIGN.md Is Broken?
When a token reference in DESIGN.md cannot be resolved, the linter emits a specific error diagnostic, omits the broken reference from the concrete design system state, and preserves the raw token string in the affected component properties while tracking it in an unresolved references array.
The google-labs-code/design.md repository provides a CLI linter that parses DESIGN.md files into a structured DesignSystemState. Understanding how this linter handles broken token references is essential for debugging design system configurations and maintaining valid token resolution chains across your components.
How the Linter Detects and Resolves Token References
The core resolution logic resides in packages/cli/src/linter/model/handler.ts, where the linter builds a symbol table of all token definitions before attempting to resolve cross-references during the model phase.
Token Detection Phase
During parsing, every leaf value undergoes inspection via isTokenReference(), which checks for the {section.token} pattern. When the linter encounters a string matching this pattern in sections like colors, spacing, or rounded, it stores the raw reference in a symbol table for deferred resolution:
if (typeof raw === 'string' && isTokenReference(raw)) {
// Store raw reference for later resolution
symbolTable.set(`colors.${name}`, raw);
}
Source: packages/cli/src/linter/model/handler.ts lines 58-60.
Reference Resolution with Chain Following
After collecting primitive tokens, the linter walks the symbol table and attempts resolution via resolveReference(). This function follows chains of references while detecting circular paths and respecting the MAX_REFERENCE_DEPTH limit to prevent infinite recursion:
const resolved = resolveReference(symbolTable, raw.slice(1, -1), new Set());
if (resolved !== null && typeof resolved === 'object' && 'type' in resolved …
Source: packages/cli/src/linter/model/handler.ts lines 31-35.
Three Outcomes of a Broken Token Reference
When resolveReference() returns null—indicating the target token does not exist—the linter triggers a specific failure sequence with three distinct outcomes that ensure graceful degradation.
1. Omission from the Concrete Token Map
If resolution fails, the token entry is not promoted to the resolved maps (colors, spacing, or rounded). The raw reference remains in the internal symbol table but never reaches the final DesignSystemState concrete values, effectively excluding undefined tokens from the compiled design system output.
2. Literal String Preservation in Components
Components referencing broken tokens receive the raw reference string unchanged rather than a resolved value. The linter pushes the broken reference into the component's unresolvedRefs array while storing the literal string in the properties map:
const resolved = resolveReference(symbolTable, refPath, new Set());
if (resolved !== null) {
properties.set(propName, resolved);
} else {
unresolvedRefs.push(rawValue);
properties.set(propName, rawValue);
}
Source: packages/cli/src/linter/model/handler.ts lines 93-100.
3. Error Diagnostic Emission
The linter records the failure in the findings array as an error-level diagnostic attached to the specific property path. This enables precise error reporting in the CLI output:
findings.push({
severity: 'error',
path: `colors.${name}`,
message: `'${raw}' is not a valid color…`,
});
Source: packages/cli/src/linter/model/handler.ts lines 66-70.
Practical Examples: Valid vs. Broken References
Valid Token Resolution
Consider a DESIGN.md with properly defined tokens:
# DESIGN.md
colors:
primary: "#ff0000"
components:
button:
background: "{colors.primary}"
Running the validation:
design-cli lint examples/totality-festival/DESIGN.md
Output:
✔ 0 errors, 0 warnings
The resulting DesignSystemState contains components.button.background as a resolved color object with the value #ff0000.
Broken Token Reference Handling
When referencing a non-existent token:
# DESIGN.md
colors:
primary: "{colors.nonexistent}"
components:
button:
background: "{colors.primary}"
The CLI output identifies the specific failure:
✖ 1 error
colors.primary – '{colors.nonexistent}' is not a valid color. Expected a CSS color value…
The generated state preserves the raw string while tracking the failure:
components.get('button')!.properties.get('background') // → "{colors.primary}"
unresolvedRefs: [ "{colors.nonexistent}" ]
Summary
- Graceful degradation: Broken token references do not crash the parser; the linter continues processing the remaining DESIGN.md content without throwing exceptions.
- Triple recording mechanism: Failed resolutions result in omission from concrete token maps, literal string preservation in component properties, and population of the
unresolvedRefsarray. - Precise diagnostics: The
findingsarray receives error-level entries with specific paths and messages, enabling targeted debugging inpackages/cli/src/linter/model/handler.ts. - Downstream flexibility: By preserving raw reference strings in component properties, downstream tools can choose to ignore broken references or surface them as additional warnings.
Frequently Asked Questions
Does a broken token reference crash the DESIGN.md parser?
No, the parser handles broken references gracefully. According to the implementation in google-labs-code/design.md, the resolveReference() function returns null for unresolvable tokens, triggering the error recording logic without throwing exceptions or halting the linting process.
Where does the error message appear when a token reference is broken?
The error appears in the linter's findings array, which is output by the CLI at the end of the lint run. Each error includes the specific path (such as colors.primary), the severity level set to 'error', and a descriptive message indicating which token reference could not be resolved.
Can a component still use a property that references a broken token?
Yes, the component property retains the raw reference string (e.g., "{colors.nonexistent}") as its value. While the token remains unresolved in the concrete design system state, the literal string is stored in the component's properties map and tracked in the unresolvedRefs array for that component.
How does the linter prevent infinite loops with circular token references?
The resolveReference() function tracks visited references using a Set passed as an argument, and it enforces a maximum recursion depth via MAX_REFERENCE_DEPTH. If the resolver detects a circular path or exceeds the depth limit, it aborts the resolution attempt and treats the reference as broken.
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 →