Files
TREK/client/scripts/lib/size.mjs
T
Maurice 9d596d9916 ci(client): lint:size measures the Playwright specs and fixtures too
The size check walked src/ and tests/ only, while lint:format and
lint:skips already cover e2e/. The help center fixtures under
e2e/help/fixtures.ts hold 2291 wrapped lines, past the 2000 a test may
reach, and could grow without bound. e2e/ is now measured with the test
limit; fixtures.ts enters the baseline at its current size, every other
file there is within the limit.
2026-10-08 13:04:41 +02:00

107 lines
3.5 KiB
JavaScript

/*
* 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. The
* Playwright specs and their fixtures under e2e/ count as tests.
*/
export const GROUPS = [
{
name: 'test',
limit: 2000,
accepts: (key) =>
/\.tsx?$/.test(key) && (key.startsWith('tests/') || key.startsWith('e2e/') || 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', 'e2e'];
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;
}