From 8baf239759daffc7a211725021b71644f96bed19 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 12 May 2026 22:00:43 +0200 Subject: [PATCH] feat(deps): pre-commit type-check on staged TS files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .lintstagedrc.json | 2 +- scripts/tsc-staged.mjs | 55 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 scripts/tsc-staged.mjs diff --git a/.lintstagedrc.json b/.lintstagedrc.json index 34d14f96..06a3befd 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -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"] } diff --git a/scripts/tsc-staged.mjs b/scripts/tsc-staged.mjs new file mode 100644 index 00000000..65964193 --- /dev/null +++ b/scripts/tsc-staged.mjs @@ -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);