Programmatic API
Use design-token-lint as a library from Node.js — lint strings, files, or individual class names.
@takazudo/zudo-design-token-lint exports a small API for integration with build tools, editors, or custom tooling.
Installation
pnpm add @takazudo/zudo-design-token-lintExports
import {
// File/content linting
lintFile,
lintContent,
type LintResult,
// Single-class checking
checkClass,
checkClassWithConfig,
type Violation,
// Config loading and compilation
loadConfig,
compileConfig,
compilePattern,
setConfig,
getConfig,
DEFAULT_CONFIG,
CONFIG_PRESETS,
DEFAULT_PRESET_NAME,
type LintConfig,
type CompiledConfig,
type CompiledRule,
type ConfigPreset,
type ProhibitedEntry,
type ProhibitedConfigEntry,
type CompilePatternOptions,
type CssConfig,
// Class extraction
extractClasses,
DEFAULT_CLASS_ATTRIBUTES,
DEFAULT_CLASS_FUNCTIONS,
type ExtractedClass,
type ExtractorOptions,
// Ignore-comment metadata (used internally by requireIgnoreReason/reportUnusedIgnores,
// also usable directly for custom tooling)
extractClassesWithMeta,
type IgnoreKind,
type IgnoreRecord,
type ExtractWithMetaResult,
// CSS/SCSS declaration scanning (opt-in — see the `css` config field)
extractCssDeclarations,
extractCssDeclarationsWithMeta,
checkDeclaration,
type CssDeclaration,
type CssExtractorOptions,
type CssIgnoreKind,
type CssIgnoreRecord,
type CssExtractWithMetaResult,
type CssViolation,
type CompiledCssConfig,
} from '@takazudo/zudo-design-token-lint';Linting Files and Content
lintFile(filePath)
Read a file from disk and return an array of lint results — one entry per violation.
const results = await lintFile('src/App.tsx');
for (const r of results) {
console.log(`${r.filePath}:${r.line} ${r.className} ${r.reason}`);
}Returns Promise<LintResult[]>:
interface LintResult {
filePath: string;
line: number;
className: string;
reason: string;
}Each entry is a flat record for one violation. If the file has no violations, the returned array is empty.
lintContent(filePath, content)
Lint a string directly — useful for editor plugins or in-memory content. Returns LintResult[] (same shape as above).
const results = lintContent('file.tsx', '<div className="p-4 bg-gray-500">');
// [
// { filePath: 'file.tsx', line: 1, className: 'p-4', reason: '...' },
// { filePath: 'file.tsx', line: 1, className: 'bg-gray-500', reason: '...' }
// ]Checking a Single Class
checkClass(className)
Check one class name against the active config. Returns a Violation if the class is prohibited, or null if it passes.
const violation = checkClass('p-4');
if (violation) {
console.error(violation.reason);
// "Numeric spacing \"p-4\" — use a semantic spacing token or arbitrary value"
}Returns Violation | null:
interface Violation {
className: string;
reason: string;
category?: string; // present only when the matched rule set a category
}category is an optional, additive field — it's only populated when the matching rule came from a structured prohibited entry (see compilePattern below) that set a category. Plain string prohibited entries never produce a category, so existing code destructuring { className, reason } keeps working unchanged.
Note
v2.0.0: when a value matches under semanticPrefixes but its remaining tail is still numeric (e.g. p-hgap-2 with hgap- listed), reason gets an extra parenthetical naming the matched prefix, inserted before any suggestions hint — Numeric spacing "p-hgap-2" — use a semantic spacing token or arbitrary value (numeric tail after the "hgap-" semantic prefix). This is a message-content change only; the Violation shape itself is unchanged.
checkClassWithConfig(className, compiledConfig)
Same as above, but with an explicit compiled config instead of the global one.
import { loadConfig, compileConfig, checkClassWithConfig } from '@takazudo/zudo-design-token-lint';
const config = await loadConfig(process.cwd());
const compiled = compileConfig(config);
const violation = checkClassWithConfig('bg-blue-500', compiled);Working with Config
loadConfig(cwd)
Load .design-token-lint.json or design-token-lint.config.json from a directory. Falls back to DEFAULT_CONFIG if neither exists.
const config = await loadConfig(process.cwd());compileConfig(config)
Compile a plain config object into an efficient rule set ready for matching.
const compiled = compileConfig({
prohibited: ['p-{n}', 'bg-{color}-{shade}'],
allowed: ['p-0'],
ignore: [],
});Structured prohibited entries
Each prohibited/prohibitedAdd entry accepts either a plain pattern string (unchanged) or a structured object:
interface ProhibitedEntry {
pattern: string;
reason?: string; // full reason override — supports "{CLASS}"
category?: string; // rule-family tag, surfaced on Violation.category when this rule matches
}
type ProhibitedConfigEntry = string | ProhibitedEntry;
const compiled = compileConfig({
prohibited: [
'p-{n}', // plain string — unchanged behavior, no category
{
pattern: 'w-{n}',
reason: 'Numeric width "{CLASS}" — use a semantic sizing token or arbitrary value',
category: 'sizing',
},
],
allowed: [],
ignore: [],
});
checkClassWithConfig('w-4', compiled);
// { className: 'w-4', reason: 'Numeric width "w-4" — use a semantic sizing token or arbitrary value', category: 'sizing' }setConfig(compiled) / getConfig()
Set or get the global compiled config used by checkClass() and lintFile().
setConfig(compiled);
const active = getConfig();compilePattern(pattern, options?)
Compile a single pattern string (like p-{n}) into a CompiledRule.
const rule = compilePattern('bg-{color}-{shade}');
// { prefix: 'bg', valuePattern: /^(slate|gray|...)-(\d{2,3})$/, reasonTemplate: '...', isSpacingRule: false }The second argument accepts either a bare string (legacy shape, equivalent to { suggestionSuffix }) or a CompilePatternOptions bag:
interface CompilePatternOptions {
suggestionSuffix?: string; // tweaks the shape-inferred default reason (spacing/color patterns only)
reason?: string; // full reason override — supports "{CLASS}"; wins over suggestionSuffix
category?: string; // rule-family tag carried onto CompiledRule.category and, on a match, Violation.category
}
const rule = compilePattern('w-{n}', {
reason: 'Numeric width "{CLASS}" — use a semantic sizing token or arbitrary value',
category: 'sizing',
});
// { prefix: 'w', valuePattern: /^\d+(\.\d+)?$/, reasonTemplate: 'Numeric width "{CLASS}" — ...', isSpacingRule: true, category: 'sizing' }prohibited/prohibitedAdd config entries accept the same shape as an object — see Structured prohibited entries below.
Extracting Classes
extractClasses(content, options?)
Extract all class name tokens from a source file string, with their line numbers.
const extracted = extractClasses('<div className="p-4 bg-red-500">');
// [
// { className: 'p-4', line: 1 },
// { className: 'bg-red-500', line: 1 }
// ]Accepts an optional options parameter to customize which attributes and functions are scanned:
const extracted = extractClasses(content, {
classAttributes: ['className', 'class', 'inputClassName'],
classFunctions: ['cn', 'clsx', 'cva', 'tv'],
});Returns ExtractedClass[]:
interface ExtractedClass {
className: string;
line: number;
}Supported syntaxes by default:
className="..."andclass="..."(JSX/Astro)className='...'andclass='...'single-quote HTML attribute (common in Astro/HTML)className={'...'}single-quote braceclassName={"..."}double-quote braceclassName={`...`}template literals (simple cases)class:list={["...", '...']}Astro class:list arrays (always scanned)cn(...),clsx(...),classNames(...),twMerge(...)utility calls
DEFAULT_CLASS_ATTRIBUTES
The default list of attribute names scanned by extractClasses:
const DEFAULT_CLASS_ATTRIBUTES: string[];
// ["className", "class"]DEFAULT_CLASS_FUNCTIONS
The default list of utility function names scanned by extractClasses:
const DEFAULT_CLASS_FUNCTIONS: string[];
// ["cn", "clsx", "classNames", "twMerge"]extractClassesWithMeta(content, options?)
Same extraction as extractClasses(), plus structured metadata about every ignore comment in the file — used internally by the requireIgnoreReason/reportUnusedIgnores hygiene flags, and available directly for custom tooling that needs to know what an ignore comment suppressed.
const { classes, ignores } = extractClassesWithMeta(
['// design-token-lint-ignore', '<div className="p-4">', '<div className="m-8">'].join('\n'),
);
// classes: [{ className: 'm-8', line: 3 }] — same as extractClasses(), the ignored line 2 is absent
// ignores: [{
// line: 1, kind: 'next-line', reasonText: null, targetLine: 2,
// suppressedClasses: [{ className: 'p-4', line: 2 }]
// }]Returns ExtractWithMetaResult:
interface ExtractWithMetaResult {
classes: ExtractedClass[]; // byte-identical to extractClasses(content, options)
ignores: IgnoreRecord[];
}
interface IgnoreRecord {
line: number; // 1-based line the ignore comment itself appears on
kind: IgnoreKind; // 'next-line' | 'same-line' | 'file'
reasonText: string | null; // trailing "- reason" text after the directive, or null
targetLine: number; // 1-based line this record suppresses (0 for kind: 'file')
suppressedClasses: ExtractedClass[]; // candidate classes this ignore comment suppressed
}
type IgnoreKind = 'next-line' | 'same-line' | 'file';A trailing same-line ignore (e.g. <div className="p-4"> {/* design-token-lint-ignore */}) produces two records sharing the same comment line: one kind: 'same-line' (targetLine = its own line) and one kind: 'next-line' (targetLine = the line after) — see Ignore Syntax for the suppression semantics this reflects.
Checking a CSS/SCSS Declaration
checkDeclaration(decl, config)
Check a single {property, value, line} declaration (from extractCssDeclarations) against the opt-in css rules. Returns a CssViolation or null.
import { extractCssDeclarations, checkDeclaration } from '@takazudo/zudo-design-token-lint';
const [decl] = extractCssDeclarations('.modal {\n z-index: 9999;\n}\n');
const violation = checkDeclaration(decl, { zIndex: true, colorLiterals: false, patterns: [] });
// { className: 'z-index: 9999', reason: 'Raw z-index integer "9999" — use a --z-* token', category: 'z-index' }Returns CssViolation | null:
interface CssViolation {
className: string; // stable "property: value" form, e.g. "z-index: 9999"
reason: string;
category: 'z-index' | 'color';
}lintContent/lintFile call this internally when the config has a css section and the file extension is .css/.scss — you only need to call it directly for custom tooling that bypasses the linter's own file dispatch.
Extracting CSS/SCSS Declarations
extractCssDeclarations(content, options?)
Extract {property, value, line} triples from plain CSS/SCSS source — the CSS-side counterpart to extractClasses(). Line/regex-based, comment- and string-aware (no PostCSS, no AST), matching the project's no-dependency approach.
const declarations = extractCssDeclarations('.a {\n z-index: 9999;\n color: #fff;\n}\n');
// [
// { property: 'z-index', value: '9999', line: 2 },
// { property: 'color', value: '#fff', line: 3 }
// ]Pass { scss: true } to also treat / as a line comment (valid in SCSS, not in plain CSS; guarded to paren-depth 0 so url(http: is never mistaken for one):
const declarations = extractCssDeclarations(content, { scss: true });Returns CssDeclaration[]:
interface CssDeclaration {
property: string; // as written, e.g. "z-index", "--brand" (not lowercased)
value: string; // comments stripped, whitespace trimmed
line: number; // 1-based line the property starts on
}v1 is strictly declaration-based — see Known Limitations for the documented false negatives (custom-property/SCSS-variable definitions, SCSS maps).
extractCssDeclarationsWithMeta(content, options?)
The CSS-side counterpart to extractClassesWithMeta() — same declarations, plus ignore-comment metadata (CssIgnoreRecord[]), used internally by requireIgnoreReason/reportUnusedIgnores for .css/.scss files.
interface CssExtractWithMetaResult {
declarations: CssDeclaration[];
ignores: CssIgnoreRecord[];
}
interface CssIgnoreRecord {
line: number;
kind: CssIgnoreKind; // 'next-line' | 'same-line' | 'file'
reasonText: string | null;
targetLine: number;
suppressedDeclarations: CssDeclaration[];
}
type CssIgnoreKind = 'next-line' | 'same-line' | 'file';Types
LintConfig
interface LintConfig {
prohibited?: ProhibitedConfigEntry[];
allowed?: string[];
ignore: string[];
patterns?: string[];
suggestionSuffix?: string;
suggestions?: Record<string, string>;
semanticPrefixes?: string[];
classAttributes?: string[];
classFunctions?: string[];
extends?: string | string[];
prohibitedAdd?: ProhibitedConfigEntry[];
allowedAdd?: string[];
css?: CssConfig;
requireIgnoreReason?: boolean;
reportUnusedIgnores?: boolean;
}See Configuration for what each field does, and Structured prohibited entries above for the ProhibitedConfigEntry shape.
CssConfig
The optional css field's shape — see the css configuration section for the full behavior of each flag.
interface CssConfig {
zIndex?: boolean; // default false
colorLiterals?: boolean; // default false
patterns?: string[]; // CSS/SCSS globs the CLI scans, in addition to the Tailwind `patterns`
}ConfigPreset
A named, registered preset usable via the extends field — see CONFIG_PRESETS below.
interface ConfigPreset {
prohibited: ProhibitedConfigEntry[];
allowed?: string[];
}ProhibitedEntry / ProhibitedConfigEntry
interface ProhibitedEntry {
pattern: string;
reason?: string;
category?: string;
}
type ProhibitedConfigEntry = string | ProhibitedEntry;CompilePatternOptions
interface CompilePatternOptions {
suggestionSuffix?: string;
reason?: string;
category?: string;
}CONFIG_PRESETS / DEFAULT_PRESET_NAME
const DEFAULT_PRESET_NAME: string; // "default"
const CONFIG_PRESETS: Record<string, ConfigPreset>;
// { default: { prohibited: [...], allowed: [...] }, "z-index": { prohibited: [...], allowed: ["z-0"] } }CONFIG_PRESETS is the registry consulted by extends. The built-in "z-index" preset (opt-in, not part of DEFAULT_CONFIG) flags z-{n} classes with a z-index-specific reason — use extends: ["default", "z-index"] to layer it on top of the defaults.
ExtractorOptions
Options passed to extractClasses() to customize which attributes and functions are scanned.
interface ExtractorOptions {
classAttributes?: string[];
classFunctions?: string[];
}Both fields are optional. When omitted, DEFAULT_CLASS_ATTRIBUTES and DEFAULT_CLASS_FUNCTIONS are used respectively.
LintResult
interface LintResult {
filePath: string;
line: number;
className: string;
reason: string;
}Violation
interface Violation {
className: string;
reason: string;
category?: string;
}category is optional and additive — only present when the matched rule's prohibited entry was a structured object setting category (e.g. the default sizing rules use category: "sizing", the "z-index" preset uses category: "z-index").
Ignore-metadata and CSS types
Full definitions are given alongside the function that returns them:
IgnoreKind,IgnoreRecord,ExtractWithMetaResult— seeextractClassesWithMetaabove.CssDeclaration,CssExtractorOptions— seeextractCssDeclarationsabove.CssIgnoreKind,CssIgnoreRecord,CssExtractWithMetaResult— seeextractCssDeclarationsWithMetaabove.CssViolation,CompiledCssConfig— seecheckDeclarationabove.
Example: Custom Linter Script
import { glob } from 'glob';
import {
loadConfig,
compileConfig,
setConfig,
lintFile,
} from '@takazudo/zudo-design-token-lint';
async function main() {
const config = await loadConfig(process.cwd());
setConfig(compileConfig(config));
const files = await glob('src/**/*.{tsx,jsx}');
let totalViolations = 0;
for (const file of files) {
const results = await lintFile(file);
for (const r of results) {
console.log(`${r.filePath}:${r.line} ${r.className} ${r.reason}`);
totalViolations++;
}
}
process.exit(totalViolations > 0 ? 1 : 0);
}
main();