How to Use the DESIGN.md Programmatic Linter API in TypeScript
The DESIGN.md programmatic linter API exposes three core functions—lint(), runLinter(), and preEvaluate()—from packages/cli/src/linter/index.ts, enabling you to parse design system markdown, resolve tokens into a typed model, and execute custom lint rules entirely within TypeScript.
The google-labs-code/design.md repository provides a composable TypeScript library for validating design system documentation. By importing the DESIGN.md programmatic linter API, you can integrate design token validation into build pipelines, custom CLI tools, or IDE extensions without spawning external processes.
Understanding the Linter Architecture
The library implements a four-stage pure functional pipeline implemented across specific handler modules:
- Parser –
ParserHandlerextracts YAML front-matter and H2 sections from raw markdown. Located inpackages/cli/src/linter/parser/handler.ts. - Model –
ModelHandlerresolves design tokens (colors, typography, dimensions) into a strongly-typedDesignSystemState. Located inpackages/cli/src/linter/model/handler.ts. - Lint Rules – Pure functions in
packages/cli/src/linter/rules/*.tsexamine the model and emitFindingobjects. TherunLinterfunction inpackages/cli/src/linter/linter/runner.tsaggregates these into aLintResult. - Emitters –
TailwindEmitterHandlergenerates Tailwind CSS configuration. Located inpackages/cli/src/linter/tailwind/handler.ts.
The high-level lint() function orchestrates this flow: parsing → modeling → linting → Tailwind generation.
Core API Methods
The public API surface in packages/cli/src/linter/index.ts exports three primary entry points:
lint(content: string, options?: LintOptions): LintReport
Parses a DESIGN.md file string, resolves the design system model, runs default lint rules, and returns a full report including Tailwind configuration.
runLinter(state: DesignSystemState, rules?: LintRule[] | RuleDescriptor[]): LintResult
Executes lint rules on an already-resolved design system state. Use this when caching models or running custom rule sets.
preEvaluate(state: DesignSystemState, rules?: LintRule[] | RuleDescriptor[]): GradedTokenEdits
Groups findings by severity (error, warning, info) into a structure suitable for automated fixes or UI suggestions.
TypeScript Implementation Examples
Basic Usage: Linting a DESIGN.md String
Import the lint function and pass your markdown content as a string:
import { lint } from '@design/cli';
const designMd = await Bun.file('my-design.md').text();
const report = lint(designMd);
console.log('Errors:', report.summary.errors);
console.log('Warnings:', report.summary.warnings);
console.log('Tailwind config:', report.tailwindConfig);
This executes the complete pipeline defined in packages/cli/src/linter/lint.ts.
Advanced Usage: Custom Rules and Model Reuse
For scenarios requiring custom validation logic or repeated linting against the same design system, manually orchestrate the pipeline:
import {
ParserHandler,
ModelHandler,
runLinter,
preEvaluate,
type LintRule,
} from '@design/cli';
const designMd = await Bun.file('design.md').text();
const parser = new ParserHandler();
const parseResult = parser.execute({ content: designMd });
if (!parseResult.success) {
throw new Error('Failed to parse DESIGN.md');
}
const model = new ModelHandler();
const { designSystem, findings: modelFindings } = model.execute(parseResult.data);
const noRedRule: LintRule = (state) => {
const redTokens = state.tokens.filter(t => t.value === '#ff0000');
return redTokens.map(t => ({
severity: 'error' as const,
path: t.path,
message: `Avoid using hard‑coded red color at ${t.path}`,
}));
};
const lintResult = runLinter(designSystem, [noRedRule]);
const graded = preEvaluate(designSystem, [noRedRule]);
console.log('Custom errors:', lintResult.summary.errors);
console.log('Fix suggestions:', graded.fixes);
Custom rules must conform to the LintRule signature (state: DesignSystemState) => Finding[]. The runLinter function accepts either rule function arrays or RuleDescriptor objects.
Configuring Your TypeScript Project
Ensure your tsconfig.json supports ES modules:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "node16",
"esModuleInterop": true,
"strict": true
}
}
Key Source Files and Entry Points
packages/cli/src/linter/index.ts– Public API surface exportinglint,runLinter, andpreEvaluate.packages/cli/src/linter/lint.ts– Orchestrates the complete parse → model → lint → Tailwind pipeline.packages/cli/src/linter/linter/runner.ts– Pure functional runner implementingrunLinterandpreEvaluate.packages/cli/src/linter/parser/handler.ts– Markdown parsing logic.packages/cli/src/linter/model/handler.ts– Design token resolution intoDesignSystemState.packages/cli/src/linter/rules/types.ts– TypeScript interfaces forLintRuleandFinding.packages/cli/src/linter/rules/index.ts– Default rule set (DEFAULT_RULES).packages/cli/src/commands/lint.ts– Reference implementation showing how the CLI consumes the programmatic API.
Summary
- The DESIGN.md programmatic linter API provides three main entry points:
lint()for full pipeline execution,runLinter()for rule execution against cached models, andpreEvaluate()for severity-graded findings. - The architecture separates concerns into Parser, Model, Lint Rules, and Emitters, allowing granular control over the validation process.
- Custom lint rules are pure functions implementing the
LintRuleinterface, enabling domain-specific validation beyond the default rule set. - All components are implemented in pure TypeScript and importable from
@design/cliwithout CLI dependencies.
Frequently Asked Questions
What is the difference between lint() and runLinter()?
Use lint() when you have raw markdown content and need the complete pipeline including parsing and Tailwind generation. Use runLinter() when you already have a resolved DesignSystemState and want to execute rules against it, which is more efficient for repeated validation or custom rule testing.
How do I define a custom lint rule?
Custom rules implement the LintRule type from packages/cli/src/linter/rules/types.ts. They receive the DesignSystemState and return an array of Finding objects with severity, path, and message properties. Pass your rule array to runLinter() or preEvaluate().
Can I use the linter without generating Tailwind configuration?
Yes. The runLinter() and preEvaluate() functions perform pure linting without invoking the Tailwind emitter. Only the high-level lint() function includes Tailwind generation in its output.
What TypeScript module resolution should I use?
Configure moduleResolution: "node16" or "bundler" in your tsconfig.json along with "module": "ESNext" to properly resolve the @design/cli exports, as the library ships with ES module syntax.
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 →