Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 35 additions & 19 deletions scripts/hooks/post-edit-format.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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';
}

Expand Down
43 changes: 43 additions & 0 deletions tests/hooks/hooks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 || ''}`,
});
Comment on lines +716 to +726
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make the formatter shim work on Windows too.

Lines 716-726 only create an npx stub, but scripts/hooks/post-edit-format.js resolves npx.cmd on Windows. The hook will miss this shim there, so the new assertion on logFile becomes platform-dependent.

🪟 Suggested fix
+    const shimJs = path.join(binDir, 'npx-shim.js');
+    fs.writeFileSync(
+      shimJs,
+      `const fs = require('fs');\nfs.writeFileSync(${JSON.stringify(logFile)}, JSON.stringify({ cwd: process.cwd(), args: process.argv.slice(2) }));\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`,
+      `#!/usr/bin/env node\nrequire(${JSON.stringify(shimJs)});\n`,
       { mode: 0o755 }
     );
+    fs.writeFileSync(
+      path.join(binDir, 'npx.cmd'),
+      `@echo off\r\nnode "%~dp0\\npx-shim.js" %*\r\n`,
+      { mode: 0o755 }
+    );

As per coding guidelines, "Ensure cross-platform compatibility for Windows, macOS, and Linux via Node.js scripts".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 || ''}`,
});
const shimJs = path.join(binDir, 'npx-shim.js');
fs.writeFileSync(
shimJs,
`const fs = require('fs');\nfs.writeFileSync(${JSON.stringify(logFile)}, JSON.stringify({ cwd: process.cwd(), args: process.argv.slice(2) }));\n`
);
fs.writeFileSync(
path.join(binDir, 'npx'),
`#!/usr/bin/env node\nrequire(${JSON.stringify(shimJs)});\n`,
{ mode: 0o755 }
);
fs.writeFileSync(
path.join(binDir, 'npx.cmd'),
`@echo off\r\nnode "%~dp0\\npx-shim.js" %*\r\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 || ''}`,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/hooks/hooks.test.js` around lines 716 - 726, The test only writes a
Unix `npx` shim (fs.writeFileSync(path.join(binDir, 'npx'), ...)) but the hook
script (scripts/hooks/post-edit-format.js) resolves `npx.cmd` on Windows; update
the test to create the Windows shim as well (e.g., write a corresponding
`npx.cmd` file in binDir that writes the same logFile) or write both `npx` and
`npx.cmd` unconditionally so runScript and post-edit-format.js find the shim on
all platforms; modify the fs.writeFileSync calls that create the shim(s) in the
test near runScript to include the Windows variant (ensure executable/mode
behavior is handled appropriately).


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:');

Expand Down
Loading