How the DESIGN.md Linter Validates Files: A Three-Stage Pipeline
The DESIGN.md linter validates files through a three-stage pipeline that parses markdown and YAML front matter, builds a typed design-system model with reference resolution, and executes configurable lint rules to surface errors, warnings, and infos.
The google-labs-code/design.md repository provides a robust validation system for design token files. Understanding how the DESIGN.md linter validates files helps you debug configuration issues and extend the tool with custom rules. The entire process is orchestrated by the lint() function in packages/cli/src/linter/lint.ts and operates without side effects, returning a pure LintResult that the CLI formats as JSON or plain text.
Stage 1: Parsing the Markdown
The validation process begins with ParserHandler, located in packages/cli/src/linter/parser/handler.ts. This stage walks the AST created by unified and remark-parse to extract structured data from the markdown.
The parser handles two critical tasks:
- YAML extraction – It extracts YAML front matter or fenced
yamlcode blocks and parses them using the yaml library. All YAML blocks are merged into a single token map. - Source mapping – It records line numbers for every token to provide accurate error locations later.
If the parser detects duplicate top-level sections, it reports them as recoverable errors rather than throwing exceptions. The output includes the raw sections, document sections, and the source map for downstream consumption.
Stage 2: Building the Design-System Model
Next, ModelHandler (in packages/cli/src/linter/model/handler.ts) receives the parsed token map and constructs a validated DesignSystemState. This stage transforms raw YAML tokens into a typed design system.
Key responsibilities include:
- Type validation – It validates primitive token types including colors, dimensions, and typography values.
- Reference resolution – It resolves token references like
{color: "{primary}"}through a symbol table, catching circular dependencies and undefined tokens. - Component building – It constructs component definitions and validates their integrity.
- Error collection – Invalid values (malformed colors, unsupported units, excessive nesting depth) become
Findingobjects with appropriate severity. The model never throws; all problems are collected as findings.
The model also flags unknown top-level keys, ensuring the design system adheres to the expected schema before rule evaluation begins.
Stage 3: Running Lint Rules
The final stage executes the runLinter function defined in packages/cli/src/linter/linter/runner.ts. This stage operates on the resolved DesignSystemState and applies the DEFAULT_RULES set.
Rule execution is pure and side-effect free:
- Each rule receives the
DesignSystemStateand may emitFindingobjects classified as error, warning, or info. - The runner aggregates findings from all rules and builds a summary containing counts of errors, warnings, and infos.
- Individual rules live in
packages/cli/src/linter/linter/rules/*(e.g.,missing-primary.ts,contrast-ratio.ts).
The runner returns a LintResult containing the aggregated findings and summary statistics, which the CLI command in packages/cli/src/commands/lint.ts formats for display.
Entry Point and Orchestration
The top-level lint() function in packages/cli/src/linter/lint.ts orchestrates the three-stage pipeline:
import { ParserHandler } from './parser/handler.js';
import { ModelHandler } from './model/handler.js';
import { TailwindEmitterHandler } from './tailwind/handler.js';
import { runLinter } from './linter/runner.js';
const parser = new ParserHandler();
const model = new ModelHandler();
const tailwind = new TailwindEmitterHandler();
const parseResult = parser.execute({ content });
if (!parseResult.success) {
// Recoverable parse error becomes a warning
}
const { designSystem, findings: modelFindings } = model.execute(parseResult.data);
const lintResult = runLinter(designSystem, options?.rules);
The combined findings from the model validation and rule execution are returned to the caller, along with a generated Tailwind configuration if applicable.
Usage Examples
Running the Linter from the CLI
Validate a DESIGN.md file and output JSON formatted results:
# Validate a specific file
design-md lint path/to/DESIGN.md --format json
# Validate from stdin
cat DESIGN.md | design-md lint - --format json
Programmatic Usage
Import the lint function to validate content directly in TypeScript:
import { lint } from 'design-md/packages/cli/src/linter/lint.js';
import { readFileSync } from 'fs';
const content = readFileSync('examples/totality-festival/DESIGN.md', 'utf8');
const report = lint(content);
// Access validation results
console.log(report.summary);
// { errors: 0, warnings: 2, infos: 1 }
// Iterate over specific findings
report.findings.forEach(finding => {
console.log(`${finding.severity}: ${finding.message} at ${finding.path}`);
});
The report object contains:
- designSystem: The fully resolved model
- findings: Array of
{severity, path?, message}objects - summary: Aggregated counts of errors, warnings, and infos
- tailwindConfig: Generated Tailwind theme configuration
Custom Rule Sets
Extend the linter with custom rules by passing a LintOptions configuration:
import { lint, LintOptions } from 'design-md/packages/cli/src/linter/lint.js';
import { myCustomRule } from './my-rules.js';
const opts: LintOptions = {
rules: [myCustomRule]
};
const report = lint(myDesignMdContent, opts);
Custom rules receive the DesignSystemState and return Finding objects, behaving identically to the default rules in packages/cli/src/linter/linter/rules/*.
Summary
- ParserHandler extracts YAML and markdown structure from
packages/cli/src/linter/parser/handler.ts, creating a source-mapped token map. - ModelHandler validates token types and resolves references in
packages/cli/src/linter/model/handler.ts, producing aDesignSystemStateorFindingobjects for errors. - runLinter executes the rule set in
packages/cli/src/linter/linter/runner.ts, aggregating all findings into aLintResult. - The lint() entry point in
packages/cli/src/linter/lint.tsorchestrates the pipeline and returns both the validated model and Tailwind configuration. - All stages are pure functions that return findings rather than throwing, enabling programmatic error recovery and custom reporting.
Frequently Asked Questions
What happens if the YAML front matter contains syntax errors?
The ParserHandler catches YAML syntax errors during the parsing stage and reports them as recoverable findings with severity: error. The linter continues processing to detect additional issues, returning all findings in the final report rather than failing on the first error.
How does the linter resolve token references like {color: "{primary}"}?
During the model building stage, ModelHandler maintains a symbol table of all defined tokens. When it encounters a reference like "{primary}", it resolves the value through this table, checking for circular dependencies and undefined tokens. Unresolvable references generate Finding objects that identify the specific path and missing token name.
Can I disable specific lint rules or add custom ones?
Yes. The lint() function accepts an optional rules array in LintOptions. You can pass a subset of the DEFAULT_RULES to disable specific checks, or provide custom rule functions that receive the DesignSystemState and return Finding arrays. Custom rules reside alongside the defaults in packages/cli/src/linter/linter/rules/* or in your own source files.
Where does the linter store information about line numbers for error reporting?
The ParserHandler creates a source map during the initial parsing stage, recording line numbers for every extracted token. This source map travels with the data through the model and rule stages, ensuring that Finding objects reference the original line numbers in DESIGN.md even after the YAML blocks have been parsed and merged.
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 →