Files
meshcore-analyzer/scripts/check-css-vars.js
T
Kpa-clawbot 608d81f8c9 test(#1128): add scripts/check-css-vars.js — fails on 3 undefined vars (red commit)
Adds a CSS-variable lint that walks public/*.css and asserts every
var(--name) reference WITHOUT a fallback resolves to a definition
in some public/*.css. References WITH a fallback are tolerated.

This is the red commit (TDD): on the current master tree, the
script exits 1 with three undefined references that bypassed
review:

  public/style.css:2628  var(--text-primary)
  public/style.css:2675  var(--bg-hover)
  public/style.css:2924  var(--primary)

These are the same class of bug as the original Bug 4 in #1128
(the --surface regression that left 8 dropdowns transparent —
see specs/packets-layout-audit.md Section 5 #1).

Refs #1128
2026-05-06 06:47:14 +00:00

68 lines
2.5 KiB
JavaScript

#!/usr/bin/env node
/*
* scripts/check-css-vars.js — issue #1128 (audit Section 5 #1)
*
* Walks every public/*.css file and asserts that every `var(--name)`
* reference WITHOUT a fallback resolves to a `--name:` definition in
* SOME public/*.css file. (References WITH a fallback like
* `var(--maybe, var(--always))` are tolerated; the fallback chain
* keeps them safe.)
*
* Catches regressions like the original Bug 4 in #1128, where
* `background: var(--surface)` was shipped with `--surface` undefined,
* leaving 8 dropdowns/popovers transparent.
*
* Exit code: 0 = clean, 1 = one or more undefined vars (with locations).
*
* Usage:
* node scripts/check-css-vars.js # default (lints public/)
* node scripts/check-css-vars.js --dir x # lint a different directory
*/
'use strict';
const fs = require('fs');
const path = require('path');
let dir = 'public';
for (let i = 2; i < process.argv.length; i++) {
if (process.argv[i] === '--dir' && process.argv[i + 1]) { dir = process.argv[++i]; }
}
if (!fs.existsSync(dir)) {
console.error('check-css-vars: directory not found: ' + dir);
process.exit(2);
}
const files = fs.readdirSync(dir).filter(f => f.endsWith('.css')).map(f => path.join(dir, f));
if (!files.length) {
console.error('check-css-vars: no .css files found in ' + dir);
process.exit(2);
}
const defined = new Set();
const uses = []; // { file, line, name }
const defRe = /(?:^|[^a-zA-Z0-9_-])(--[a-zA-Z0-9_-]+)\s*:/g;
// Match var(--name) ONLY when the closing ')' immediately follows the name
// (optional whitespace). Anything else (a comma → fallback) is exempt.
const useRe = /var\(\s*(--[a-zA-Z0-9_-]+)\s*\)/g;
for (const f of files) {
const lines = fs.readFileSync(f, 'utf8').split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
let m;
defRe.lastIndex = 0;
while ((m = defRe.exec(line)) !== null) defined.add(m[1]);
useRe.lastIndex = 0;
while ((m = useRe.exec(line)) !== null) uses.push({ file: f, line: i + 1, name: m[1] });
}
}
const undef = uses.filter(u => !defined.has(u.name));
if (undef.length) {
console.error('check-css-vars: ' + undef.length + ' undefined CSS variable reference(s):');
for (const u of undef) console.error(' ' + u.file + ':' + u.line + ' var(' + u.name + ')');
console.error('Fix: define the variable in :root, or use var(' + undef[0].name + ', <fallback>).');
process.exit(1);
}
console.log('check-css-vars: OK — ' + uses.length + ' var() refs, ' + defined.size + ' definitions, 0 undefined.');