mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-08 18:33:28 +08:00
perf(hooks): eliminate npx overhead and merge biome invocations
- Use local node_modules/.bin/biome binary instead of npx (~200-500ms savings) - Change post-edit-format from `biome format --write` to `biome check --write` (format + lint in one pass) - Skip redundant biome check in quality-gate for JS/TS files already handled by post-edit-format - Fix quality-gate to use findProjectRoot instead of process.cwd() - Export run() function from both hooks for direct invocation - Update tests to match shared resolve-formatter module usage
This commit is contained in:
@@ -7,159 +7,59 @@
|
|||||||
* Runs after Edit tool use. If the edited file is a JS/TS file,
|
* Runs after Edit tool use. If the edited file is a JS/TS file,
|
||||||
* auto-detects the project formatter (Biome or Prettier) by looking
|
* auto-detects the project formatter (Biome or Prettier) by looking
|
||||||
* for config files, then formats accordingly.
|
* for config files, then formats accordingly.
|
||||||
|
*
|
||||||
|
* For Biome, uses `check --write` (format + lint in one pass) to
|
||||||
|
* avoid a redundant second invocation from quality-gate.js.
|
||||||
|
*
|
||||||
|
* Prefers the local node_modules/.bin binary over npx to skip
|
||||||
|
* package-resolution overhead (~200-500ms savings per invocation).
|
||||||
|
*
|
||||||
* Fails silently if no formatter is found or installed.
|
* Fails silently if no formatter is found or installed.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { execFileSync, spawnSync } = require('child_process');
|
const { execFileSync } = require('child_process');
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { getPackageManager } = require('../lib/package-manager');
|
|
||||||
|
const {
|
||||||
|
findProjectRoot,
|
||||||
|
detectFormatter,
|
||||||
|
resolveFormatterBin,
|
||||||
|
} = require('../lib/resolve-formatter');
|
||||||
|
|
||||||
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
||||||
const BIOME_CONFIGS = ['biome.json', 'biome.jsonc'];
|
|
||||||
const PRETTIER_CONFIGS = [
|
|
||||||
'.prettierrc',
|
|
||||||
'.prettierrc.json',
|
|
||||||
'.prettierrc.json5',
|
|
||||||
'.prettierrc.js',
|
|
||||||
'.prettierrc.cjs',
|
|
||||||
'.prettierrc.mjs',
|
|
||||||
'.prettierrc.ts',
|
|
||||||
'.prettierrc.cts',
|
|
||||||
'.prettierrc.mts',
|
|
||||||
'.prettierrc.yml',
|
|
||||||
'.prettierrc.yaml',
|
|
||||||
'.prettierrc.toml',
|
|
||||||
'prettier.config.js',
|
|
||||||
'prettier.config.cjs',
|
|
||||||
'prettier.config.mjs',
|
|
||||||
'prettier.config.ts',
|
|
||||||
'prettier.config.cts',
|
|
||||||
'prettier.config.mts',
|
|
||||||
];
|
|
||||||
const PROJECT_ROOT_MARKERS = ['package.json', ...BIOME_CONFIGS, ...PRETTIER_CONFIGS];
|
|
||||||
let data = '';
|
|
||||||
process.stdin.setEncoding('utf8');
|
|
||||||
|
|
||||||
process.stdin.on('data', chunk => {
|
/**
|
||||||
if (data.length < MAX_STDIN) {
|
* Core logic — exported so run-with-flags.js can call directly
|
||||||
const remaining = MAX_STDIN - data.length;
|
* without spawning a child process.
|
||||||
data += chunk.substring(0, remaining);
|
*
|
||||||
}
|
* @param {string} rawInput - Raw JSON string from stdin
|
||||||
});
|
* @returns {string} The original input (pass-through)
|
||||||
|
*/
|
||||||
function findProjectRoot(startDir) {
|
function run(rawInput) {
|
||||||
let dir = startDir;
|
|
||||||
let fallbackDir = null;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
if (detectFormatter(dir)) {
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fallbackDir && PROJECT_ROOT_MARKERS.some(marker => fs.existsSync(path.join(dir, marker)))) {
|
|
||||||
fallbackDir = dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentDir = path.dirname(dir);
|
|
||||||
if (parentDir === dir) break;
|
|
||||||
dir = parentDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallbackDir || startDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectFormatter(projectRoot) {
|
|
||||||
for (const cfg of BIOME_CONFIGS) {
|
|
||||||
if (fs.existsSync(path.join(projectRoot, cfg))) return 'biome';
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const cfg of PRETTIER_CONFIGS) {
|
|
||||||
if (fs.existsSync(path.join(projectRoot, cfg))) return 'prettier';
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRunnerBin(bin) {
|
|
||||||
if (process.platform !== 'win32') return bin;
|
|
||||||
if (bin === 'npx') return 'npx.cmd';
|
|
||||||
if (bin === 'pnpm') return 'pnpm.cmd';
|
|
||||||
if (bin === 'yarn') return 'yarn.cmd';
|
|
||||||
if (bin === 'bunx') return 'bunx.cmd';
|
|
||||||
return bin;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFormatterRunner(projectRoot) {
|
|
||||||
const pm = getPackageManager({ projectDir: projectRoot });
|
|
||||||
const execCmd = pm?.config?.execCmd || 'npx';
|
|
||||||
const [bin = 'npx', ...prefix] = execCmd.split(/\s+/).filter(Boolean);
|
|
||||||
|
|
||||||
return {
|
|
||||||
bin: getRunnerBin(bin),
|
|
||||||
prefix
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFormatterCommand(formatter, filePath, projectRoot) {
|
|
||||||
const runner = getFormatterRunner(projectRoot);
|
|
||||||
|
|
||||||
if (formatter === 'biome') {
|
|
||||||
return {
|
|
||||||
bin: runner.bin,
|
|
||||||
args: [...runner.prefix, '@biomejs/biome', 'format', '--write', filePath]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (formatter === 'prettier') {
|
|
||||||
return {
|
|
||||||
bin: runner.bin,
|
|
||||||
args: [...runner.prefix, 'prettier', '--write', filePath]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function runFormatterCommand(cmd, projectRoot) {
|
|
||||||
if (process.platform === 'win32' && cmd.bin.endsWith('.cmd')) {
|
|
||||||
const result = spawnSync(cmd.bin, cmd.args, {
|
|
||||||
cwd: projectRoot,
|
|
||||||
shell: true,
|
|
||||||
stdio: 'pipe',
|
|
||||||
timeout: 15000
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
throw result.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof result.status === 'number' && result.status !== 0) {
|
|
||||||
throw new Error(result.stderr?.toString() || `Formatter exited with status ${result.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
execFileSync(cmd.bin, cmd.args, {
|
|
||||||
cwd: projectRoot,
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
timeout: 15000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
process.stdin.on('end', () => {
|
|
||||||
try {
|
try {
|
||||||
const input = JSON.parse(data);
|
const input = JSON.parse(rawInput);
|
||||||
const filePath = input.tool_input?.file_path;
|
const filePath = input.tool_input?.file_path;
|
||||||
|
|
||||||
if (filePath && /\.(ts|tsx|js|jsx)$/.test(filePath)) {
|
if (filePath && /\.(ts|tsx|js|jsx)$/.test(filePath)) {
|
||||||
try {
|
try {
|
||||||
const projectRoot = findProjectRoot(path.dirname(path.resolve(filePath)));
|
const projectRoot = findProjectRoot(path.dirname(path.resolve(filePath)));
|
||||||
const formatter = detectFormatter(projectRoot);
|
const formatter = detectFormatter(projectRoot);
|
||||||
const cmd = getFormatterCommand(formatter, filePath, projectRoot);
|
if (!formatter) return rawInput;
|
||||||
|
|
||||||
if (cmd) {
|
const resolved = resolveFormatterBin(projectRoot, formatter);
|
||||||
runFormatterCommand(cmd, projectRoot);
|
|
||||||
}
|
// Biome: `check --write` = format + lint in one pass
|
||||||
|
// Prettier: `--write` = format only
|
||||||
|
const args =
|
||||||
|
formatter === 'biome'
|
||||||
|
? [...resolved.prefix, 'check', '--write', filePath]
|
||||||
|
: [...resolved.prefix, '--write', filePath];
|
||||||
|
|
||||||
|
execFileSync(resolved.bin, args, {
|
||||||
|
cwd: projectRoot,
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
timeout: 15000,
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
// Formatter not installed, file missing, or failed — non-blocking
|
// Formatter not installed, file missing, or failed — non-blocking
|
||||||
}
|
}
|
||||||
@@ -168,6 +68,26 @@ process.stdin.on('end', () => {
|
|||||||
// Invalid input — pass through
|
// Invalid input — pass through
|
||||||
}
|
}
|
||||||
|
|
||||||
process.stdout.write(data);
|
return rawInput;
|
||||||
process.exit(0);
|
}
|
||||||
});
|
|
||||||
|
// ── stdin entry point (backwards-compatible) ────────────────────
|
||||||
|
if (require.main === module) {
|
||||||
|
let data = '';
|
||||||
|
process.stdin.setEncoding('utf8');
|
||||||
|
|
||||||
|
process.stdin.on('data', (chunk) => {
|
||||||
|
if (data.length < MAX_STDIN) {
|
||||||
|
const remaining = MAX_STDIN - data.length;
|
||||||
|
data += chunk.substring(0, remaining);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stdin.on('end', () => {
|
||||||
|
const result = run(data);
|
||||||
|
process.stdout.write(result);
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { run };
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
* Runs lightweight quality checks after file edits.
|
* Runs lightweight quality checks after file edits.
|
||||||
* - Targets one file when file_path is provided
|
* - Targets one file when file_path is provided
|
||||||
* - Falls back to no-op when language/tooling is unavailable
|
* - Falls back to no-op when language/tooling is unavailable
|
||||||
|
*
|
||||||
|
* For JS/TS files with Biome, this hook is skipped because
|
||||||
|
* post-edit-format.js already runs `biome check --write`.
|
||||||
|
* This hook still handles .json/.md files for Biome, and all
|
||||||
|
* Prettier / Go / Python checks.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
@@ -13,10 +18,15 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { spawnSync } = require('child_process');
|
const { spawnSync } = require('child_process');
|
||||||
|
|
||||||
const MAX_STDIN = 1024 * 1024;
|
const {
|
||||||
let raw = '';
|
findProjectRoot,
|
||||||
|
detectFormatter,
|
||||||
|
resolveFormatterBin,
|
||||||
|
} = require('../lib/resolve-formatter');
|
||||||
|
|
||||||
function run(command, args, cwd = process.cwd()) {
|
const MAX_STDIN = 1024 * 1024;
|
||||||
|
|
||||||
|
function exec(command, args, cwd = process.cwd()) {
|
||||||
return spawnSync(command, args, {
|
return spawnSync(command, args, {
|
||||||
cwd,
|
cwd,
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
@@ -38,31 +48,42 @@ function maybeRunQualityGate(filePath) {
|
|||||||
const strict = String(process.env.ECC_QUALITY_GATE_STRICT || '').toLowerCase() === 'true';
|
const strict = String(process.env.ECC_QUALITY_GATE_STRICT || '').toLowerCase() === 'true';
|
||||||
|
|
||||||
if (['.ts', '.tsx', '.js', '.jsx', '.json', '.md'].includes(ext)) {
|
if (['.ts', '.tsx', '.js', '.jsx', '.json', '.md'].includes(ext)) {
|
||||||
// Prefer biome if present
|
const projectRoot = findProjectRoot(path.dirname(path.resolve(filePath)));
|
||||||
if (fs.existsSync(path.join(process.cwd(), 'biome.json')) || fs.existsSync(path.join(process.cwd(), 'biome.jsonc'))) {
|
const formatter = detectFormatter(projectRoot);
|
||||||
const args = ['biome', 'check', filePath];
|
|
||||||
|
if (formatter === 'biome') {
|
||||||
|
// JS/TS already handled by post-edit-format via `biome check --write`
|
||||||
|
if (['.ts', '.tsx', '.js', '.jsx'].includes(ext)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// .json / .md — still need quality gate
|
||||||
|
const resolved = resolveFormatterBin(projectRoot, 'biome');
|
||||||
|
const args = [...resolved.prefix, 'check', filePath];
|
||||||
if (fix) args.push('--write');
|
if (fix) args.push('--write');
|
||||||
const result = run('npx', args);
|
const result = exec(resolved.bin, args, projectRoot);
|
||||||
if (result.status !== 0 && strict) {
|
if (result.status !== 0 && strict) {
|
||||||
log(`[QualityGate] Biome check failed for ${filePath}`);
|
log(`[QualityGate] Biome check failed for ${filePath}`);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to prettier when installed
|
if (formatter === 'prettier') {
|
||||||
const prettierArgs = ['prettier', '--check', filePath];
|
const resolved = resolveFormatterBin(projectRoot, 'prettier');
|
||||||
if (fix) {
|
const args = [...resolved.prefix, fix ? '--write' : '--check', filePath];
|
||||||
prettierArgs[1] = '--write';
|
const result = exec(resolved.bin, args, projectRoot);
|
||||||
}
|
if (result.status !== 0 && strict) {
|
||||||
const prettier = run('npx', prettierArgs);
|
log(`[QualityGate] Prettier check failed for ${filePath}`);
|
||||||
if (prettier.status !== 0 && strict) {
|
}
|
||||||
log(`[QualityGate] Prettier check failed for ${filePath}`);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No formatter configured — skip
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ext === '.go' && fix) {
|
if (ext === '.go' && fix) {
|
||||||
run('gofmt', ['-w', filePath]);
|
exec('gofmt', ['-w', filePath]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,29 +91,45 @@ function maybeRunQualityGate(filePath) {
|
|||||||
const args = ['format'];
|
const args = ['format'];
|
||||||
if (!fix) args.push('--check');
|
if (!fix) args.push('--check');
|
||||||
args.push(filePath);
|
args.push(filePath);
|
||||||
const r = run('ruff', args);
|
const r = exec('ruff', args);
|
||||||
if (r.status !== 0 && strict) {
|
if (r.status !== 0 && strict) {
|
||||||
log(`[QualityGate] Ruff check failed for ${filePath}`);
|
log(`[QualityGate] Ruff check failed for ${filePath}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
process.stdin.setEncoding('utf8');
|
/**
|
||||||
process.stdin.on('data', chunk => {
|
* Core logic — exported so run-with-flags.js can call directly.
|
||||||
if (raw.length < MAX_STDIN) {
|
*
|
||||||
const remaining = MAX_STDIN - raw.length;
|
* @param {string} rawInput - Raw JSON string from stdin
|
||||||
raw += chunk.substring(0, remaining);
|
* @returns {string} The original input (pass-through)
|
||||||
}
|
*/
|
||||||
});
|
function run(rawInput) {
|
||||||
|
|
||||||
process.stdin.on('end', () => {
|
|
||||||
try {
|
try {
|
||||||
const input = JSON.parse(raw);
|
const input = JSON.parse(rawInput);
|
||||||
const filePath = String(input.tool_input?.file_path || '');
|
const filePath = String(input.tool_input?.file_path || '');
|
||||||
maybeRunQualityGate(filePath);
|
maybeRunQualityGate(filePath);
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore parse errors.
|
// Ignore parse errors.
|
||||||
}
|
}
|
||||||
|
return rawInput;
|
||||||
|
}
|
||||||
|
|
||||||
process.stdout.write(raw);
|
// ── stdin entry point (backwards-compatible) ────────────────────
|
||||||
});
|
if (require.main === module) {
|
||||||
|
let raw = '';
|
||||||
|
process.stdin.setEncoding('utf8');
|
||||||
|
process.stdin.on('data', (chunk) => {
|
||||||
|
if (raw.length < MAX_STDIN) {
|
||||||
|
const remaining = MAX_STDIN - raw.length;
|
||||||
|
raw += chunk.substring(0, remaining);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stdin.on('end', () => {
|
||||||
|
const result = run(raw);
|
||||||
|
process.stdout.write(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { run };
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user