mirror of
https://github.com/affaan-m/ECC.git
synced 2026-11-07 08:35:54 +00:00
config-protection.js and post-edit-typecheck.js attached process.stdin listeners at module scope. tests/hooks/hook-input.test.js requires config-protection.js, so that test never exits while stdin stays open. Attach the listeners only when the script runs directly (require.main === module), as stop-format-typecheck.js and quality-gate.js already do. post-edit-typecheck.js picked tsc diagnostics with line.includes(candidate), so editing a root index.ts reported the errors of src/index.ts, and a Windows relative path (src\index.ts) never matched tsc's forward-slash output. Compare the file at the start of each diagnostic instead, the same check diagnosticMatchesFile() does in stop-format-typecheck.js. Refs #3821
121 lines
4.1 KiB
JavaScript
121 lines
4.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* PostToolUse Hook: TypeScript check after editing .ts/.tsx files
|
|
*
|
|
* Cross-platform (Windows, macOS, Linux)
|
|
*
|
|
* Runs after Edit tool use on TypeScript files. Walks up from the file's
|
|
* directory to find the nearest tsconfig.json, then runs tsc --noEmit
|
|
* and reports only errors related to the edited file.
|
|
*/
|
|
|
|
const { execFileSync } = require("child_process");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const MAX_DIRECT_STDIN_BYTES = 16 * 1024 * 1024;
|
|
|
|
// tsc --pretty false starts each diagnostic with the file it is about:
|
|
// `src/a.ts(1,14): error TS2322: ...`. Compare that path instead of looking
|
|
// for the edited path anywhere in the line, which let a root `index.ts` claim
|
|
// the errors of `src/index.ts`. Same check as diagnosticMatchesFile() in
|
|
// stop-format-typecheck.js.
|
|
function diagnosticMatchesFile(line, paths) {
|
|
const location = /^(.+?)\(\d+,\d+\): /.exec(line);
|
|
if (!location) return false;
|
|
const file = location[1].replace(/\\/g, "/");
|
|
for (const candidate of paths) {
|
|
if (file === candidate.replace(/\\/g, "/")) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (require.main === module) {
|
|
let data = "";
|
|
let stdinBytes = 0;
|
|
let oversized = false;
|
|
process.stdin.setEncoding("utf8");
|
|
|
|
process.stdin.on("data", (chunk) => {
|
|
if (oversized) return;
|
|
stdinBytes += Buffer.byteLength(chunk, "utf8");
|
|
if (stdinBytes > MAX_DIRECT_STDIN_BYTES) {
|
|
data = "";
|
|
oversized = true;
|
|
return;
|
|
}
|
|
data += chunk;
|
|
});
|
|
|
|
process.stdin.on("end", () => {
|
|
if (oversized) {
|
|
process.exit(0);
|
|
return;
|
|
}
|
|
try {
|
|
const input = JSON.parse(data);
|
|
const filePath = input.tool_input?.file_path;
|
|
|
|
if (filePath && /\.(ts|tsx)$/.test(filePath)) {
|
|
const resolvedPath = path.resolve(filePath);
|
|
if (!fs.existsSync(resolvedPath)) {
|
|
process.stdout.write(data, () => process.exit(0));
|
|
return;
|
|
}
|
|
// Find nearest tsconfig.json by walking up (max 20 levels to prevent infinite loop)
|
|
let dir = path.dirname(resolvedPath);
|
|
const root = path.parse(dir).root;
|
|
let depth = 0;
|
|
|
|
while (dir !== root && depth < 20) {
|
|
if (fs.existsSync(path.join(dir, "tsconfig.json"))) {
|
|
break;
|
|
}
|
|
dir = path.dirname(dir);
|
|
depth++;
|
|
}
|
|
|
|
if (fs.existsSync(path.join(dir, "tsconfig.json"))) {
|
|
try {
|
|
// Use npx.cmd on Windows to avoid shell: true which enables command injection
|
|
const npxBin = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
execFileSync(npxBin, ["tsc", "--noEmit", "--pretty", "false"], {
|
|
cwd: dir,
|
|
encoding: "utf8",
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
timeout: 30000,
|
|
});
|
|
} catch (err) {
|
|
// tsc exits non-zero when there are errors — filter to edited file
|
|
const output = (err.stdout || "") + (err.stderr || "");
|
|
// Compute paths that uniquely identify the edited file.
|
|
// tsc output uses paths relative to its cwd (the tsconfig dir),
|
|
// so check for the relative path, absolute path, and original path.
|
|
// Avoid bare basename matching — it causes false positives when
|
|
// multiple files share the same name (e.g., src/utils.ts vs tests/utils.ts).
|
|
const relPath = path.relative(dir, resolvedPath);
|
|
const candidates = new Set([filePath, resolvedPath, relPath]);
|
|
const relevantLines = output
|
|
.split("\n")
|
|
.filter((line) => diagnosticMatchesFile(line, candidates))
|
|
.slice(0, 10);
|
|
|
|
if (relevantLines.length > 0) {
|
|
console.error(
|
|
"[Hook] TypeScript errors in " + path.basename(filePath) + ":",
|
|
);
|
|
relevantLines.forEach((line) => console.error(line));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// Invalid input — pass through
|
|
}
|
|
|
|
process.stdout.write(data, () => process.exit(0));
|
|
});
|
|
}
|
|
|
|
module.exports = { diagnosticMatchesFile };
|