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:

  1. Parser – ParserHandler extracts YAML front-matter and H2 sections from raw markdown. Located in packages/cli/src/linter/parser/handler.ts.
  2. Model – ModelHandler resolves design tokens (colors, typography, dimensions) into a strongly-typed DesignSystemState. Located in packages/cli/src/linter/model/handler.ts.
  3. Lint Rules – Pure functions in packages/cli/src/linter/rules/*.ts examine the model and emit Finding objects. The runLinter function in packages/cli/src/linter/linter/runner.ts aggregates these into a LintResult.
  4. Emitters – TailwindEmitterHandler generates Tailwind CSS configuration. Located in packages/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

Summary

  • The DESIGN.md programmatic linter API provides three main entry points: lint() for full pipeline execution, runLinter() for rule execution against cached models, and preEvaluate() 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 LintRule interface, enabling domain-specific validation beyond the default rule set.
  • All components are implemented in pure TypeScript and importable from @design/cli without 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →