mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-28 02:53:15 -04:00
fix: use ENOENT check instead of which.sync for command-not-found on Windows (#11004)
* fix: use ENOENT check instead of which.sync for command-not-found on Windows On Windows, `which.sync()` only checks if a command exists in PATH, not whether it actually executed successfully. This caused false "Command not found" errors when a command exists but exits with a non-zero code. Use the same `spawn ENOENT` check across all platforms, which is reliable thanks to cross-spawn used by execa. Closes #11000 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve prependPaths against exec prefix for correct Windows command lookup The previous ENOENT-only approach doesn't work on Windows because execa 9.x uses cross-spawn only for command parsing, not spawning. This means cross-spawn's ENOENT hook (hookChildProcess) never fires, and non-existent commands wrapped as `cmd.exe /c <command>` exit with code 1 instead of emitting ENOENT. Restore the which.sync fallback for Windows, but fix the original #11000 bug by resolving relative prependPaths (like ./node_modules/.bin) against the exec prefix instead of relying on process.cwd(). This ensures correct path resolution in --filter contexts where the command runs in a different package directory. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: zubeyralmaho <zubeyralmaho@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
6
.changeset/fix-windows-exec-command-not-found.md
Normal file
6
.changeset/fix-windows-exec-command-not-found.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@pnpm/exec.commands": patch
|
||||||
|
"pnpm": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixed false "Command not found" error on Windows when the command exists but exits with a non-zero exit code [#11000](https://github.com/pnpm/pnpm/issues/11000).
|
||||||
@@ -296,7 +296,7 @@ export async function handler (
|
|||||||
result[prefix].status = 'passed'
|
result[prefix].status = 'passed'
|
||||||
result[prefix].duration = getExecutionDuration(startTime)
|
result[prefix].duration = getExecutionDuration(startTime)
|
||||||
} catch (err: any) { // eslint-disable-line
|
} catch (err: any) { // eslint-disable-line
|
||||||
if (isErrorCommandNotFound(params[0], err, prependPaths)) {
|
if (isErrorCommandNotFound(params[0], err, prefix, prependPaths)) {
|
||||||
err.message = `Command "${params[0]}" not found`
|
err.message = `Command "${params[0]}" not found`
|
||||||
err.hint = await createExecCommandNotFoundHint(params[0], {
|
err.hint = await createExecCommandNotFoundHint(params[0], {
|
||||||
implicitlyFellbackFromRun: opts.implicitlyFellbackFromRun ?? false,
|
implicitlyFellbackFromRun: opts.implicitlyFellbackFromRun ?? false,
|
||||||
@@ -394,19 +394,20 @@ interface CommandError extends Error {
|
|||||||
shortMessage: string
|
shortMessage: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function isErrorCommandNotFound (command: string, error: CommandError, prependPaths: string[]): boolean {
|
function isErrorCommandNotFound (command: string, error: CommandError, prefix: string, prependPaths: string[]): boolean {
|
||||||
// Mac/Linux
|
if (error.originalMessage === `spawn ${command} ENOENT`) {
|
||||||
if (process.platform === 'linux' || process.platform === 'darwin') {
|
return true
|
||||||
return error.originalMessage === `spawn ${command} ENOENT`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Windows
|
// On Windows, execa 9.x uses cross-spawn only for command parsing (not spawning),
|
||||||
|
// so cross-spawn's ENOENT hook never fires. Non-existent commands get wrapped as
|
||||||
|
// `cmd.exe /c <command>` which exits with code 1 instead of emitting ENOENT.
|
||||||
|
// Fall back to checking if the command exists in PATH, resolving relative paths
|
||||||
|
// against the exec prefix to correctly handle --filter contexts.
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
const { value: path } = prependDirsToPath(prependPaths)
|
const absolutePrependPaths = prependPaths.map(p => path.resolve(prefix, p))
|
||||||
return !which.sync(command, {
|
const { value: searchPath } = prependDirsToPath(absolutePrependPaths)
|
||||||
nothrow: true,
|
return !which.sync(command, { nothrow: true, path: searchPath })
|
||||||
path,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
|||||||
Reference in New Issue
Block a user