Initial commit
This commit is contained in:
139
hooks/tsc-check.ts
Normal file
139
hooks/tsc-check.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* PostToolUse hook that runs TypeScript type checking on edited files.
|
||||
* Performs single-file type checking for fast feedback during implementation.
|
||||
*
|
||||
* Git-aware: Only processes files that are tracked by git or staged.
|
||||
*
|
||||
* Exit codes:
|
||||
* - 0: Success (no type errors, or unsupported file type)
|
||||
* - 2: Blocking error (type errors found, shown to Claude)
|
||||
*/
|
||||
|
||||
import { spawn } from "bun";
|
||||
import { TSC_SUPPORTED_EXTENSIONS } from "./shared/constants";
|
||||
import { isFileInRepo } from "./shared/git-utils";
|
||||
import {
|
||||
extractFilePaths,
|
||||
parseHookInput,
|
||||
type TscError,
|
||||
type TscParseResult,
|
||||
} from "./shared/types";
|
||||
|
||||
/**
|
||||
* Parse TypeScript compiler output into structured format.
|
||||
*
|
||||
* @param output - Raw stdout/stderr from tsc command
|
||||
* @returns Structured error data with count and detailed error array
|
||||
*/
|
||||
export function parseTscOutput(output: string): TscParseResult {
|
||||
const errors: TscError[] = [];
|
||||
|
||||
// TSC output format: file(line,col): error TS1234: message
|
||||
const errorPattern = /^(.+?)\((\d+),(\d+)\):\s*error\s+TS\d+:\s*(.+)$/gm;
|
||||
const matches = output.matchAll(errorPattern);
|
||||
|
||||
for (const match of matches) {
|
||||
const [, file, line, col, message] = match;
|
||||
if (file && line && col && message) {
|
||||
errors.push({
|
||||
file,
|
||||
line: Number.parseInt(line, 10),
|
||||
col: Number.parseInt(col, 10),
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { errorCount: errors.length, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* Format errors for Claude-friendly output.
|
||||
*
|
||||
* @param parsed - Parsed TSC output
|
||||
* @param filePath - Path to filter errors by
|
||||
* @returns Formatted error string
|
||||
*/
|
||||
function formatErrors(parsed: TscParseResult, filePath: string): string {
|
||||
// Filter to only errors in the edited file
|
||||
const fileErrors = parsed.errors.filter(
|
||||
(e) => e.file === filePath || e.file.endsWith(filePath),
|
||||
);
|
||||
|
||||
if (fileErrors.length === 0) return "";
|
||||
|
||||
const lines: string[] = [
|
||||
`${fileErrors.length} type error(s) in ${filePath}:`,
|
||||
];
|
||||
|
||||
for (const e of fileErrors) {
|
||||
lines.push(` ${e.file}:${e.line}:${e.col} - ${e.message}`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const input = await Bun.stdin.text();
|
||||
const hookInput = parseHookInput(input);
|
||||
|
||||
if (!hookInput) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const filePaths = extractFilePaths(hookInput);
|
||||
|
||||
if (filePaths.length === 0) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Process each file
|
||||
const allErrors: string[] = [];
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
// Skip non-TypeScript files
|
||||
if (!TSC_SUPPORTED_EXTENSIONS.some((ext) => filePath.endsWith(ext))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Git-aware: Skip files outside the git repository
|
||||
const inRepo = await isFileInRepo(filePath);
|
||||
if (!inRepo) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Run tsc --noEmit on the single file
|
||||
const proc = spawn({
|
||||
cmd: ["bunx", "tsc", "--noEmit", "--pretty", "false", filePath],
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const exitCode = await proc.exited;
|
||||
const stdout = await new Response(proc.stdout).text();
|
||||
const stderr = await new Response(proc.stderr).text();
|
||||
const output = `${stdout}\n${stderr}`;
|
||||
|
||||
if (exitCode !== 0) {
|
||||
const parsed = parseTscOutput(output);
|
||||
const formatted = formatErrors(parsed, filePath);
|
||||
if (formatted) {
|
||||
allErrors.push(formatted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allErrors.length > 0) {
|
||||
console.error(`TypeScript type errors:\n${allErrors.join("\n\n")}`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Only run main() when executed directly, not when imported by tests
|
||||
if (import.meta.main) {
|
||||
main();
|
||||
}
|
||||
Reference in New Issue
Block a user