Files
technitium-console/dev/css-dead.mjs
T
DenozordecandClaude Sonnet 5.5 f3531c64f9 Move the console onto ReUI PRO
Install ReUI (MCP, skill, @reui registry, Tailwind v4, shadcn base-nova) and move
the shared UI onto it: Button, Tag, Check, Input/Textarea, Segmented, Alert, Panel,
Dialog, Tooltip, Menu and Select (Popover), Toast (sonner), the Table frame
(DataGrid), the Dashboard KPI strip, the shnt Ops palette, Geist and the type scale.

Design only: behaviour, texts and roles are unchanged. Tests that asserted the old
DOM were updated; the tests now format numbers in English wherever they run.
docs/reui-migration-plan.md records the plan and its decisions.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 00:06:06 +07:00

316 lines
13 KiB
JavaScript

/*
CSS rules written and never applied.
This is the defect no test sees and no screenshot gives away: a class declared in
a module that no component references. It happened with `.v`, `.p` and `.k` on
the Dashboard tiles, which had gone unapplied since early in the redesign.
It is static on purpose. Sweeping the DOM would produce false positives for
everything living in a state the session never reached (a closed dialog, an empty
table, a hover).
## Why it resolves the import instead of just searching for the name
The first version treated any class as alive if its name appeared in ANY file
under `src/`. Its comment claimed the only possible error was staying silent
about a dead class, never inventing one, and that was true, but it stayed silent
about many: `.fail` was declared in four modules and used in one, and the other
three copies passed because the word existed in Settings. Unifying the panel-form
kit surfaced half a dozen leftovers that way.
Now it resolves which module each `styles` in each file points at, following the
two hops this codebase has: the direct import and the re-export
(`export { default as settingsStyles } from './Settings.module.css'`, used by the
nine Settings sub-tabs). Whatever cannot be resolved is not judged.
node dev/css-dead.mjs
*/
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
import { join, dirname, resolve } from 'node:path'
const ROOT = join(import.meta.dirname, '..', 'src')
function files(dir, filter, acc = []) {
for (const e of readdirSync(dir)) {
const p = join(dir, e)
if (statSync(p).isDirectory()) files(p, filter, acc)
else if (filter.test(e)) acc.push(p)
}
return acc
}
const modules = files(ROOT, /\.module\.css$/)
const sources = files(ROOT, /\.tsx?$/).filter((f) => !/\.test\./.test(f))
const code = new Map(sources.map((f) => [f, readFileSync(f, 'utf8')]))
/** Resolves a relative import to a real file under `src/`. */
function resolveImport(from, rel) {
const base = resolve(dirname(from), rel)
for (const cand of [base, `${base}.tsx`, `${base}.ts`, join(base, 'index.tsx')]) {
if (existsSync(cand) && statSync(cand).isFile()) return cand
}
return null
}
/*
Step 1: which module each file brings in under its own name, and under what name
it re-exports it. `alias` is what that file writes; `exposes` is the name another
file can ask for it by.
*/
const alias = new Map() // file -> Map(identifier -> module)
const exposes = new Map() // file -> Map(exported name -> module)
for (const [f, src] of code) {
const a = new Map()
const e = new Map()
for (const m of src.matchAll(/import\s+(\w+)\s+from\s+['"]([^'"]*\.module\.css)['"]/g)) {
a.set(m[1], resolve(dirname(f), m[2]))
}
for (const m of src.matchAll(
/export\s*\{\s*default\s+as\s+(\w+)\s*\}\s*from\s*['"]([^'"]*\.module\.css)['"]/g,
)) {
e.set(m[1], resolve(dirname(f), m[2]))
}
for (const m of src.matchAll(/export\s*\{\s*(\w+)\s+as\s+(\w+)\s*\}/g)) {
if (a.has(m[1])) e.set(m[2], a.get(m[1]))
}
alias.set(f, a)
exposes.set(f, e)
}
/* Step 2: the indirect hop, as in `import { settingsStyles as styles } from '../parts'`. */
for (const [f, src] of code) {
for (const m of src.matchAll(/import\s*\{([^}]*)\}\s*from\s*['"](\.[^'"]*)['"]/g)) {
const target = resolveImport(f, m[2])
if (!target || !exposes.has(target)) continue
for (const piece of m[1].split(',')) {
const [, name, local] = /^\s*(\w+)(?:\s+as\s+(\w+))?\s*$/.exec(piece.trim()) ?? []
if (name && exposes.get(target).has(name)) {
alias.get(f).set(local ?? name, exposes.get(target).get(name))
}
}
}
}
/*
A module consumed with brackets (`styles[variant]`) can use any class, so it is
not judged. You have to look at the identifier EACH file imports its module
under: the Dashboard calls a data object `s` and does `s[m.k]`, which has nothing
to do with its styles.
*/
const dynamic = new Set()
for (const [f, src] of code) {
for (const [id, mod] of alias.get(f)) {
if (new RegExp(`\\b${id}\\[`).test(src)) dynamic.add(mod)
}
}
/* Step 3: which classes each module names, now properly attributed. */
const named = new Map(modules.map((m) => [m, new Set()]))
const consumers = new Map(modules.map((m) => [m, new Set()]))
for (const [f, src] of code) {
for (const [id, mod] of alias.get(f)) {
if (!named.has(mod)) continue
consumers.get(mod).add(f)
for (const m of src.matchAll(new RegExp(`\\b${id}\\.(\\w+)`, 'g'))) {
named.get(mod).add(m[1])
}
}
}
/*
Four places in a CSS file look like they declare a class and declare none: the
comments (`.zt` lived in one), the contents of `url()` (where `www.w3.org` reads
as `.w3`), the `:global()` blocks, and the path of a `composes: … from '…'`,
which ends in `.module.css` and would read as two classes, `.module` and `.css`.
They are stripped before looking.
*/
const strip = (css) =>
css
.replaceAll(/\/\*[\s\S]*?\*\//g, '')
.replaceAll(/url\((?:[^()]|\([^()]*\))*\)/g, '')
.replaceAll(/:global\([^)]*\)/g, '')
.replaceAll(/(['"])(?:(?!\1).)*\1/g, "''")
/* And a class is also alive if another module inherits it with `composes: x from '…'`. */
const composed = new Map(modules.map((m) => [m, new Set()]))
for (const mod of modules) {
// No `strip` here: that removes strings, and the `from` path is one.
const css = readFileSync(mod, 'utf8').replaceAll(/\/\*[\s\S]*?\*\//g, '')
for (const m of css.matchAll(/composes:\s*([\w\s-]+?)\s+from\s*['"]([^'"]+)['"]/g)) {
const target = resolve(dirname(mod), m[2])
if (!composed.has(target)) continue
for (const c of m[1].trim().split(/\s+/)) composed.get(target).add(c)
}
}
let dead = 0
let total = 0
const unjudged = new Set()
for (const mod of modules) {
const css = readFileSync(mod, 'utf8')
const declared = new Set()
for (const m of strip(css).matchAll(/\.(-?[A-Za-z_][\w-]*)/g)) declared.add(m[1])
total += declared.size
if (dynamic.has(mod)) {
unjudged.add(`${mod.slice(ROOT.length + 1)} (bracket access: not judged)`)
continue
}
if (consumers.get(mod).size === 0 && composed.get(mod).size === 0) {
unjudged.add(`${mod.slice(ROOT.length + 1)} (no resolved consumer: not judged)`)
continue
}
const alive = new Set([...named.get(mod), ...composed.get(mod)])
const unused = [...declared].filter((c) => !alive.has(c))
if (unused.length) {
dead += unused.length
console.log(`\n${mod.slice(ROOT.length + 1)}: ${consumers.get(mod).size} consumer(s)`)
for (const c of unused) {
const line = css.split('\n').findIndex((l) => l.includes(`.${c}`)) + 1
console.log(` .${c} (line ${line})`)
}
}
}
/*
And the other direction, which is the one that bites: `styles.foo` where the
module declares no `.foo`.
A CSS module is a plain object at runtime, so a name that is not there is
`undefined`, React drops the attribute and the element renders with no class at
all. Nothing fails: TypeScript sees `Record<string, string>`, the element is in
the DOM so every query for it passes, and the only symptom is a box that lost its
box.
It is the same shape of defect as `<Th field="nombre">` against `{ name: … }`,
found on 2026-09-07 after a week alive: an identifier that travels as a STRING
between two files, with nothing checking the pair.
Modules read with brackets are skipped for the same reason as above: `styles[k]`
can name anything.
*/
const missing = []
for (const [f, src] of code) {
for (const [id, mod] of alias.get(f)) {
if (!named.has(mod) || dynamic.has(mod)) continue
const declared = new Set()
for (const m of strip(readFileSync(mod, 'utf8')).matchAll(/\.(-?[A-Za-z_][\w-]*)/g)) declared.add(m[1])
/* Without stripping the strings, the import path itself matches: `text` in
`from './text.module.css'` reads as `text.module`. */
const prose = src.replaceAll(/(['"`])(?:(?!\1).)*\1/g, "''")
for (const m of prose.matchAll(new RegExp(`\\b${id}\\.(\\w+)`, 'g'))) {
if (!declared.has(m[1])) {
const line = prose.slice(0, m.index).split('\n').length
missing.push(`${f.slice(ROOT.length + 1)}:${line} ${id}.${m[1]}: ${mod.slice(ROOT.length + 1)} declares no .${m[1]}`)
}
}
}
}
/*
And the modules read with BRACKETS, which until now were simply "not judged".
`styles[tone]` can name anything, so the reverse walk above cannot judge them,
but the set of things it can name is not unknown: it is the string-literal union
of the variable's own type, declared a few lines up in the same file. So each
literal of that union is checked against the module.
They are the five most-used primitives in the console (`Alert`, `Button`,
`Dialog`, `Tag` and the three list screens), and a tone with no class is a warning
that silently loses its colour, on every screen at once. "Not judged" was the
wrong answer for exactly the files that could do the most damage.
A literal the code EXCLUDES from the lookup is not required: `Button` writes
`variant !== 'secondary' && styles[variant]` and `Tag` writes
`tone === 'neutral' ? '' : styles[tone]`, and neither declares a class for the one
it steps around. Those are found by looking for a comparison of the same variable
against the literal.
*/
const bracketFindings = []
for (const [f, src] of code) {
for (const [id, mod] of alias.get(f)) {
if (!dynamic.has(mod)) continue
const declared = new Set()
for (const m of strip(readFileSync(mod, 'utf8')).matchAll(/\.(-?[A-Za-z_][\w-]*)/g)) declared.add(m[1])
for (const use of src.matchAll(new RegExp(`\\b${id}\\[(\\w+)\\]`, 'g'))) {
const variable = use[1]
/* The union as the file declares it: `size?: 'compact' | 'form' | …` or
`type ButtonVariant = 'primary' | …` assigned to that variable. */
const decl =
new RegExp(`\\b${variable}\\??\\s*:\\s*((?:'[^']+'\\s*\\|\\s*)+'[^']+')`).exec(src) ??
new RegExp(`\\b${variable}\\??\\s*:\\s*(\\w+)`).exec(src)
if (!decl) continue
let union = decl[1]
if (!union.includes("'")) {
const named = new RegExp(`type\\s+${union}\\s*=\\s*((?:'[^']+'\\s*\\|\\s*)+'[^']+')`).exec(src)
if (!named) continue
union = named[1]
}
const literals = [...union.matchAll(/'([^']+)'/g)].map((m) => m[1])
for (const lit of literals) {
if (declared.has(lit)) continue
const stepped = new RegExp(`${variable}\\s*[!=]==\\s*'${lit}'`).test(src)
if (stepped) continue
bracketFindings.push(
`${f.slice(ROOT.length + 1)} ${id}[${variable}] can be '${lit}' and ${mod.slice(ROOT.length + 1)} declares no .${lit}`,
)
}
}
}
}
if (bracketFindings.length) {
console.log('\nRead with brackets, and one of its values has no class:')
for (const m of bracketFindings) console.log(` ${m}`)
}
if (missing.length) {
console.log('\nNamed but never declared; these elements render with no class:')
for (const m of missing) console.log(` ${m}`)
}
console.log(`\n${dead} classes declared and never named, out of ${total} in ${modules.length} modules`)
if (unjudged.size) {
console.log('\nNot judged for dead classes (bracket access); their values ARE checked above:')
for (const d of unjudged) console.log(` ${d}`)
}
/*
Every `composes: X from './y.module.css'` has to resolve, and until 2026-09-07
nothing checked it. A target that does not exist is ignored in silence: the build
passes, the tests pass (jsdom stubs CSS modules), and the class simply arrives
without the declarations it was composing.
It happened that day. Renaming `rotulo.module.css` sent four `composes` to
`text.module.css`, which has no `smallCaps`, and the Matrix headers lost their
small caps. Four gates were green over it; the only thing that saw it was the
uniformity sweep, and only when the signature was compared string by string
instead of by count.
*/
const broken = []
for (const file of modules) {
const css = readFileSync(file, 'utf8')
for (const m of css.matchAll(/composes:\s*([^;]+?)\s+from\s+['"]([^'"]+)['"]/g)) {
const target = resolve(dirname(file), m[2])
const rel = file.slice(ROOT.length + 1)
if (!existsSync(target)) { broken.push(`${rel}: composes from ${m[2]}, which does not exist`); continue }
const source = readFileSync(target, 'utf8')
for (const name of m[1].trim().split(/\s+/)) {
if (!new RegExp(`\\.${name}\\b`).test(source)) {
broken.push(`${rel}: composes \`${name}\` from ${m[2]}, which does not declare it`)
}
}
}
}
console.log(
broken.length === 0
? `every composes resolves, out of ${modules.length} modules`
: '\nComposes that do not resolve:',
)
for (const b of broken) console.log(' ' + b)
process.exitCode = broken.length + missing.length + bracketFindings.length