How to Programmatically Use the Linter API in Node.js with @google/design.md

The @google/design.md package exposes a pure-JavaScript linter API that parses DESIGN.md documents and generates Tailwind CSS configurations via the lint(content, options?) function.

The google-labs-code/design.md repository provides a framework-agnostic linting pipeline for design system documentation. By importing the linter API in Node.js, you can programmatically validate DESIGN.md files, resolve design tokens into a typed state, and emit Tailwind configurations without shelling out to a CLI process.

Importing the Linter Entry Point

The package exposes its programmatic interface through the ./linter subpath export defined in packages/cli/package.json (lines 31-34). This entry point re-exports the core lint function and type definitions from packages/cli/src/linter/index.ts.

Install the package via npm:

npm i @google/design.md

Import the linter using ES modules (recommended) or CommonJS:

// ES Modules
import { lint } from '@google/design.md/linter';

// CommonJS
const { lint } = require('@google/design.md/linter');

Architecture of the Linting Pipeline

The linter API orchestrates four distinct layers to transform raw markdown into a validated design system model. According to the source code in packages/cli/src/linter/lint.ts (lines 56-71), the lint function executes the following flow:

  1. ParserHandler (packages/cli/src/linter/parser/handler.ts) – Parses markdown and extracts YAML front-matter, sections, and raw headings.
  2. ModelHandler (packages/cli/src/linter/model/handler.ts) – Transforms parsed tokens into a typed DesignSystemState.
  3. Rule Runner (packages/cli/src/linter/linter/runner.ts, lines 30-40) – Executes runLinter(state, rules?) against the default rule set (DEFAULT_RULES) or custom rules you provide.
  4. TailwindEmitterHandler (packages/cli/src/linter/tailwind/handler.ts) – Converts the resolved design system into a ready-to-use Tailwind CSS configuration object.

The result is a LintReport object (defined in packages/cli/src/linter/lint.ts, lines 30-44) containing the designSystem model, an array of findings, a severity summary, and the generated tailwindConfig.

Running the Linter Programmatically

To lint a DESIGN.md file, read the content into memory and pass it to the lint function. The API has no runtime dependencies beyond peer dependencies like yaml and unified, making it safe for CI pipelines and server-side services.

import { readFile } from 'node:fs/promises';
import { lint } from '@google/design.md/linter';

// Load the DESIGN.md content
const designMd = await readFile('path/to/DESIGN.md', 'utf8');

// Run the linter with default rules
const report = lint(designMd);

You can optionally pass a custom rules array via the options parameter:

import { myCustomRule } from './my-rules.js';

const report = lint(designMd, { rules: [myCustomRule] });

The main execution flow (lines 91-107 in packages/cli/src/linter/lint.ts) handles markdown parsing, model resolution, rule execution, and Tailwind config generation in a single synchronous call.

Handling the LintReport Output

The LintReport returned by the API provides structured data for validation results and theme extraction. Access the severity summary and individual findings to enforce quality gates:

console.log(`Errors: ${report.summary.errors}`);
console.log(`Warnings: ${report.summary.warnings}`);

if (report.findings.length) {
  for (const f of report.findings) {
    console.log(`[${f.severity}] ${f.path ?? '(global)'} – ${f.message}`);
  }
}

To integrate with a build pipeline, write the generated Tailwind configuration to a file that the Tailwind CLI can consume:

import { writeFile } from 'node:fs/promises';

const tailwindConfig = `module.exports = ${JSON.stringify(report.tailwindConfig, null, 2)};`;
await writeFile('tailwind.config.cjs', tailwindConfig);

The tailwindConfig property contains the complete theme object derived from your DESIGN.md tokens, ready for immediate use by Tailwind CSS.

Summary

  • Import the linter API from @google/design.md/linter to access the lint function.
  • The pipeline parses markdown via ParserHandler, resolves tokens via ModelHandler, validates via runLinter in packages/cli/src/linter/linter/runner.ts, and emits configs via TailwindEmitterHandler.
  • Pass a string of DESIGN.md content to lint() and receive a LintReport with designSystem, findings, summary, and tailwindConfig.
  • The API is framework-agnostic, runs synchronously, and requires no external CLI invocation.

Frequently Asked Questions

Can I use the linter API with CommonJS?

Yes. While ES modules are recommended, the package supports CommonJS via require('@google/design.md/linter'). The ./linter export in packages/cli/package.json provides the same surface for both module systems.

What Node.js version is required?

The package requires Node.js 18 or higher. The linter API uses native Node.js APIs like node:fs/promises and modern JavaScript features without additional polyfills.

How do I add custom lint rules?

Pass a rules array in the options object to the lint function. Each rule should conform to the rule interface used by runLinter in packages/cli/src/linter/linter/runner.ts (lines 30-40). Custom rules execute alongside the DEFAULT_RULES set unless explicitly disabled.

Does the API require Tailwind CSS to be installed?

No. The TailwindEmitterHandler generates a configuration object compatible with Tailwind CSS, but the linter itself has no runtime dependency on Tailwind. You can use the API to validate DESIGN.md files without ever generating CSS, or extract the tailwindConfig to use with your own Tailwind installation separately.

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 →