apps/glyph Public The code is public; the app answers only to 1 person who can change it and the crew.

Files 5c2d82ded0cf18c81c70d9f4a3f1de0b1688b125
77 lines · 2.5 KB · ~743 tok Raw History
1
/**
2
 * Every colour comes from `design/tokens.json`. This refuses a raw one —
3
 * `#hex`, `rgb(`, `hsl(` — anywhere else the app draws from: `static/`
4
 * and `src/`, tests aside. A colour spelled out in a stylesheet is a colour
5
 * the dark theme never hears about. It refuses a `var(--…)` in a stylesheet
6
 * that names no token, too: a typo there is a page quietly unstyled.
7
 *
8
 *   bun run lint:design    exit 1 and one line per raw colour, or silence
9
 */
10
11
import { readdirSync, readFileSync } from "node:fs";
12
import { join } from "node:path";
13
import { tokens } from "../src/tokens.ts";
14
15
const RAW =
16
  /(?<![\w&])#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3,4})(?![\w-])|\b(?:rgba?|hsla?)\(/gi;
17
18
/** The raw colours in one file's text: `path:line: …`, one per find. */
19
export function rawColours(path: string, text: string): string[] {
20
  return text
21
    .split("\n")
22
    .flatMap((line, i) =>
23
      [...line.matchAll(RAW)].map(
24
        (m) =>
25
          `${path}:${i + 1}: raw colour ${m[0]} — name it in design/tokens.json and use var(--…)`,
26
      ),
27
    );
28
}
29
30
/** The custom properties a stylesheet uses that no token defines. */
31
export function unknownTokens(
32
  path: string,
33
  text: string,
34
  known = new Set(tokens.map((t) => t.name)),
35
): string[] {
36
  return text
37
    .split("\n")
38
    .flatMap((line, i) =>
39
      [...line.matchAll(/var\((--[\w-]+)/g)]
40
        .filter((m) => !known.has(m[1]!))
41
        .map(
42
          (m) =>
43
            `${path}:${i + 1}: ${m[1]} is not a token in design/tokens.json`,
44
        ),
45
    );
46
}
47
48
/** The files the lint reads: what the page is drawn from, never the tokens themselves or the tests. */
49
export function drawnFrom(root: string): string[] {
50
  const walk = (dir: string): string[] =>
51
    readdirSync(join(root, dir), { withFileTypes: true }).flatMap((entry) => {
52
      const path = join(dir, entry.name);
53
      if (entry.isDirectory()) return walk(path);
54
      return /\.(css|js|ts|html)$/.test(entry.name) &&
55
        !/\.test\.ts$/.test(entry.name)
56
        ? [path]
57
        : [];
58
    });
59
  return ["static", "src"].flatMap(walk);
60
}
61
62
export function lint(root: string): string[] {
63
  return drawnFrom(root).flatMap((path) => {
64
    const text = readFileSync(join(root, path), "utf8");
65
    return [
66
      ...rawColours(path, text),
67
      ...(path.endsWith(".css") ? unknownTokens(path, text) : []),
68
    ];
69
  });
70
}
71
72
if (import.meta.main) {
73
  const problems = lint(join(import.meta.dir, ".."));
74
  // Generated app's lint CLI: stderr is its output.
75
  for (const problem of problems) console.error(problem);
76
  process.exit(problems.length === 0 ? 0 : 1);
77
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts