Files
technitium-console/dev/check-sort-fields.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

97 lines
3.4 KiB
JavaScript

/*
Every `<Th field="x">` has to name a key that exists in its `Keys` object.
## What this catches, and how it got through everything else
`useSort` looks the field up and gives up quietly when it is not there:
function toggle(field) {
const read = keys[field]
if (read == null) return // <- the column simply does not sort
...
So a header whose `field` does not match a key is a header you can click for ever.
Nothing else in this project sees it: `field` is a string, so TypeScript has
nothing to check; the column renders, so no test that looks for the header fails;
and no gate reads the pair.
It was found on 2026-09-07 in the Edit Permissions modal, where `field="nombre"`
had been left behind by the English sweep of 2026-08-31 (the sweep renamed the
`Keys` object's key and not the string inside the attribute, because a string in
double quotes is where you put data, not identifiers). The column had been dead
for a week with every gate green.
## How it measures
Per file: the keys declared in any `Keys<...> = { ... }` object or passed inline to
`useSort`, against every `field="..."` in the same file. Both sides are read from
the source, so there is no list to keep in sync.
Files with no `field=` are not looked at. Exit code is the number of findings.
*/
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const SRC = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../src')
function walk(dir, out = []) {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, e.name)
if (e.isDirectory()) walk(p, out)
else if (/\.tsx?$/.test(e.name)) out.push(p)
}
return out
}
const findings = []
let checked = 0
let columns = 0
for (const file of walk(SRC)) {
const text = fs.readFileSync(file, 'utf8')
const fields = [...text.matchAll(/<Th[^>]*?\bfield="([^"]+)"/g)].map((m) => m[1])
if (fields.length === 0) continue
checked++
columns += fields.length
/* The keys of every `Keys` object in the file, however it is written: a named
`const KEYS: Keys<Row> = { … }`, or the object handed straight to `useSort`.
Reading them by brace-matching keeps this a linter and not a parser. */
const keys = new Set()
for (const m of text.matchAll(/(?:Keys<[^>]*>\s*=|useSort(?:<[^>]*>)?\s*\()\s*\{/g)) {
let i = m.index + m[0].length - 1
let depth = 0
let end = i
for (; end < text.length; end++) {
if (text[end] === '{') depth++
else if (text[end] === '}') { depth--; if (depth === 0) break }
}
const body = text.slice(i + 1, end)
for (const k of body.matchAll(/(?:^|[,{])\s*(?:'([^']+)'|"([^"]+)"|([A-Za-z_$][\w$]*))\s*:/g)) {
keys.add(k[1] ?? k[2] ?? k[3])
}
}
if (keys.size === 0) {
findings.push(`${path.relative(SRC, file)}: ${fields.length} sortable columns and no Keys object in sight`)
continue
}
for (const f of new Set(fields)) {
if (!keys.has(f)) {
findings.push(
`${path.relative(SRC, file)}: field="${f}" names no key, so that column does not sort. Keys: ${[...keys].join(', ')}`,
)
}
}
}
if (findings.length === 0) {
console.log(`\n SORT FIELDS: the ${columns} sortable columns of ${checked} files all name a key that exists.\n`)
} else {
console.log('')
for (const f of findings) console.log(` ${f}`)
console.log(`\n ${findings.length} findings\n`)
}
process.exit(Math.min(findings.length, 250))