Skip to content

Commit b4e2086

Browse files
committed
fix(spawn): preserve path for Windows script files with absolute paths
When spawning .cmd/.bat/.ps1 files on Windows with shell: true, the spawn function was stripping the path and extension to let cmd.exe resolve the command via PATH+PATHEXT. This works for binaries in PATH but fails for files in temp directories or other locations not in PATH. Now only strips the extension when the command is a bare name without any path components. Absolute and relative paths are kept intact so cmd.exe executes the exact file. Fixes test failure: "should invalidate cache when binary no longer exists"
1 parent 1e056e6 commit b4e2086

File tree

1 file changed

+24
-6
lines changed

1 file changed

+24
-6
lines changed

src/spawn.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -719,9 +719,18 @@ export function spawn(
719719
// Inline WIN32 constant for coverage mode compatibility
720720
const WIN32 = process.platform === 'win32'
721721
if (WIN32 && shell && windowsScriptExtRegExp.test(actualCmd)) {
722-
// path is imported at the top
723-
// Extract just the command name without path and extension.
724-
actualCmd = getPath().basename(actualCmd, getPath().extname(actualCmd))
722+
// Only strip the extension if the command doesn't contain a path.
723+
// If it's an absolute or relative path, keep it intact so cmd.exe
724+
// executes the exact file. Stripping would fail for files in directories
725+
// not in PATH (e.g., temp directories, project-local bins).
726+
const hasPath =
727+
actualCmd.includes('/') ||
728+
actualCmd.includes('\\') ||
729+
/^[a-zA-Z]:/.test(actualCmd)
730+
if (!hasPath) {
731+
// Extract just the command name without extension for PATH lookup.
732+
actualCmd = getPath().basename(actualCmd, getPath().extname(actualCmd))
733+
}
725734
}
726735
// The stdio option can be a string or an array.
727736
// https://nodejs.org/api/child_process.html#optionsstdio
@@ -894,9 +903,18 @@ export function spawnSync(
894903
// Inline WIN32 constant for coverage mode compatibility
895904
const WIN32 = process.platform === 'win32'
896905
if (WIN32 && shell && windowsScriptExtRegExp.test(actualCmd)) {
897-
// path is imported at the top
898-
// Extract just the command name without path and extension.
899-
actualCmd = getPath().basename(actualCmd, getPath().extname(actualCmd))
906+
// Only strip the extension if the command doesn't contain a path.
907+
// If it's an absolute or relative path, keep it intact so cmd.exe
908+
// executes the exact file. Stripping would fail for files in directories
909+
// not in PATH (e.g., temp directories, project-local bins).
910+
const hasPath =
911+
actualCmd.includes('/') ||
912+
actualCmd.includes('\\') ||
913+
/^[a-zA-Z]:/.test(actualCmd)
914+
if (!hasPath) {
915+
// Extract just the command name without extension for PATH lookup.
916+
actualCmd = getPath().basename(actualCmd, getPath().extname(actualCmd))
917+
}
900918
}
901919
const { stripAnsi: shouldStripAnsi = true, ...rawSpawnOptions } = {
902920
__proto__: null,

0 commit comments

Comments
 (0)