diff --git a/scripts/hooks/post-edit-format.js b/scripts/hooks/post-edit-format.js index 2f1d0334b..e2c77188d 100644 --- a/scripts/hooks/post-edit-format.js +++ b/scripts/hooks/post-edit-format.js @@ -15,6 +15,29 @@ const fs = require('fs'); const path = require('path'); 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'); @@ -27,33 +50,26 @@ process.stdin.on('data', chunk => { function findProjectRoot(startDir) { let dir = startDir; - while (dir !== path.dirname(dir)) { - if (fs.existsSync(path.join(dir, 'package.json'))) return dir; - dir = path.dirname(dir); + + while (true) { + if (PROJECT_ROOT_MARKERS.some(marker => fs.existsSync(path.join(dir, marker)))) { + return dir; + } + + const parentDir = path.dirname(dir); + if (parentDir === dir) break; + dir = parentDir; } + return startDir; } function detectFormatter(projectRoot) { - const biomeConfigs = ['biome.json', 'biome.jsonc']; - for (const cfg of biomeConfigs) { + for (const cfg of BIOME_CONFIGS) { if (fs.existsSync(path.join(projectRoot, cfg))) return 'biome'; } - const prettierConfigs = [ - '.prettierrc', - '.prettierrc.json', - '.prettierrc.js', - '.prettierrc.cjs', - '.prettierrc.mjs', - '.prettierrc.yml', - '.prettierrc.yaml', - '.prettierrc.toml', - 'prettier.config.js', - 'prettier.config.cjs', - 'prettier.config.mjs', - ]; - for (const cfg of prettierConfigs) { + for (const cfg of PRETTIER_CONFIGS) { if (fs.existsSync(path.join(projectRoot, cfg))) return 'prettier'; } diff --git a/tests/hooks/hooks.test.js b/tests/hooks/hooks.test.js index 2c8732042..31d486d35 100644 --- a/tests/hooks/hooks.test.js +++ b/tests/hooks/hooks.test.js @@ -701,6 +701,49 @@ async function runTests() { assert.ok(result.stdout.includes('tool_input'), 'Should pass through original data'); })) passed++; else failed++; + if (await asyncTest('finds formatter config in parent dirs even without package.json', async () => { + const testDir = createTestDir(); + const repoRoot = path.join(testDir, 'config-only-repo'); + const nestedDir = path.join(repoRoot, 'src', 'nested'); + const targetFile = path.join(nestedDir, 'file.ts'); + const binDir = path.join(testDir, 'bin'); + const logFile = path.join(testDir, 'npx-log.json'); + + fs.mkdirSync(nestedDir, { recursive: true }); + fs.mkdirSync(binDir, { recursive: true }); + fs.writeFileSync(path.join(repoRoot, 'biome.json'), '{}\n'); + fs.writeFileSync(targetFile, 'const value = 1;\n'); + fs.writeFileSync( + path.join(binDir, 'npx'), + `#!/usr/bin/env node\nconst fs = require('fs');\nfs.writeFileSync(${JSON.stringify(logFile)}, JSON.stringify({ cwd: process.cwd(), args: process.argv.slice(2) }));\n`, + { mode: 0o755 } + ); + + try { + const stdinJson = JSON.stringify({ tool_input: { file_path: targetFile } }); + const result = await runScript(path.join(scriptsDir, 'post-edit-format.js'), stdinJson, { + PATH: `${binDir}${path.delimiter}${process.env.PATH || ''}`, + }); + + assert.strictEqual(result.code, 0, 'Should exit 0 for config-only repos'); + assert.ok(fs.existsSync(logFile), 'Should invoke formatter when parent biome config exists'); + + const invocation = JSON.parse(fs.readFileSync(logFile, 'utf8')); + assert.strictEqual( + fs.realpathSync(invocation.cwd), + fs.realpathSync(repoRoot), + 'Should run formatter from discovered repo root' + ); + assert.deepStrictEqual( + invocation.args, + ['@biomejs/biome', 'format', '--write', targetFile], + 'Should invoke biome formatter for config-only repo' + ); + } finally { + cleanupTestDir(testDir); + } + })) passed++; else failed++; + // post-edit-typecheck.js tests console.log('\npost-edit-typecheck.js:');