/* * The measuring and the comparison behind lint:size (scripts/size-lint.mjs). * * A file's size is its lines as an editor wraps them at WIDTH columns: a line * up to WIDTH characters counts once, a longer one once per WIDTH characters * it spans. Counting newlines alone let a file stay under its entry by joining * lines, and the trip planner page already held a 3760 character one; WIDTH is * the printWidth in .prettierrc, so formatted code counts what it shows. */ import { join } from 'node:path' import { countMap, listFiles, lowerCounts, readBaseline, readText, TEST_FILE, toKey, writeBaseline } from './ratchet.mjs' export const WIDTH = 120 /** * The kinds of file the check covers, each with the size a file without a * baseline entry may reach. Tests get more room: a spec mirrors the component * it covers and holds fixtures, but past LIMIT it is a god file too. */ export const GROUPS = [ { name: 'test', limit: 2000, accepts: (key) => /\.tsx?$/.test(key) && (key.startsWith('tests/') || TEST_FILE.test(key)) }, { name: 'source', limit: 1000, accepts: (key) => key.startsWith('src/') && /\.tsx?$/.test(key) && !TEST_FILE.test(key) }, { name: 'stylesheet', limit: 1000, accepts: (key) => key.startsWith('src/') && key.endsWith('.css') }, ] export const DIRS = ['src', 'tests'] export function groupOf(key) { return GROUPS.find((group) => group.accepts(key)) ?? null } /** The size of a text as wrapped at WIDTH; a final newline does not start another line. */ export function sizeOf(text) { if (text === '') return 0 const lines = text.replace(/\r\n?/g, '\n').split('\n') if (text.endsWith('\n')) lines.pop() return lines.reduce((sum, line) => sum + Math.max(1, Math.ceil(line.length / WIDTH)), 0) } /** Every covered file under root with its size, keyed by its path from root. */ export function measure(root) { const sizes = {} for (const path of listFiles(root, DIRS, (key) => groupOf(key) !== null)) sizes[toKey(root, path)] = sizeOf(readText(path)) return sizes } const limitOf = (key) => groupOf(key)?.limit ?? 0 /** * Runs the check against the baseline at baselinePath. With update, the * baseline is lowered first. Returns the exit code. */ export function check({ root, baselinePath = join(root, 'scripts/size-baseline.json'), update = false, log = console.log, error = console.error }) { const sizes = measure(root) let baseline = readBaseline(baselinePath, countMap) if (update) { baseline = lowerCounts(baseline, sizes, limitOf) writeBaseline(baselinePath, baseline) } const grown = Object.entries(sizes).filter(([key, n]) => n > Math.max(baseline[key] ?? 0, limitOf(key))) const lowerable = Object.entries(baseline).filter(([key, n]) => (sizes[key] ?? 0) < n) for (const [key, n] of grown) { const entry = baseline[key] error( `FAIL ${key}: ${n} lines (wrapped at ${WIDTH} columns), ` + (entry ? `its baseline is ${entry}. ` : `the limit for a ${groupOf(key).name} file is ${limitOf(key)}. `) + 'Move a concern into a module of its own instead of growing the file or packing its lines.', ) } if (lowerable.length && !update) { log(`${lowerable.length} file(s) are smaller than their baseline now: run with --update to lower it.`) } log(`size: ${Object.keys(sizes).length} file(s), ${Object.keys(baseline).length} past their limit held at their baseline`) return grown.length ? 1 : 0 }