diff --git a/scripts/preinstall-package-manager-warning.mjs b/scripts/preinstall-package-manager-warning.mjs index c38fd1fb9cd6..1026a73c6981 100644 --- a/scripts/preinstall-package-manager-warning.mjs +++ b/scripts/preinstall-package-manager-warning.mjs @@ -2,6 +2,10 @@ import { pathToFileURL } from "node:url"; const allowedLifecyclePackageManagers = new Set(["pnpm", "npm", "yarn", "bun"]); +const lifecyclePackageManagerLauncherAliases = new Map([ + ["yarnpkg", "yarn"], + ["yarn-berry", "yarn"], +]); function normalizeEnvValue(value) { return typeof value === "string" ? value.trim() : ""; @@ -15,6 +19,31 @@ function normalizeLifecyclePackageManagerName(value) { return allowedLifecyclePackageManagers.has(normalized) ? normalized : null; } +function detectLifecyclePackageManagerFromExecPath(value) { + const execPath = normalizeEnvValue(value).toLowerCase(); + const executableName = execPath.split(/[\\/]/u).findLast((segment) => segment.length > 0) ?? ""; + const launcherName = executableName.replace(/\.(?:c?js|mjs|cmd|ps1|exe)$/u, ""); + const candidates = [launcherName, launcherName.replace(/-cli$/u, "")]; + + for (const candidate of candidates) { + if (/^yarn(?:pkg)?-\d/u.test(candidate)) { + return "yarn"; + } + + const aliasedPackageManager = lifecyclePackageManagerLauncherAliases.get(candidate); + if (aliasedPackageManager) { + return aliasedPackageManager; + } + + const packageManager = normalizeLifecyclePackageManagerName(candidate); + if (packageManager) { + return packageManager; + } + } + + return null; +} + /** * Detects the package manager running the current lifecycle script. */ @@ -25,21 +54,7 @@ export function detectLifecyclePackageManager(env = process.env) { return normalizeLifecyclePackageManagerName(userAgentMatch[1]); } - const execPath = normalizeEnvValue(env.npm_execpath).toLowerCase(); - if (execPath.includes("pnpm")) { - return "pnpm"; - } - if (execPath.includes("npm")) { - return "npm"; - } - if (execPath.includes("yarn")) { - return "yarn"; - } - if (execPath.includes("bun")) { - return "bun"; - } - - return null; + return detectLifecyclePackageManagerFromExecPath(env.npm_execpath); } /** diff --git a/test/scripts/preinstall-package-manager-warning.test.ts b/test/scripts/preinstall-package-manager-warning.test.ts index 8de15248f133..5f7e2bb2c2f1 100644 --- a/test/scripts/preinstall-package-manager-warning.test.ts +++ b/test/scripts/preinstall-package-manager-warning.test.ts @@ -35,6 +35,46 @@ describe("detectLifecyclePackageManager", () => { ).toBe("pnpm"); }); + it("detects npm cli launchers from npm_execpath", () => { + expect( + detectLifecyclePackageManager({ + npm_execpath: "C:\\Tools\\nodejs\\node_modules\\npm\\bin\\npm-cli.js", + }), + ).toBe("npm"); + }); + + it("detects yarnpkg launchers from npm_execpath", () => { + expect( + detectLifecyclePackageManager({ + npm_execpath: "C:\\Tools\\corepack\\yarnpkg.cmd", + }), + ).toBe("yarn"); + }); + + it("detects versioned Yarn release launchers from npm_execpath", () => { + expect( + detectLifecyclePackageManager({ + npm_execpath: "/work/project/.yarn/releases/yarn-4.5.0.cjs", + }), + ).toBe("yarn"); + }); + + it("detects Yarn Berry release launchers from npm_execpath", () => { + expect( + detectLifecyclePackageManager({ + npm_execpath: "/work/project/.yarn/releases/yarn-berry.cjs", + }), + ).toBe("yarn"); + }); + + it("ignores package manager names in npm_execpath parent directories", () => { + expect( + detectLifecyclePackageManager({ + npm_execpath: "/tmp/npm-cache/bin/yarn.js", + }), + ).toBe("yarn"); + }); + it("ignores untrusted user-agent tokens with control characters", () => { expect( detectLifecyclePackageManager({