mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
5189d7fcca
lint:size counts the lines of every source file under client/src, tests aside. A file may hold 1000 lines; the 24 files that are longer today are listed in scripts/size-baseline.json with their current length and fail the check when they grow. --update lowers an entry once a file shrinks and never raises one, so a long file is split by concern instead of growing. A baseline that cannot be read or parsed stops the check with its reason, and --update leaves it as it is. It runs in the client job next to the reading direction check.
102 lines
4.2 KiB
JavaScript
102 lines
4.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/*
|
|
* lint:size: keeps source files from growing into the next god file.
|
|
*
|
|
* A file past a thousand lines holds several concerns that no longer fit in
|
|
* one reading, and every change to one of them risks the others. Each source
|
|
* file under src/ (tests aside) may hold LIMIT lines; the files that were
|
|
* already longer are listed in scripts/size-baseline.json with the length
|
|
* they had, and the check fails when one of them grows past its entry. A
|
|
* file that needs to grow is split by concern instead (the trip planner hook,
|
|
* page and road trip sidebar are the precedent).
|
|
*
|
|
* npm run lint:size check against the baseline (CI)
|
|
* npm run lint:size -- --update lower the baseline to what the files hold now;
|
|
* it never raises an entry, and drops a file
|
|
* that is back under the limit
|
|
*/
|
|
import { existsSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs'
|
|
import { join, relative } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const SRC = fileURLToPath(new URL('../src', import.meta.url))
|
|
const BASELINE = fileURLToPath(new URL('./size-baseline.json', import.meta.url))
|
|
|
|
/** The most lines a file without a baseline entry may hold. */
|
|
export const LIMIT = 1000
|
|
|
|
function walk(dir, files = []) {
|
|
for (const name of readdirSync(dir)) {
|
|
const path = join(dir, name)
|
|
if (statSync(path).isDirectory()) walk(path, files)
|
|
else if (/\.tsx?$/.test(name) && !/\.(?:test|spec)\./.test(name)) files.push(path)
|
|
}
|
|
return files
|
|
}
|
|
|
|
/** Lines as an editor numbers them: a final newline does not start another. */
|
|
export function lineCount(text) {
|
|
if (text === '') return 0
|
|
const breaks = text.split('\n').length - 1
|
|
return text.endsWith('\n') ? breaks : breaks + 1
|
|
}
|
|
|
|
function scan() {
|
|
const counts = {}
|
|
for (const path of walk(SRC)) counts[relative(SRC, path).split('\\').join('/')] = lineCount(readFileSync(path, 'utf8'))
|
|
return counts
|
|
}
|
|
|
|
/**
|
|
* The baseline as committed. A missing file is an empty baseline. One that cannot be
|
|
* read or parsed stops the run with its reason: read as empty, every long file would
|
|
* fail with no hint at the cause, and --update would replace it with an empty one.
|
|
*/
|
|
function readBaseline() {
|
|
try {
|
|
return JSON.parse(readFileSync(BASELINE, 'utf8'))
|
|
} catch (err) {
|
|
if (err.code === 'ENOENT') return {}
|
|
console.error(`FAIL scripts/size-baseline.json cannot be read: ${err.message}`)
|
|
console.error('Restore the file from git before running the check or --update again.')
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
// Compared by real path, so the check still runs when the script is started through a symlink.
|
|
const isCli =
|
|
Boolean(process.argv[1]) && existsSync(process.argv[1]) && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
if (isCli) {
|
|
const counts = scan()
|
|
let baseline = readBaseline()
|
|
|
|
if (process.argv.includes('--update')) {
|
|
const lowered = {}
|
|
for (const [file, allowed] of Object.entries(baseline)) {
|
|
const now = counts[file] ?? 0
|
|
if (now > LIMIT) lowered[file] = Math.min(allowed, now)
|
|
}
|
|
const sorted = Object.fromEntries(Object.entries(lowered).sort(([a], [b]) => a.localeCompare(b)))
|
|
writeFileSync(BASELINE, JSON.stringify(sorted, null, 2) + '\n')
|
|
baseline = sorted
|
|
}
|
|
|
|
const grown = Object.entries(counts).filter(([file, n]) => n > Math.max(baseline[file] ?? 0, LIMIT))
|
|
const lowerable = Object.entries(baseline).filter(([file, n]) => (counts[file] ?? 0) < n)
|
|
|
|
for (const [file, n] of grown) {
|
|
const entry = baseline[file]
|
|
console.error(
|
|
`FAIL ${file}: ${n} lines, ` +
|
|
(entry ? `its baseline is ${entry}. ` : `the limit is ${LIMIT}. `) +
|
|
'Move a concern into a hook or component of its own instead of growing the file.',
|
|
)
|
|
}
|
|
if (lowerable.length && !process.argv.includes('--update')) {
|
|
console.log(`${lowerable.length} file(s) are shorter than their baseline now: run with --update to lower it.`)
|
|
}
|
|
const listed = Object.keys(baseline).length
|
|
console.log(`size: ${Object.keys(counts).length} file(s), ${listed} over ${LIMIT} lines held at their baseline`)
|
|
if (grown.length) process.exit(1)
|
|
}
|