/** * Every colour comes from `design/tokens.json`. This refuses a raw one — * `#hex`, `rgb(`, `hsl(` — anywhere else the app draws from: `static/` * and `src/`, tests aside. A colour spelled out in a stylesheet is a colour * the dark theme never hears about. It refuses a `var(--…)` in a stylesheet * that names no token, too: a typo there is a page quietly unstyled. * * bun run lint:design exit 1 and one line per raw colour, or silence */ import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { tokens } from "../src/tokens.ts"; const RAW = /(? [...line.matchAll(RAW)].map( (m) => `${path}:${i + 1}: raw colour ${m[0]} — name it in design/tokens.json and use var(--…)`, ), ); } /** The custom properties a stylesheet uses that no token defines. */ export function unknownTokens( path: string, text: string, known = new Set(tokens.map((t) => t.name)), ): string[] { return text .split("\n") .flatMap((line, i) => [...line.matchAll(/var\((--[\w-]+)/g)] .filter((m) => !known.has(m[1]!)) .map( (m) => `${path}:${i + 1}: ${m[1]} is not a token in design/tokens.json`, ), ); } /** The files the lint reads: what the page is drawn from, never the tokens themselves or the tests. */ export function drawnFrom(root: string): string[] { const walk = (dir: string): string[] => readdirSync(join(root, dir), { withFileTypes: true }).flatMap((entry) => { const path = join(dir, entry.name); if (entry.isDirectory()) return walk(path); return /\.(css|js|ts|html)$/.test(entry.name) && !/\.test\.ts$/.test(entry.name) ? [path] : []; }); return ["static", "src"].flatMap(walk); } export function lint(root: string): string[] { return drawnFrom(root).flatMap((path) => { const text = readFileSync(join(root, path), "utf8"); return [ ...rawColours(path, text), ...(path.endsWith(".css") ? unknownTokens(path, text) : []), ]; }); } if (import.meta.main) { const problems = lint(join(import.meta.dir, "..")); for (const problem of problems) console.error(problem); process.exit(problems.length === 0 ? 0 : 1); }