Skip to content

Commit e1e84f5

Browse files
committed
fix(hooks): respect package manager in formatter fallback
1 parent 6090401 commit e1e84f5

2 files changed

Lines changed: 107 additions & 5 deletions

File tree

scripts/hooks/post-edit-format.js

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
const { execFileSync } = require('child_process');
1414
const fs = require('fs');
1515
const path = require('path');
16+
const { getPackageManager } = require('../lib/package-manager');
1617

1718
const MAX_STDIN = 1024 * 1024; // 1MB limit
1819
let data = '';
@@ -60,13 +61,42 @@ function detectFormatter(projectRoot) {
6061
return null;
6162
}
6263

63-
function getFormatterCommand(formatter, filePath) {
64-
const npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
64+
function getRunnerBin(bin) {
65+
if (process.platform !== 'win32') return bin;
66+
67+
if (bin === 'npx') return 'npx.cmd';
68+
if (bin === 'pnpm') return 'pnpm.cmd';
69+
if (bin === 'yarn') return 'yarn.cmd';
70+
if (bin === 'bunx') return 'bunx.cmd';
71+
72+
return bin;
73+
}
74+
75+
function getFormatterRunner(projectRoot) {
76+
const pm = getPackageManager({ projectDir: projectRoot });
77+
const execCmd = pm?.config?.execCmd || 'npx';
78+
const [bin = 'npx', ...prefix] = execCmd.split(/\s+/).filter(Boolean);
79+
80+
return {
81+
bin: getRunnerBin(bin),
82+
prefix
83+
};
84+
}
85+
86+
function getFormatterCommand(formatter, filePath, projectRoot) {
87+
const runner = getFormatterRunner(projectRoot);
88+
6589
if (formatter === 'biome') {
66-
return { bin: npxBin, args: ['@biomejs/biome', 'format', '--write', filePath] };
90+
return {
91+
bin: runner.bin,
92+
args: [...runner.prefix, '@biomejs/biome', 'format', '--write', filePath]
93+
};
6794
}
6895
if (formatter === 'prettier') {
69-
return { bin: npxBin, args: ['prettier', '--write', filePath] };
96+
return {
97+
bin: runner.bin,
98+
args: [...runner.prefix, 'prettier', '--write', filePath]
99+
};
70100
}
71101
return null;
72102
}
@@ -80,7 +110,7 @@ process.stdin.on('end', () => {
80110
try {
81111
const projectRoot = findProjectRoot(path.dirname(path.resolve(filePath)));
82112
const formatter = detectFormatter(projectRoot);
83-
const cmd = getFormatterCommand(formatter, filePath);
113+
const cmd = getFormatterCommand(formatter, filePath, projectRoot);
84114

85115
if (cmd) {
86116
execFileSync(cmd.bin, cmd.args, {

tests/hooks/hooks.test.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,20 @@ function cleanupTestDir(testDir) {
7575
fs.rmSync(testDir, { recursive: true, force: true });
7676
}
7777

78+
function createCommandShim(binDir, name, logFile) {
79+
const shimPath = path.join(binDir, name);
80+
const script = `#!/bin/sh
81+
{
82+
printf '%s\n' "$(basename \"$0\")"
83+
for arg in "$@"; do
84+
printf '%s\n' "$arg"
85+
done
86+
} > ${JSON.stringify(logFile)}
87+
`;
88+
fs.writeFileSync(shimPath, script, { mode: 0o755 });
89+
return shimPath;
90+
}
91+
7892
// Test suite
7993
async function runTests() {
8094
console.log('\n=== Testing Hook Scripts ===\n');
@@ -701,6 +715,64 @@ async function runTests() {
701715
assert.ok(result.stdout.includes('tool_input'), 'Should pass through original data');
702716
})) passed++; else failed++;
703717

718+
if (await asyncTest('uses CLAUDE_PACKAGE_MANAGER runner for formatter fallback', async () => {
719+
const testDir = createTestDir();
720+
const binDir = path.join(testDir, 'bin');
721+
const logFile = path.join(testDir, 'pnpm.log');
722+
fs.mkdirSync(binDir, { recursive: true });
723+
createCommandShim(binDir, 'pnpm', logFile);
724+
725+
const testFile = path.join(testDir, 'src', 'example.ts');
726+
fs.mkdirSync(path.dirname(testFile), { recursive: true });
727+
fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ name: 'pm-env-test' }));
728+
fs.writeFileSync(path.join(testDir, '.prettierrc'), '{}');
729+
fs.writeFileSync(testFile, 'const answer=42;\n');
730+
731+
try {
732+
const stdinJson = JSON.stringify({ tool_input: { file_path: testFile } });
733+
const result = await runScript(path.join(scriptsDir, 'post-edit-format.js'), stdinJson, {
734+
CLAUDE_PACKAGE_MANAGER: 'pnpm',
735+
PATH: `${binDir}${path.delimiter}${process.env.PATH || ''}`
736+
});
737+
738+
assert.strictEqual(result.code, 0, 'Should exit 0 with pnpm fallback');
739+
const logLines = fs.readFileSync(logFile, 'utf8').trim().split('\n');
740+
assert.deepStrictEqual(logLines, ['pnpm', 'dlx', 'prettier', '--write', testFile]);
741+
} finally {
742+
cleanupTestDir(testDir);
743+
}
744+
})) passed++; else failed++;
745+
746+
if (await asyncTest('uses project package manager config for formatter fallback', async () => {
747+
const testDir = createTestDir();
748+
const binDir = path.join(testDir, 'bin');
749+
const logFile = path.join(testDir, 'bunx.log');
750+
fs.mkdirSync(binDir, { recursive: true });
751+
fs.mkdirSync(path.join(testDir, '.claude'), { recursive: true });
752+
createCommandShim(binDir, 'bunx', logFile);
753+
754+
const testFile = path.join(testDir, 'src', 'example.ts');
755+
fs.mkdirSync(path.dirname(testFile), { recursive: true });
756+
fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ name: 'pm-project-test' }));
757+
fs.writeFileSync(path.join(testDir, 'biome.json'), '{}');
758+
fs.writeFileSync(path.join(testDir, '.claude', 'package-manager.json'), JSON.stringify({ packageManager: 'bun' }));
759+
fs.writeFileSync(testFile, 'const answer=42;\n');
760+
761+
try {
762+
const stdinJson = JSON.stringify({ tool_input: { file_path: testFile } });
763+
const result = await runScript(path.join(scriptsDir, 'post-edit-format.js'), stdinJson, {
764+
CLAUDE_PACKAGE_MANAGER: '',
765+
PATH: `${binDir}${path.delimiter}${process.env.PATH || ''}`
766+
});
767+
768+
assert.strictEqual(result.code, 0, 'Should exit 0 with project-config fallback');
769+
const logLines = fs.readFileSync(logFile, 'utf8').trim().split('\n');
770+
assert.deepStrictEqual(logLines, ['bunx', '@biomejs/biome', 'format', '--write', testFile]);
771+
} finally {
772+
cleanupTestDir(testDir);
773+
}
774+
})) passed++; else failed++;
775+
704776
// post-edit-typecheck.js tests
705777
console.log('\npost-edit-typecheck.js:');
706778

0 commit comments

Comments
 (0)