mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
b1da50466d
tsconfig.strict.json is the build config with strictNullChecks and noImplicitAny on. lint:strict runs tsc over it and holds the errors per file to scripts/strict-baseline.json (204 in 109 files today): a file may only lose errors, a new file starts at none, and an entry above its count fails until --update lowers it. An error outside any file or an unreadable tsc run fails closed. It runs in the server job; the build itself stays on strict: false.
111 lines
4.7 KiB
JavaScript
111 lines
4.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/*
|
|
* lint:strict: the strict type errors per file may only go down.
|
|
*
|
|
* The server compiles with strict: false, so a value that can be null or
|
|
* undefined reads as always present, and an untyped parameter is a silent
|
|
* `any` that no lint rule counts. Switching strict on at once is not a change
|
|
* anyone can review, so this runs tsc over tsconfig.strict.json (the build
|
|
* config plus strictNullChecks and noImplicitAny) and counts its errors per
|
|
* file. A file may hold at most its entry in scripts/strict-baseline.json and
|
|
* a file without an entry, a new one in particular, none. An entry above what
|
|
* its file holds now, or for a file that is gone, fails as well until
|
|
* --update lowers it, so a fixed error cannot come back unseen. An error tsc
|
|
* reports outside any file (a broken config) fails the run, and so does a
|
|
* tsc that exits non-zero without one error it can name: the check never
|
|
* passes on output it could not read.
|
|
*
|
|
* npm run lint:strict check against the baseline (CI)
|
|
* npm run lint:strict -- --update lower the baseline to today's counts;
|
|
* it never raises or adds an entry
|
|
*
|
|
* --dir=<path> points it at another server root and --tsc-output=<file> reads
|
|
* a saved `tsc --pretty false` output instead of running tsc (the unit tests
|
|
* use both). The counting lives in scripts/lib/tsc-errors.mjs, the baseline
|
|
* handling in scripts/lib/ratchet.mjs at the repository root.
|
|
*/
|
|
import { execFileSync } from 'node:child_process';
|
|
import { readFileSync } from 'node:fs';
|
|
import { createRequire } from 'node:module';
|
|
import { join, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import {
|
|
countMap,
|
|
lowerCounts,
|
|
RatchetError,
|
|
readBaseline,
|
|
reportStale,
|
|
staleCounts,
|
|
writeBaseline,
|
|
} from '../../scripts/lib/ratchet.mjs';
|
|
import { countErrors } from './lib/tsc-errors.mjs';
|
|
|
|
/** tsc over tsconfig.strict.json, as its output and exit status. */
|
|
function runTsc(serverDir) {
|
|
const tsc = createRequire(join(serverDir, 'package.json')).resolve('typescript/bin/tsc');
|
|
try {
|
|
const output = execFileSync(process.execPath, [tsc, '-p', 'tsconfig.strict.json', '--noEmit', '--pretty', 'false'], {
|
|
cwd: serverDir,
|
|
encoding: 'utf8',
|
|
maxBuffer: 256 * 1024 * 1024,
|
|
});
|
|
return { output, status: 0 };
|
|
} catch (err) {
|
|
if (typeof err.status !== 'number') throw err;
|
|
return { output: `${err.stdout ?? ''}${err.stderr ?? ''}`, status: err.status };
|
|
}
|
|
}
|
|
|
|
const sum = (map) => Object.values(map).reduce((a, b) => a + b, 0);
|
|
|
|
function check(argv) {
|
|
const dirArg = argv.find((a) => a.startsWith('--dir='));
|
|
const outputArg = argv.find((a) => a.startsWith('--tsc-output='));
|
|
const serverDir = dirArg ? resolve(dirArg.slice('--dir='.length)) : fileURLToPath(new URL('..', import.meta.url));
|
|
const baselinePath = join(serverDir, 'scripts', 'strict-baseline.json');
|
|
|
|
let baseline = readBaseline(baselinePath, countMap);
|
|
const { output, status } = outputArg
|
|
? { output: readFileSync(resolve(outputArg.slice('--tsc-output='.length)), 'utf8'), status: null }
|
|
: runTsc(serverDir);
|
|
const { counts, global } = countErrors(output);
|
|
if (global.length) {
|
|
throw new RatchetError(`tsc reported errors outside any file:\n ${global.join('\n ')}`);
|
|
}
|
|
if (status !== null && status !== 0 && !Object.keys(counts).length) {
|
|
throw new RatchetError(`tsc exited with ${status} without an error it could name:\n${output.slice(0, 2000)}`);
|
|
}
|
|
|
|
if (argv.includes('--update')) {
|
|
baseline = lowerCounts(baseline, counts);
|
|
writeBaseline(baselinePath, baseline);
|
|
}
|
|
|
|
const grown = Object.entries(counts).filter(([file, n]) => n > (baseline[file] ?? 0));
|
|
for (const [file, n] of grown) {
|
|
const entry = baseline[file];
|
|
console.error(
|
|
`FAIL ${file}: ${n} strict error(s), ` +
|
|
(entry ? `its baseline is ${entry}. ` : 'a file without a baseline entry may have none. ') +
|
|
'Run npx tsc -p tsconfig.strict.json --noEmit to see them; narrow the null case or type the parameter.',
|
|
);
|
|
}
|
|
const stale = staleCounts(baseline, counts);
|
|
reportStale(stale, { file: 'strict-baseline.json', command: 'lint:strict', root: serverDir, error: console.error });
|
|
console.log(
|
|
`strict: ${sum(counts)} error(s) in ${Object.keys(counts).length} file(s), baseline ${sum(baseline)} in ${Object.keys(baseline).length}`,
|
|
);
|
|
return grown.length || stale.length ? 1 : 0;
|
|
}
|
|
|
|
const isCli = Boolean(process.argv[1]) && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
if (isCli) {
|
|
try {
|
|
process.exitCode = check(process.argv.slice(2));
|
|
} catch (err) {
|
|
if (!(err instanceof RatchetError)) throw err;
|
|
console.error(`FAIL lint:strict: ${err.message}`);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|