From 7afc51d85bb478e9c7b4a8976e645994c229f88d Mon Sep 17 00:00:00 2001 From: Anthony Shew Date: Wed, 12 Nov 2025 10:49:50 -0700 Subject: [PATCH] fix(workspaces): use shell option on Windows for package manager commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add preferLocal and shell options to execa calls to properly resolve package manager executables on Windows, especially when Bun is installed via the bash installer. This fixes issue #11035 where "no package.json found" error occurs during create-turbo on Windows with Bun. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../turbo-workspaces/__tests__/index.test.ts | 106 +++++++++++++++++- packages/turbo-workspaces/src/install.ts | 2 + .../turbo-workspaces/src/managers/pnpm.ts | 2 + packages/turbo-workspaces/src/utils.ts | 2 + 4 files changed, 110 insertions(+), 2 deletions(-) diff --git a/packages/turbo-workspaces/__tests__/index.test.ts b/packages/turbo-workspaces/__tests__/index.test.ts index 7efa709643958..d5107d09f513a 100644 --- a/packages/turbo-workspaces/__tests__/index.test.ts +++ b/packages/turbo-workspaces/__tests__/index.test.ts @@ -2,8 +2,8 @@ import path from "node:path"; import execa from "execa"; import * as turboUtils from "@turbo/utils"; import { setupTestFixtures } from "@turbo/test-utils"; -import { describe, it, expect, jest } from "@jest/globals"; -import { getWorkspaceDetails, convert } from "../src"; +import { describe, it, expect, jest, beforeEach } from "@jest/globals"; +import { getWorkspaceDetails, convert, install } from "../src"; import { generateConvertMatrix } from "./test-utils"; jest.mock("execa", () => jest.fn()); @@ -13,6 +13,108 @@ describe("Node entrypoint", () => { directory: path.join(__dirname, "../"), }); + beforeEach(() => { + jest.clearAllMocks(); + (execa as jest.MockedFunction).mockResolvedValue({ + stdout: "", + stderr: "", + exitCode: 0, + command: "", + failed: false, + timedOut: false, + isCanceled: false, + killed: false, + } as any); + }); + + describe("install", () => { + it("should use shell option on Windows for all package managers", async () => { + const originalPlatform = process.platform; + Object.defineProperty(process, "platform", { + value: "win32", + }); + + const { root } = useFixture({ + fixture: `./bun/monorepo`, + }); + + const mockProject = { + name: "test-project", + description: undefined, + packageManager: "bun" as const, + paths: { + root, + packageJson: path.join(root, "package.json"), + lockfile: path.join(root, "bun.lockb"), + nodeModules: path.join(root, "node_modules"), + }, + workspaceData: { + globs: ["apps/*", "packages/*"], + workspaces: [], + }, + }; + + await install({ + project: mockProject, + to: { name: "bun", version: "1.0.1" }, + options: { dry: false }, + }); + + expect(execa).toHaveBeenCalledWith("bun", ["install"], { + cwd: root, + preferLocal: true, + shell: true, + }); + + Object.defineProperty(process, "platform", { + value: originalPlatform, + }); + }); + + it("should not use shell option on non-Windows platforms", async () => { + const originalPlatform = process.platform; + Object.defineProperty(process, "platform", { + value: "darwin", + }); + + const { root } = useFixture({ + fixture: `./bun/monorepo`, + }); + + const mockProject = { + name: "test-project", + description: undefined, + packageManager: "bun" as const, + paths: { + root, + packageJson: path.join(root, "package.json"), + lockfile: path.join(root, "bun.lockb"), + nodeModules: path.join(root, "node_modules"), + }, + workspaceData: { + globs: ["apps/*", "packages/*"], + workspaces: [], + }, + }; + + await install({ + project: mockProject, + to: { name: "bun", version: "1.0.1" }, + options: { dry: false }, + }); + + expect(execa).toHaveBeenCalledWith("bun", ["install"], { + cwd: root, + preferLocal: true, + shell: false, + }); + + Object.defineProperty(process, "platform", { + value: originalPlatform, + }); + }); + }); + describe("convert", () => { it.each(generateConvertMatrix())( "detects $fixtureType project using $fixtureManager and converts to $toManager (interactive=$interactive dry=$dry install=$install)", diff --git a/packages/turbo-workspaces/src/install.ts b/packages/turbo-workspaces/src/install.ts index 588b1d0a886ed..879eb1e5ba0c8 100644 --- a/packages/turbo-workspaces/src/install.ts +++ b/packages/turbo-workspaces/src/install.ts @@ -127,6 +127,8 @@ export async function install(args: InstallArgs) { try { await execa(packageManager.command, packageManager.installArgs, { cwd: args.project.paths.root, + preferLocal: true, + shell: process.platform === "win32", }); if (spinner) { spinner.stop(); diff --git a/packages/turbo-workspaces/src/managers/pnpm.ts b/packages/turbo-workspaces/src/managers/pnpm.ts index fd4f31a976663..db8a1463b18ba 100644 --- a/packages/turbo-workspaces/src/managers/pnpm.ts +++ b/packages/turbo-workspaces/src/managers/pnpm.ts @@ -240,6 +240,8 @@ async function convertLock(args: ConvertArgs): Promise { await execa(PACKAGE_MANAGER_DETAILS.name, ["import"], { stdio: "ignore", cwd: project.paths.root, + preferLocal: true, + shell: process.platform === "win32", }); } catch (err) { // do nothing diff --git a/packages/turbo-workspaces/src/utils.ts b/packages/turbo-workspaces/src/utils.ts index c1b9123c9d061..cf0fac6f79416 100644 --- a/packages/turbo-workspaces/src/utils.ts +++ b/packages/turbo-workspaces/src/utils.ts @@ -276,6 +276,8 @@ async function bunLockToYarnLock({ const { stdout } = await execa("bun", ["bun.lockb"], { stdin: "ignore", cwd: project.paths.root, + preferLocal: true, + shell: process.platform === "win32", }); // write the yarn lockfile await writeFile(path.join(project.paths.root, "yarn.lock"), stdout);