70 lines
2.6 KiB
JavaScript
70 lines
2.6 KiB
JavaScript
// Doctrine palette audit (shell-wide).
|
|
// The whole app is now blueprint-native: NO source file may contain a
|
|
// hardcoded hex/rgb color outside the 11 doctrinal tokens.
|
|
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
const DOCTRINE = new Set([
|
|
"#f5f7fb", "#e8edf5", "#d5dde9",
|
|
"#1a2740", "#243453",
|
|
"#4a5b80", "#7a8aa8",
|
|
"#c46a14", "#3d6a2c", "#a6342a", "#1d6f82",
|
|
"#0c1322", "#e6edf7",
|
|
]);
|
|
|
|
function fail(msg) { console.log("✗", msg); process.exitCode = 1; }
|
|
function pass(msg) { console.log("✓", msg); }
|
|
|
|
function walk(dir, exts) {
|
|
const out = [];
|
|
for (const e of readdirSync(dir)) {
|
|
const p = join(dir, e);
|
|
const s = statSync(p);
|
|
if (s.isDirectory()) { out.push(...walk(p, exts)); continue; }
|
|
if (exts.some((x) => p.endsWith(x))) out.push(p);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
const FILES = [
|
|
"src/index.css",
|
|
...walk("src", [".ts", ".tsx", ".json"]),
|
|
];
|
|
|
|
// CSS color keywords that are not allowed (besides the ~doctrinal greys we
|
|
// already use via tokens). The doctrine forbids "hardcoded color drift", so
|
|
// we reject named colors anywhere outside test fixtures.
|
|
const FORBIDDEN_NAMED = new Set([
|
|
"white", "black", "red", "green", "blue", "yellow", "orange", "purple",
|
|
"pink", "brown", "gray", "grey", "silver", "gold", "violet", "magenta",
|
|
"cyan", "lime", "teal", "navy", "maroon", "olive", "aqua", "fuchsia",
|
|
]);
|
|
|
|
let violations = 0;
|
|
for (const f of FILES) {
|
|
if (f.endsWith(".test.ts") || f.endsWith(".test.tsx")) continue;
|
|
const src = readFileSync(f, "utf8");
|
|
// Match a hex color only when it is NOT followed by another hex digit
|
|
// (so a literal like `#d3f1a` inside a deploy id is not mis-matched as `#d3f`).
|
|
const hex = (src.match(/#[0-9a-fA-F]{6}(?![0-9a-fA-F])|#[0-9a-fA-F]{3}(?![0-9a-fA-F])/g) || [])
|
|
.filter((h) => !DOCTRINE.has(h.toLowerCase()));
|
|
const rgb = src.match(/\b(?:rgba?|hsla?)\(\s*\d+/g) || [];
|
|
const named = [];
|
|
for (const w of FORBIDDEN_NAMED) {
|
|
const re = new RegExp(`\\b(?:background|color|fill|stroke|border)[^;:]*:\\s*${w}\\b`, "gi");
|
|
const m = src.match(re);
|
|
if (m) named.push(...m);
|
|
}
|
|
if (hex.length || rgb.length || named.length) {
|
|
fail(`${f}: hex=${hex.join(",") || "none"} rgb=${rgb.join(",") || "none"} named=${named.length ? named.join("; ") : "none"}`);
|
|
violations += hex.length + rgb.length + named.length;
|
|
}
|
|
}
|
|
|
|
if (violations === 0) {
|
|
pass(`palette clean: ${FILES.length} files scanned, 0 non-doctrinal color literals`);
|
|
console.log("\nPalette audit PASSED");
|
|
} else {
|
|
console.log(`\nPalette audit FAILED · ${violations} non-doctrinal literals`);
|
|
}
|