137 lines
3.3 KiB
JavaScript
Executable File
137 lines
3.3 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
const { execSync } = require('child_process');
|
|
const { readFileSync } = require('fs');
|
|
const { basename, relative } = require('path');
|
|
|
|
/**
|
|
* @typedef {Object} HookInput
|
|
* @property {string} [cwd]
|
|
* @property {string} [file_path]
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} ESLintMessage
|
|
* @property {number} line
|
|
* @property {number} column
|
|
* @property {string} message
|
|
* @property {number} severity
|
|
* @property {string} [ruleId]
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} ESLintResult
|
|
* @property {ESLintMessage[]} [messages]
|
|
*/
|
|
|
|
// Read hook input from stdin
|
|
/** @type {HookInput} */
|
|
let input;
|
|
try {
|
|
input = JSON.parse(readFileSync(0, 'utf-8'));
|
|
} catch {
|
|
// Invalid JSON, skip gracefully
|
|
console.log(JSON.stringify({ continue: true }));
|
|
process.exit(0);
|
|
}
|
|
|
|
const { cwd, file_path } = input;
|
|
|
|
// Validate required file_path field
|
|
if (!file_path) {
|
|
console.log(JSON.stringify({ continue: true }));
|
|
process.exit(0);
|
|
}
|
|
|
|
// Only process JS/TS files (including .d.ts declaration files)
|
|
const JS_TS_EXTENSIONS = /\.(ts|tsx|js|jsx|cjs|mjs|mts|cts)$/i;
|
|
if (!JS_TS_EXTENSIONS.test(file_path)) {
|
|
console.log(JSON.stringify({ continue: true }));
|
|
process.exit(0);
|
|
}
|
|
|
|
// Check if eslint is available
|
|
try {
|
|
execSync('npx eslint --version', {
|
|
cwd,
|
|
stdio: 'ignore',
|
|
});
|
|
} catch {
|
|
// ESLint not available, skip silently
|
|
console.log(JSON.stringify({ continue: true }));
|
|
process.exit(0);
|
|
}
|
|
|
|
// Run eslint --fix with JSON output
|
|
// Use relative path to avoid ESLint base path issues
|
|
const relativePath = cwd ? relative(cwd, file_path) : file_path;
|
|
/** @type {string} */
|
|
let output;
|
|
try {
|
|
output = execSync(`npx eslint --fix --format json "${relativePath}"`, {
|
|
cwd,
|
|
encoding: 'utf-8',
|
|
stdio: 'pipe',
|
|
});
|
|
} catch (error) {
|
|
// ESLint exits with non-zero when errors exist
|
|
const stdout =
|
|
error && typeof error === 'object' && 'stdout' in error
|
|
? error.stdout
|
|
: null;
|
|
output = (typeof stdout === 'string' ? stdout : null) || '[]';
|
|
}
|
|
|
|
// Parse results and extract remaining errors
|
|
/** @type {ESLintResult[]} */
|
|
let results;
|
|
try {
|
|
const parsed = JSON.parse(output);
|
|
results = Array.isArray(parsed) ? parsed : [];
|
|
} catch {
|
|
// Invalid JSON, assume no errors
|
|
results = [];
|
|
}
|
|
|
|
const errors =
|
|
results[0]?.messages?.filter(
|
|
(/** @type {ESLintMessage} */ msg) => msg.severity === 2,
|
|
) || [];
|
|
|
|
// Format and report errors if any exist
|
|
if (errors.length > 0) {
|
|
const fileName = basename(file_path);
|
|
const MAX_ERRORS_TO_SHOW = 5;
|
|
|
|
let errorList;
|
|
if (errors.length > MAX_ERRORS_TO_SHOW) {
|
|
const shown = errors.slice(0, MAX_ERRORS_TO_SHOW);
|
|
errorList = shown
|
|
.map(
|
|
(/** @type {ESLintMessage} */ e) =>
|
|
`Line ${e.line}:${e.column}: ${e.message} (${e.ruleId || 'unknown'})`,
|
|
)
|
|
.join('\n');
|
|
errorList += `\n...and ${errors.length - MAX_ERRORS_TO_SHOW} more error(s)`;
|
|
} else {
|
|
errorList = errors
|
|
.map(
|
|
(/** @type {ESLintMessage} */ e) =>
|
|
`Line ${e.line}:${e.column}: ${e.message} (${e.ruleId || 'unknown'})`,
|
|
)
|
|
.join('\n');
|
|
}
|
|
|
|
const message = `ESLint found ${errors.length} error(s) in ${fileName}:\n${errorList}`;
|
|
|
|
console.log(
|
|
JSON.stringify({
|
|
continue: true,
|
|
systemMessage: message,
|
|
}),
|
|
);
|
|
} else {
|
|
console.log(JSON.stringify({ continue: true }));
|
|
}
|
|
|
|
process.exit(0);
|