From ccedc506a5c82a62babee73c944bcdaa6a4337d3 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Wed, 15 Apr 2026 10:21:41 +0530 Subject: [PATCH] fix: handle native pnpm execpath --- scripts/pnpm-runner.mjs | 42 +++++++++++++++++++++++++-- test/scripts/pnpm-runner.test.ts | 50 +++++++++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/scripts/pnpm-runner.mjs b/scripts/pnpm-runner.mjs index 0d713e99c5e..307030537f8 100644 --- a/scripts/pnpm-runner.mjs +++ b/scripts/pnpm-runner.mjs @@ -1,9 +1,43 @@ import { spawn } from "node:child_process"; +import { closeSync, openSync, readSync } from "node:fs"; import path from "node:path"; import { buildCmdExeCommandLine } from "./windows-cmd-helpers.mjs"; function isPnpmExecPath(value) { - return /^pnpm(?:-cli)?(?:\.(?:c?js|cmd|exe))?$/.test(path.basename(value).toLowerCase()); + return /^pnpm(?:-cli)?(?:\.([cm]?js|cmd|exe))?$/.test(path.basename(value).toLowerCase()); +} + +function hasScriptShebang(value) { + let fd; + try { + fd = openSync(value, "r"); + const header = Buffer.alloc(2); + return ( + readSync(fd, header, 0, header.length, 0) === header.length && + header[0] === 0x23 && + header[1] === 0x21 + ); + } catch { + return false; + } finally { + if (fd !== undefined) { + closeSync(fd); + } + } +} + +function isNodeRunnablePnpmExecPath(value) { + if (!isPnpmExecPath(value)) { + return false; + } + const extension = path.extname(value).toLowerCase(); + if (extension === ".js" || extension === ".cjs" || extension === ".mjs") { + return true; + } + if (extension.length > 0) { + return false; + } + return hasScriptShebang(value); } export function resolvePnpmRunner(params = {}) { @@ -14,7 +48,11 @@ export function resolvePnpmRunner(params = {}) { const platform = params.platform ?? process.platform; const comSpec = params.comSpec ?? process.env.ComSpec ?? "cmd.exe"; - if (typeof npmExecPath === "string" && npmExecPath.length > 0 && isPnpmExecPath(npmExecPath)) { + if ( + typeof npmExecPath === "string" && + npmExecPath.length > 0 && + isNodeRunnablePnpmExecPath(npmExecPath) + ) { return { command: nodeExecPath, args: [...nodeArgs, npmExecPath, ...pnpmArgs], diff --git a/test/scripts/pnpm-runner.test.ts b/test/scripts/pnpm-runner.test.ts index 232a7a791be..c27c604c3b2 100644 --- a/test/scripts/pnpm-runner.test.ts +++ b/test/scripts/pnpm-runner.test.ts @@ -1,8 +1,11 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; import { describe, expect, it } from "vitest"; import { createPnpmRunnerSpawnSpec, resolvePnpmRunner } from "../../scripts/pnpm-runner.mjs"; describe("resolvePnpmRunner", () => { - it("uses npm_execpath when it points to pnpm", () => { + it("uses npm_execpath when it points to a JS pnpm entrypoint", () => { expect( resolvePnpmRunner({ npmExecPath: "/home/test/.cache/node/corepack/v1/pnpm/10.32.1/bin/pnpm.cjs", @@ -22,6 +25,29 @@ describe("resolvePnpmRunner", () => { }); }); + it("uses npm_execpath when it points to a shebang pnpm script", () => { + const tempDir = mkdtempSync(path.join(os.tmpdir(), "pnpm-runner-")); + const npmExecPath = path.join(tempDir, "pnpm"); + writeFileSync(npmExecPath, "#!/usr/bin/env node\nconsole.log('pnpm');\n"); + + try { + expect( + resolvePnpmRunner({ + npmExecPath, + nodeExecPath: "/usr/local/bin/node", + pnpmArgs: ["exec", "vitest", "run"], + platform: "linux", + }), + ).toEqual({ + command: "/usr/local/bin/node", + args: [npmExecPath, "exec", "vitest", "run"], + shell: false, + }); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + it("prepends node args when launching pnpm through node", () => { expect( resolvePnpmRunner({ @@ -44,6 +70,28 @@ describe("resolvePnpmRunner", () => { }); }); + it("falls back to bare pnpm when npm_execpath points to a native pnpm binary", () => { + const tempDir = mkdtempSync(path.join(os.tmpdir(), "pnpm-runner-")); + const npmExecPath = path.join(tempDir, "pnpm"); + writeFileSync(npmExecPath, Buffer.from([0x7f, 0x45, 0x4c, 0x46])); + + try { + expect( + resolvePnpmRunner({ + npmExecPath, + pnpmArgs: ["exec", "vitest", "run"], + platform: "linux", + }), + ).toEqual({ + command: "pnpm", + args: ["exec", "vitest", "run"], + shell: false, + }); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + it("falls back to bare pnpm on non-Windows when npm_execpath is missing", () => { expect( resolvePnpmRunner({