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:

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 counts
  • findings: Array of structured Finding objects with location and severity data
  • designSystem: The resolved DesignSystemState for 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:

Summary

  • The DESIGN.md linter is built as a library, with the CLI merely forwarding content to the lint() function in packages/cli/src/linter/lint.ts
  • Import from @google/design.md/linter to access lint, runLinter, and preEvaluate without subprocess overhead
  • Synchronous execution allows integration in build tools, test suites, and server environments
  • Custom rule support via the rules option enables domain-specific validation logic
  • Tailwind generation is accessible programmatically through TailwindEmitterHandler in packages/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:

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 →