mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:21:10 +02:00
The plan-doc commits129971ffc+9db044178dropped the audit-theme-tokens → validate-theme-variables rename, the validate-theme-tokens → validate-theme-utilities rename, the new validate-theme-parity script, brand-literals.md, and the corresponding package.json + lint-staged.config.js + themes.css wiring. The files still existed on disk (git mv changes survived) but were untracked. Restore the validator suite so `pnpm run validate:all` works again: - validate:theme-variables (CSS var names: --muted → --color-muted) - validate:theme-utilities (Tailwind: no white/N, no neutral palette) - validate:theme-parity (every --color-* in :root ⇔ .dark + each [data-theme="..."]) All three wired into validate:all and lint-staged. `pnpm run validate:all` is clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
219 lines
6 KiB
JavaScript
219 lines
6 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Validate theme CSS variable names.
|
|
*
|
|
* The Mana theme system standardizes on `--color-*` CSS custom properties
|
|
* (defined in `packages/shared-tailwind/src/themes.css`). Earlier, components
|
|
* ported from shadcn drifted into bare names (`--muted`, `--primary`, …) and
|
|
* `--theme-*` prefixes, neither of which exist in the theme. Those
|
|
* references silently fell back to nothing (or to literal fallbacks) and
|
|
* stopped tracking the active theme variant.
|
|
*
|
|
* This check greps Svelte/CSS/TS source files for those legacy patterns
|
|
* and fails if any remain. Run in CI and lint-staged so the drift can't
|
|
* sneak back in.
|
|
*
|
|
* Companion check: `validate-theme-utilities.mjs` enforces theme-token
|
|
* Tailwind classes (e.g. `text-foreground` instead of `text-white/80`).
|
|
*
|
|
* Usage:
|
|
* node scripts/validate-theme-variables.mjs
|
|
*/
|
|
|
|
import { readdirSync, statSync, readFileSync } from 'fs';
|
|
import { join, dirname } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = join(__dirname, '..');
|
|
|
|
const RED = '\x1b[31m';
|
|
const GREEN = '\x1b[32m';
|
|
const YELLOW = '\x1b[33m';
|
|
const DIM = '\x1b[2m';
|
|
const RESET = '\x1b[0m';
|
|
const BOLD = '\x1b[1m';
|
|
|
|
const SCAN_ROOTS = ['apps', 'packages'];
|
|
const SCAN_EXTS = new Set(['.svelte', '.css', '.ts', '.tsx']);
|
|
const SKIP_DIRS = new Set([
|
|
'node_modules',
|
|
'dist',
|
|
'build',
|
|
'.svelte-kit',
|
|
'.next',
|
|
'.turbo',
|
|
'.vercel',
|
|
'.vite',
|
|
]);
|
|
|
|
// Shadcn-convention names that should be `--color-*` in this repo.
|
|
const SHADCN_TOKENS = [
|
|
'muted',
|
|
'muted-foreground',
|
|
'primary',
|
|
'primary-foreground',
|
|
'secondary',
|
|
'secondary-foreground',
|
|
'accent',
|
|
'accent-foreground',
|
|
'foreground',
|
|
'background',
|
|
'border',
|
|
'border-strong',
|
|
'card',
|
|
'card-foreground',
|
|
'popover',
|
|
'popover-foreground',
|
|
'destructive',
|
|
'destructive-foreground',
|
|
'input',
|
|
'ring',
|
|
'surface',
|
|
'surface-hover',
|
|
'surface-elevated',
|
|
'error',
|
|
'success',
|
|
'warning',
|
|
];
|
|
|
|
// Per-element inline custom properties and unrelated local vars that happen
|
|
// to collide on a word. Keeping this list narrow — add entries with care.
|
|
const ALLOWED_LOCAL_NAMES = new Set([
|
|
'primary-color', // shared-auth-ui inline prop carrier
|
|
'app-color',
|
|
'btn-color',
|
|
'tag-color',
|
|
'content-bg',
|
|
'ring-color',
|
|
]);
|
|
|
|
// Files we know carry module-literal brand accents (see themes.css §4 —
|
|
// brand/domain semantics stay as literals, not theme tokens).
|
|
const ALLOWED_FILES = new Set([
|
|
'apps/mana/apps/web/src/lib/modules/news-research/ListView.svelte',
|
|
'apps/mana/apps/web/src/routes/(app)/news-research/+page.svelte',
|
|
// Agent template colors are set per-element via style="--accent: ..."
|
|
'apps/mana/apps/web/src/routes/(app)/agents/templates/+page.svelte',
|
|
]);
|
|
|
|
const bareTokenRe = new RegExp(`var\\(--(${SHADCN_TOKENS.join('|')})\\s*[,)]`, 'g');
|
|
const themePrefixRe = /var\(--theme-[a-z-]+\s*[,)]/g;
|
|
|
|
function* walk(dir) {
|
|
let entries;
|
|
try {
|
|
entries = readdirSync(dir);
|
|
} catch {
|
|
return;
|
|
}
|
|
for (const name of entries) {
|
|
if (SKIP_DIRS.has(name)) continue;
|
|
const full = join(dir, name);
|
|
let st;
|
|
try {
|
|
st = statSync(full);
|
|
} catch {
|
|
continue;
|
|
}
|
|
if (st.isDirectory()) {
|
|
yield* walk(full);
|
|
} else if (SCAN_EXTS.has(name.slice(name.lastIndexOf('.')))) {
|
|
yield full;
|
|
}
|
|
}
|
|
}
|
|
|
|
function* findViolations(filePath) {
|
|
const rel = filePath.slice(ROOT.length + 1);
|
|
if (ALLOWED_FILES.has(rel)) return;
|
|
|
|
let content;
|
|
try {
|
|
content = readFileSync(filePath, 'utf8');
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
const lines = content.split('\n');
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
|
|
// Skip var(--color-*) — those are correct. We also skip the
|
|
// redefinition block inside @theme in themes.css itself.
|
|
for (const m of line.matchAll(bareTokenRe)) {
|
|
const match = m[0];
|
|
// Ignore if this is actually var(--color-X) with a shared suffix
|
|
// (regex doesn't match because of the leading `--color-`, but be
|
|
// doubly safe in case of future edits).
|
|
if (match.startsWith('var(--color-')) continue;
|
|
yield { line: i + 1, col: m.index + 1, text: line.trim(), kind: 'shadcn' };
|
|
}
|
|
|
|
for (const m of line.matchAll(themePrefixRe)) {
|
|
yield { line: i + 1, col: m.index + 1, text: line.trim(), kind: 'theme-prefix' };
|
|
}
|
|
}
|
|
}
|
|
|
|
let totalFiles = 0;
|
|
let violationCount = 0;
|
|
const byFile = new Map();
|
|
|
|
for (const sub of SCAN_ROOTS) {
|
|
for (const file of walk(join(ROOT, sub))) {
|
|
totalFiles++;
|
|
const hits = [...findViolations(file)];
|
|
if (hits.length === 0) continue;
|
|
|
|
// Second-level filter: ALLOWED_LOCAL_NAMES (collision guard).
|
|
const filtered = hits.filter((h) => {
|
|
if (h.kind !== 'shadcn') return true;
|
|
const nameMatch = h.text.match(/var\(--([a-z-]+)/);
|
|
if (!nameMatch) return true;
|
|
return !ALLOWED_LOCAL_NAMES.has(nameMatch[1]);
|
|
});
|
|
if (filtered.length === 0) continue;
|
|
|
|
byFile.set(file.slice(ROOT.length + 1), filtered);
|
|
violationCount += filtered.length;
|
|
}
|
|
}
|
|
|
|
if (violationCount === 0) {
|
|
console.log(
|
|
`${GREEN}✓${RESET} Theme tokens clean — scanned ${totalFiles} files, no bare ${BOLD}--muted${RESET}/${BOLD}--primary${RESET}/${BOLD}--theme-*${RESET} references.`
|
|
);
|
|
process.exit(0);
|
|
}
|
|
|
|
console.log(
|
|
`${RED}✗${RESET} Found ${BOLD}${violationCount}${RESET} theme-token violation${violationCount === 1 ? '' : 's'} across ${byFile.size} file${byFile.size === 1 ? '' : 's'}:\n`
|
|
);
|
|
|
|
for (const [file, hits] of byFile) {
|
|
console.log(`${BOLD}${file}${RESET}`);
|
|
for (const h of hits) {
|
|
const tag =
|
|
h.kind === 'theme-prefix' ? `${YELLOW}theme-prefix${RESET}` : `${YELLOW}shadcn${RESET}`;
|
|
console.log(` ${DIM}${h.line}:${h.col}${RESET} [${tag}] ${h.text}`);
|
|
}
|
|
console.log();
|
|
}
|
|
|
|
console.log(
|
|
`${DIM}Fix: replace bare tokens with --color-* equivalents. Example:${RESET}
|
|
${RED}-${RESET} background: hsl(var(--muted));
|
|
${GREEN}+${RESET} background: hsl(var(--color-muted));
|
|
|
|
${DIM}Or for shadcn-style inline fallbacks:${RESET}
|
|
${RED}-${RESET} color: var(--foreground, #111);
|
|
${GREEN}+${RESET} color: hsl(var(--color-foreground));
|
|
|
|
${DIM}See packages/shared-tailwind/src/themes.css for the full token list.${RESET}
|
|
`
|
|
);
|
|
|
|
process.exit(1);
|