feat(deps): pre-commit type-check on staged TS files
Pre-commit now runs `tsc` against the staged ts/tsx files (and their dep graph) in ~3s, catching type errors before they hit CI. Used to skip type-check entirely on pre-commit because full-project tsc is ~22s — too slow for the commit hook. Drops a 30-LOC shim in `scripts/tsc-staged.mjs` instead of the `tsc-files` package: that lib's binary-resolution path (`typescript/../.bin/tsc`) doesn't exist under pnpm's virtual-store layout, so spawnSync returns `status: null` and the check silently no-ops. Filed upstream-style: the package hasn't shipped in 3 years. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
|
||||
"*.{ts,tsx}": ["eslint --fix", "prettier --write", "node scripts/tsc-staged.mjs"],
|
||||
"*.{json,md,css}": ["prettier --write"]
|
||||
}
|
||||
|
||||
55
scripts/tsc-staged.mjs
Normal file
55
scripts/tsc-staged.mjs
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Pre-commit type check for staged TS files.
|
||||
*
|
||||
* Writes a temp tsconfig that extends the project root and pins
|
||||
* `files` to whatever lint-staged passed in. `tsc -p` then compiles
|
||||
* the whole dep graph from those entrypoints — catches errors in
|
||||
* the staged code AND in anything it imports — while still skipping
|
||||
* the 22s full-project pass.
|
||||
*
|
||||
* Replaces `tsc-files` (npm), which silently fails under pnpm because
|
||||
* its tsc-resolution path (typescript/../.bin/tsc) doesn't exist in
|
||||
* pnpm's virtual store layout.
|
||||
*/
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, relative, resolve } from 'node:path';
|
||||
|
||||
const cwd = process.cwd();
|
||||
const args = process.argv.slice(2);
|
||||
const files = args.filter((a) => /\.(ts|tsx)$/.test(a));
|
||||
|
||||
if (files.length === 0) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), 'tsc-staged-'));
|
||||
const tmpConfig = join(tmpDir, 'tsconfig.json');
|
||||
|
||||
const relFiles = files.map((f) => relative(tmpDir, resolve(cwd, f)));
|
||||
|
||||
writeFileSync(
|
||||
tmpConfig,
|
||||
JSON.stringify(
|
||||
{
|
||||
extends: relative(tmpDir, join(cwd, 'tsconfig.json')),
|
||||
compilerOptions: { noEmit: true, skipLibCheck: true },
|
||||
files: relFiles,
|
||||
include: [],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
const tsc = spawnSync('pnpm', ['exec', 'tsc', '-p', tmpConfig, '--pretty'], {
|
||||
cwd,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
|
||||
process.exit(tsc.status ?? 1);
|
||||
Reference in New Issue
Block a user