Files
technitium-console/dev/check-dashes.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 <[email protected]>
2026-10-27 00:06:06 +07:00

112 lines
4.6 KiB
JavaScript

/**
* The guard that keeps the em dash (U+2014) out of the repository. The owner's
* call, on 2026-10-01: in prose it reads as machine-written. A period, a colon, a
* comma or parentheses do the same job. On 2026-10-02 the rule grew from the
* Markdown and the GitHub-facing files to every text file in the repository: the
* code, its comments, the strings it prints and the titles of its tests.
*
* Only tracked files are read (`git ls-files`), so a local, untracked file (a
* note at the root, a symlink to an agent's instructions) is never part of the
* check. A tracked file deleted in the working tree is skipped too.
*
* Not read at all:
*
* - Binaries: a file with a NUL byte in it (fonts, images, the favicon).
* - `package-lock.json`, which npm writes.
* - `public/json/`, upstream's data files, shipped as they come.
*
* One use stays: the interface's "no value" placeholder is an em dash on purpose
* (the stock console draws the same). In the code under `src/` a dash is let
* through when it is the whole of a string literal (`'\u2014'`, in any of the three
* quotes) or the whole of a JSX text (`>\u2014<`, or alone on its line), which also
* covers a test asserting on it (`getByText('\u2014')`). Comments are not exempt:
* they are found and blanked first, so a comment that quotes the placeholder is a
* finding like any other. Anywhere else there is no exemption, so a tool that has
* to match the dash as data writes it as the escape, as `DASH` does below.
*
* Run by `npm test` through `check-dashes.test.mjs`, and on its own with
* `npm run lint:dashes`, which takes optional paths to narrow it
* (`npm run lint:dashes -- dev .github`).
*/
import { execFileSync } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { stripComments } from './check-colour-tokens.mjs'
export const DASH = '\u2014'
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
/** Tracked files that are never read. */
export const SKIPPED = [/^package-lock\.json$/, /^public\/json\//]
/** Where the placeholder may appear, and how each kind of file writes a comment. */
const PLACEHOLDER_DIR = 'src/'
const KINDS = { '.ts': 'ts', '.tsx': 'ts', '.js': 'ts', '.mjs': 'ts', '.css': 'css' }
const PLACEHOLDER = [
/(['"`])\u2014\1/g,
/>\u2014</g,
/^\s*\u2014\s*$/g,
]
/** The line with every placeholder use taken out. */
export function withoutPlaceholders(line) {
return PLACEHOLDER.reduce((l, re) => l.replace(re, ''), line)
}
const count = (s) => s.split(DASH).length - 1
/**
* The lines with an em dash. With `kind` ('ts' or 'css'), the text is code: a dash
* in a comment is always a finding, and one in the code only when it is not the
* placeholder.
*/
export function findDashes(text, { kind } = {}) {
const lines = text.split('\n')
const code = kind ? stripComments(text, kind).split('\n') : lines
const found = []
lines.forEach((line, i) => {
if (!line.includes(DASH)) return
const inComment = count(line) > count(code[i])
if (!kind || inComment || withoutPlaceholders(code[i]).includes(DASH))
found.push({ line: i + 1, text: line.trim() })
})
return found
}
/** The comment syntax of a file where the placeholder may appear; `undefined` elsewhere. */
export function kindOf(rel) {
if (!rel.startsWith(PLACEHOLDER_DIR)) return undefined
return KINDS[path.extname(rel)]
}
export function checkedFiles({ root = ROOT, paths = [] } = {}) {
return execFileSync('git', ['ls-files', '-z', '--', ...paths], { cwd: root, encoding: 'utf8' })
.split('\0')
.filter((rel) => rel && !SKIPPED.some((re) => re.test(rel)) && existsSync(path.join(root, rel)))
}
export function check({ root = ROOT, paths = [] } = {}) {
const findings = []
for (const rel of checkedFiles({ root, paths })) {
const bytes = readFileSync(path.join(root, rel))
if (bytes.includes(0)) continue
for (const f of findDashes(bytes.toString('utf8'), { kind: kindOf(rel) }))
findings.push(`${rel}:${f.line} ${f.text}`)
}
return findings
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const paths = process.argv.slice(2)
const findings = check({ paths })
if (findings.length === 0) {
console.log(`\n No em dash in the ${checkedFiles({ paths }).length} tracked files, apart from the "no value" placeholder.\n`)
} else {
for (const f of findings) console.log(' ' + f)
console.log(`\n ${findings.length} lines with an em dash. Use a period, a colon, a comma or parentheses.\n`)
}
process.exit(Math.min(findings.length, 250))
}