Skip to content

Commit ee95b9c

Browse files
author
Spencer
committed
Fix Windows pnpm process spawning
1 parent 5ff3da4 commit ee95b9c

2 files changed

Lines changed: 51 additions & 21 deletions

File tree

scripts/shared/process.test.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from "vitest";
2-
import { isDirectExecution, resolveSpawnCommand } from "./process.js";
2+
import { isDirectExecution, resolveSpawnCommand, shouldUseShellForCommand } from "./process.js";
33

44
describe("isDirectExecution", () => {
55
it("matches direct execution for POSIX script paths", () => {
@@ -18,8 +18,8 @@ describe("isDirectExecution", () => {
1818
});
1919

2020
describe("resolveSpawnCommand", () => {
21-
it("uses pnpm.cmd on Windows so spawn works without a shell", () => {
22-
expect(resolveSpawnCommand("pnpm", "win32")).toBe("pnpm.cmd");
21+
it("keeps pnpm unresolved on Windows so cmd can resolve the shim", () => {
22+
expect(resolveSpawnCommand("pnpm", "win32")).toBe("pnpm");
2323
});
2424

2525
it("does not rewrite native Windows executables like git", () => {
@@ -30,3 +30,17 @@ describe("resolveSpawnCommand", () => {
3030
expect(resolveSpawnCommand("pnpm", "linux")).toBe("pnpm");
3131
});
3232
});
33+
34+
describe("shouldUseShellForCommand", () => {
35+
it("uses a shell for pnpm on Windows because pnpm.cmd is not directly executable", () => {
36+
expect(shouldUseShellForCommand("pnpm", "win32")).toBe(true);
37+
});
38+
39+
it("does not use a shell for native executables like git on Windows", () => {
40+
expect(shouldUseShellForCommand("git", "win32")).toBe(false);
41+
});
42+
43+
it("does not use a shell on POSIX platforms", () => {
44+
expect(shouldUseShellForCommand("pnpm", "linux")).toBe(false);
45+
});
46+
});

scripts/shared/process.ts

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Process utilities for running child processes
33
*/
44

5-
import { type ChildProcess, spawn } from "child_process";
5+
import { type ChildProcess, type SpawnOptions, spawn } from "child_process";
66
import { posix, resolve } from "path";
77

88
export interface ProcessOptions {
@@ -11,6 +11,12 @@ export interface ProcessOptions {
1111
stdio?: "inherit" | "pipe" | "ignore";
1212
}
1313

14+
interface SpawnConfig {
15+
command: string;
16+
options: ProcessOptions;
17+
platform?: NodeJS.Platform;
18+
}
19+
1420
const WINDOWS_CMD_SHIMS = new Set(["pnpm", "npm", "npx"]);
1521

1622
function isWindowsDrivePath(path: string): boolean {
@@ -75,13 +81,29 @@ export function isDirectExecution(moduleUrl: string, argv1: string | undefined =
7581

7682
export function resolveSpawnCommand(
7783
command: string,
78-
platform: NodeJS.Platform = process.platform
84+
_platform: NodeJS.Platform = process.platform
7985
): string {
80-
if (platform !== "win32") {
81-
return command;
82-
}
86+
return command;
87+
}
8388

84-
return WINDOWS_CMD_SHIMS.has(command.toLowerCase()) ? `${command}.cmd` : command;
89+
export function shouldUseShellForCommand(
90+
command: string,
91+
platform: NodeJS.Platform = process.platform
92+
): boolean {
93+
return platform === "win32" && WINDOWS_CMD_SHIMS.has(command.toLowerCase());
94+
}
95+
96+
function createSpawnOptions({
97+
command,
98+
options,
99+
platform = process.platform,
100+
}: SpawnConfig): SpawnOptions {
101+
return {
102+
cwd: options.cwd,
103+
env: options.env ?? process.env,
104+
stdio: options.stdio ?? "inherit",
105+
shell: shouldUseShellForCommand(command, platform),
106+
};
85107
}
86108

87109
/**
@@ -93,12 +115,11 @@ export function run(
93115
options: ProcessOptions = {}
94116
): Promise<void> {
95117
return new Promise((resolve, reject) => {
96-
const child = spawn(resolveSpawnCommand(command), args, {
97-
cwd: options.cwd,
98-
env: options.env ?? process.env,
99-
stdio: options.stdio ?? "inherit",
100-
shell: false,
101-
});
118+
const child = spawn(
119+
resolveSpawnCommand(command),
120+
args,
121+
createSpawnOptions({ command, options })
122+
);
102123

103124
child.on("close", (code) => {
104125
if (code === 0) {
@@ -122,12 +143,7 @@ export function runBackground(
122143
args: string[] = [],
123144
options: ProcessOptions = {}
124145
): ChildProcess {
125-
const child = spawn(resolveSpawnCommand(command), args, {
126-
cwd: options.cwd,
127-
env: options.env ?? process.env,
128-
stdio: options.stdio ?? "inherit",
129-
shell: false,
130-
});
146+
const child = spawn(resolveSpawnCommand(command), args, createSpawnOptions({ command, options }));
131147

132148
return child;
133149
}

0 commit comments

Comments
 (0)