Skip to content

Commit fdc7e65

Browse files
anthonyshewclaude
andauthored
fix(workspaces): Use shell option on Windows in execa call (#11108)
## Summary Addresses #11035 by adding `preferLocal` and `shell` options to `execa` calls for package manager commands. This resolves the "no package.json found" error that occurs when using Bun installed via bash installer on Windows. When Bun is installed via `curl -fsSL https://bun.sh/install | bash`, it's placed in a Unix-style PATH that Node.js can't access without shell context. Using `shell: true` on Windows allows the shell to resolve these commands properly. ## Test Results CI and canarying to be verified on Windows. Co-authored-by: Claude <[email protected]>
1 parent e0d7ae1 commit fdc7e65

File tree

4 files changed

+110
-2
lines changed

4 files changed

+110
-2
lines changed

packages/turbo-workspaces/__tests__/index.test.ts

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import path from "node:path";
22
import execa from "execa";
33
import * as turboUtils from "@turbo/utils";
44
import { setupTestFixtures } from "@turbo/test-utils";
5-
import { describe, it, expect, jest } from "@jest/globals";
6-
import { getWorkspaceDetails, convert } from "../src";
5+
import { describe, it, expect, jest, beforeEach } from "@jest/globals";
6+
import { getWorkspaceDetails, convert, install } from "../src";
77
import { generateConvertMatrix } from "./test-utils";
88

99
jest.mock("execa", () => jest.fn());
@@ -13,6 +13,108 @@ describe("Node entrypoint", () => {
1313
directory: path.join(__dirname, "../"),
1414
});
1515

16+
beforeEach(() => {
17+
jest.clearAllMocks();
18+
(execa as jest.MockedFunction<typeof execa>).mockResolvedValue({
19+
stdout: "",
20+
stderr: "",
21+
exitCode: 0,
22+
command: "",
23+
failed: false,
24+
timedOut: false,
25+
isCanceled: false,
26+
killed: false,
27+
} as any);
28+
});
29+
30+
describe("install", () => {
31+
it("should use shell option on Windows for all package managers", async () => {
32+
const originalPlatform = process.platform;
33+
Object.defineProperty(process, "platform", {
34+
value: "win32",
35+
});
36+
37+
const { root } = useFixture({
38+
fixture: `./bun/monorepo`,
39+
});
40+
41+
const mockProject = {
42+
name: "test-project",
43+
description: undefined,
44+
packageManager: "bun" as const,
45+
paths: {
46+
root,
47+
packageJson: path.join(root, "package.json"),
48+
lockfile: path.join(root, "bun.lockb"),
49+
nodeModules: path.join(root, "node_modules"),
50+
},
51+
workspaceData: {
52+
globs: ["apps/*", "packages/*"],
53+
workspaces: [],
54+
},
55+
};
56+
57+
await install({
58+
project: mockProject,
59+
to: { name: "bun", version: "1.0.1" },
60+
options: { dry: false },
61+
});
62+
63+
expect(execa).toHaveBeenCalledWith("bun", ["install"], {
64+
cwd: root,
65+
preferLocal: true,
66+
shell: true,
67+
});
68+
69+
Object.defineProperty(process, "platform", {
70+
value: originalPlatform,
71+
});
72+
});
73+
74+
it("should not use shell option on non-Windows platforms", async () => {
75+
const originalPlatform = process.platform;
76+
Object.defineProperty(process, "platform", {
77+
value: "darwin",
78+
});
79+
80+
const { root } = useFixture({
81+
fixture: `./bun/monorepo`,
82+
});
83+
84+
const mockProject = {
85+
name: "test-project",
86+
description: undefined,
87+
packageManager: "bun" as const,
88+
paths: {
89+
root,
90+
packageJson: path.join(root, "package.json"),
91+
lockfile: path.join(root, "bun.lockb"),
92+
nodeModules: path.join(root, "node_modules"),
93+
},
94+
workspaceData: {
95+
globs: ["apps/*", "packages/*"],
96+
workspaces: [],
97+
},
98+
};
99+
100+
await install({
101+
project: mockProject,
102+
to: { name: "bun", version: "1.0.1" },
103+
options: { dry: false },
104+
});
105+
106+
expect(execa).toHaveBeenCalledWith("bun", ["install"], {
107+
cwd: root,
108+
preferLocal: true,
109+
shell: false,
110+
});
111+
112+
Object.defineProperty(process, "platform", {
113+
value: originalPlatform,
114+
});
115+
});
116+
});
117+
16118
describe("convert", () => {
17119
it.each(generateConvertMatrix())(
18120
"detects $fixtureType project using $fixtureManager and converts to $toManager (interactive=$interactive dry=$dry install=$install)",

packages/turbo-workspaces/src/install.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ export async function install(args: InstallArgs) {
127127
try {
128128
await execa(packageManager.command, packageManager.installArgs, {
129129
cwd: args.project.paths.root,
130+
preferLocal: true,
131+
shell: process.platform === "win32",
130132
});
131133
if (spinner) {
132134
spinner.stop();

packages/turbo-workspaces/src/managers/pnpm.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ async function convertLock(args: ConvertArgs): Promise<void> {
240240
await execa(PACKAGE_MANAGER_DETAILS.name, ["import"], {
241241
stdio: "ignore",
242242
cwd: project.paths.root,
243+
preferLocal: true,
244+
shell: process.platform === "win32",
243245
});
244246
} catch (err) {
245247
// do nothing

packages/turbo-workspaces/src/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,8 @@ async function bunLockToYarnLock({
276276
const { stdout } = await execa("bun", ["bun.lockb"], {
277277
stdin: "ignore",
278278
cwd: project.paths.root,
279+
preferLocal: true,
280+
shell: process.platform === "win32",
279281
});
280282
// write the yarn lockfile
281283
await writeFile(path.join(project.paths.root, "yarn.lock"), stdout);

0 commit comments

Comments
 (0)