#!/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, mkdirSync } from 'node:fs'; 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); } // Temp tsconfig lives inside the project tree (not /tmp) so @types/* // resolution walks up to node_modules. tsc's "atTypes" auto-discovery // is anchored to the tsconfig's directory, so a temp config in /tmp // would miss our @types/node, @types/react, etc. const baseDir = join(cwd, 'node_modules/.cache/tsc-staged'); mkdirSync(baseDir, { recursive: true }); const tmpDir = mkdtempSync(join(baseDir, 'run-')); const tmpConfig = join(tmpDir, 'tsconfig.json'); const relFiles = files.map((f) => relative(tmpDir, resolve(cwd, f))); // Pull in the project's ambient .d.ts files (css module shim, // react-pdf JSX augment, etc.) so side-effect imports like // `import 'react-pdf/dist/Page/AnnotationLayer.css'` resolve under the // staged-only compile. Without this, `include: []` would shut out // everything in src/types/ and tsc reports TS2882 for any CSS import. const ambientTypesGlob = relative(tmpDir, join(cwd, 'src/types')) + '/**/*.d.ts'; writeFileSync( tmpConfig, JSON.stringify( { extends: relative(tmpDir, join(cwd, 'tsconfig.json')), compilerOptions: { noEmit: true, skipLibCheck: true, // Explicitly list `types` so the @types/* auto-discovery // finds them — without this, the temp-tsconfig location // anchors discovery to .cache/ and misses node/react/etc. types: ['node', 'react', 'react-dom'], }, files: relFiles, include: [ambientTypesGlob], }, 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);