Can the DESIGN.md Linter Be Used Programmatically? Complete TypeScript API Guide
Yes, the DESIGN.md linter exposes a TypeScript API that allows you to validate design tokens, retrieve structured lint findings, and generate Tailwind configuration objects synchronously without invoking the CLI.
The google-labs-code/design.md package provides a programmatic interface that mirrors the functionality of the command-line tool. Whether you need to validate DESIGN.md content in a CI pipeline, build a custom IDE extension, or generate themes dynamically, the library exports in packages/cli/src/linter/index.ts provide direct access to the parsing, validation, and emission engine.
How the DESIGN.md Linter Works Programmatically
The DESIGN.md linter is architected as a library first, with the CLI acting as a thin wrapper around core functions.
Layered Architecture
The implementation follows a clear pipeline from raw markdown to validated output:
- Public API Layer: Re-exports
lint,runLinter, andpreEvaluatefrompackages/cli/src/linter/index.ts, serving as the stable entry point for consumers - Core Engine: The
lint()function inpackages/cli/src/linter/lint.tsorchestrates parsing, model building, and rule execution - Parser:
ParserHandlerinpackages/cli/src/linter/parser/handler.tsextracts front-matter and section headings - Model Builder:
ModelHandlerinpackages/cli/src/linter/model/handler.tsresolves token references and validates types into aDesignSystemState - Rule Runner:
runLinter()inpackages/cli/src/linter/linter/runner.tsexecutes validation rules likebrokenRefandcontrastCheck - Tailwind Emitter: Optional generation of Tailwind v3 or v4 configurations via
packages/cli/src/linter/tailwind/handler.ts
This design means the design.md binary simply forwards file contents to the library's lint function, making the programmatic API identical to the CLI behavior.
Core API Methods
When you use the DESIGN.md linter programmatically, you work with three primary functions that offer different levels of control.
lint(content, options?)
The lint function is the high-level entry point defined in packages/cli/src/linter/lint.ts. It accepts a string of DESIGN.md content and returns a LintReport object containing:
summary: Error, warning, and info countsfindings: Array of structuredFindingobjects with location and severity datadesignSystem: The resolvedDesignSystemStatefor further processing
runLinter(state, rules?)
For advanced use cases, runLinter in packages/cli/src/linter/linter/runner.ts allows you to execute specific rule sets against an existing DesignSystemState. This is useful when you need to:
- Apply custom validation rules alongside defaults
- Re-run validation after programmatically modifying the design system
- Build incremental linting workflows
preEvaluate(designSystem)
Also located in packages/cli/src/linter/linter/runner.ts, preEvaluate groups findings by severity into fixes, improvements, and suggestions. This grading system is ideal for UI-driven "fix suggestion" panels or automated remediation workflows.
Implementation Examples
Basic Programmatic Linting
Import the lint function and validate a DESIGN.md string without touching the filesystem:
import { lint } from '@google/design.md/linter';
const designMd = `
---
name: Demo
colors:
primary: "#1a1c1e"
---
## Overview
Simple design.
`;
const report = lint(designMd);
console.log(report.summary); // { errors: 0, warnings: 0, infos: 1 }
console.log(report.findings); // Array of Finding objects
The lint function operates synchronously with no I/O dependencies, making it safe for server-side rendering or build-time validation.
Custom Rule Integration
Extend the default rule set with custom validation logic using runLinter and the DEFAULT_RULES export:
import { lint, runLinter, DEFAULT_RULES } from '@google/design.md/linter';
import { brokenRef } from '@google/design.md/linter';
const customRule = (state) => {
const findings = brokenRef(state);
console.log('Broken references detected:', findings);
return findings;
};
const report = lint(designMd, {
rules: [...DEFAULT_RULES, customRule]
});
Custom rules follow the LintRule type signature and receive the fully resolved DesignSystemState.
Pre-Evaluation and Graded Fixes
Use preEvaluate to categorize findings for user interfaces:
import { lint, preEvaluate } from '@google/design.md/linter';
const { designSystem } = lint(designMd);
const edits = preEvaluate(designSystem);
// Returns: { fixes: [...], improvements: [...], suggestions: [...] }
Programmatic Tailwind Generation
Generate Tailwind configurations without writing intermediate files:
import { lint } from '@google/design.md/linter';
import { TailwindEmitterHandler } from '@google/design.md/linter';
const { designSystem } = lint(designMd);
const emitter = new TailwindEmitterHandler();
const tailwindConfig = emitter.execute(designSystem);
// JSON ready for tailwind.config.js
The emitter supports both Tailwind v3 (json-tailwind) and v4 (css-tailwind) output formats via packages/cli/src/linter/tailwind/handler.ts.
Async File Processing in Node.js
Combine the API with Node.js file system operations for CLI-like behavior:
import { readFile } from 'node:fs/promises';
import { lint } from '@google/design.md/linter';
async function validateDesignFile(path: string) {
const content = await readFile(path, 'utf-8');
const report = lint(content);
if (report.summary.errors > 0) {
console.error('Validation failed:', report.findings);
process.exit(1);
}
console.log('Design system valid');
}
validateDesignFile('./DESIGN.md');
Key Source Files
Understanding the source structure helps when extending the programmatic API:
packages/cli/src/linter/index.ts: Public exports and type definitionspackages/cli/src/linter/lint.ts: Corelint()implementationpackages/cli/src/linter/linter/runner.ts:runLinter()andpreEvaluate()logicpackages/cli/src/commands/lint.ts: CLI wrapper demonstrating library usagepackages/cli/src/linter/tailwind/handler.ts: Tailwind configuration generation
Summary
- The DESIGN.md linter is built as a library, with the CLI merely forwarding content to the
lint()function inpackages/cli/src/linter/lint.ts - Import from
@google/design.md/linterto accesslint,runLinter, andpreEvaluatewithout subprocess overhead - Synchronous execution allows integration in build tools, test suites, and server environments
- Custom rule support via the
rulesoption enables domain-specific validation logic - Tailwind generation is accessible programmatically through
TailwindEmitterHandlerinpackages/cli/src/linter/tailwind/handler.ts
Frequently Asked Questions
Can I use the DESIGN.md linter in a browser environment?
Yes, the core linting engine is platform-agnostic JavaScript/TypeScript. Since the lint function in packages/cli/src/linter/lint.ts operates synchronously on strings without file system I/O, you can bundle it for browser use cases such as live preview editors or in-browser design system validation.
What is the performance overhead of using the API versus the CLI?
There is no additional overhead because the CLI itself calls the same lint() function. Using the programmatic API actually eliminates the cost of spawning a subprocess, making it slightly more efficient for batch processing or watch modes.
How do I access the parsed design tokens after linting?
The lint() function returns a designSystem property containing the fully resolved DesignSystemState. This object includes all parsed tokens, colors, and typography definitions from your DESIGN.md content, accessible immediately after validation without additional parsing steps.
Can I disable specific rules when calling lint programmatically?
Yes, the lint function accepts an options object where you can pass a custom rules array. Import DEFAULT_RULES and filter or extend the array to control which validations run. This is processed by runLinter() in packages/cli/src/linter/linter/runner.ts.
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 →