fix(update): use absolute npm script shell

This commit is contained in:
Peter Steinberger
2026-05-04 22:19:17 +01:00
parent 828b6be39d
commit 86385f72e9
2 changed files with 74 additions and 1 deletions

View File

@@ -150,6 +150,47 @@ describe("update global helpers", () => {
});
});
it("uses an absolute POSIX script shell for npm lifecycle scripts during global installs", async () => {
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("linux");
try {
await expect(
createGlobalInstallEnv({
COREPACK_ENABLE_DOWNLOAD_PROMPT: "1",
PATH: "/home/peter/.npm-global/bin",
}),
).resolves.toMatchObject({
COREPACK_ENABLE_DOWNLOAD_PROMPT: "1",
NPM_CONFIG_SCRIPT_SHELL: "/bin/sh",
});
} finally {
platformSpy.mockRestore();
}
});
it("preserves explicit npm script shell config for global installs", async () => {
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("linux");
try {
await expect(
createGlobalInstallEnv({
COREPACK_ENABLE_DOWNLOAD_PROMPT: "1",
NPM_CONFIG_SCRIPT_SHELL: "/custom/sh",
}),
).resolves.toMatchObject({
NPM_CONFIG_SCRIPT_SHELL: "/custom/sh",
});
await expect(
createGlobalInstallEnv({
COREPACK_ENABLE_DOWNLOAD_PROMPT: "1",
npm_config_script_shell: "/custom/lower-sh",
}),
).resolves.toMatchObject({
npm_config_script_shell: "/custom/lower-sh",
});
} finally {
platformSpy.mockRestore();
}
});
it("resolves portable Git paths from process-local app data only", async () => {
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
try {

View File

@@ -41,6 +41,7 @@ const NPM_GLOBAL_INSTALL_OMIT_OPTIONAL_FLAGS = [
"--omit=optional",
...NPM_GLOBAL_INSTALL_QUIET_FLAGS,
] as const;
const NPM_CONFIG_SCRIPT_SHELL_KEYS = ["NPM_CONFIG_SCRIPT_SHELL", "npm_config_script_shell"];
const FIRST_PACKAGED_DIST_INVENTORY_VERSION = { major: 2026, minor: 4, patch: 15 };
const OMITTED_PRIVATE_QA_BUNDLED_PLUGIN_ROOTS = new Set([
"dist/extensions/qa-channel",
@@ -315,6 +316,31 @@ function applyCorepackDownloadPromptEnv(env: Record<string, string>) {
}
}
function hasNpmScriptShellSetting(env: NodeJS.ProcessEnv): boolean {
return NPM_CONFIG_SCRIPT_SHELL_KEYS.some((key) => Boolean(env[key]?.trim()));
}
function resolvePosixNpmScriptShell(env: NodeJS.ProcessEnv): string | null {
if (process.platform === "win32") {
return null;
}
if (fsSync.existsSync("/bin/sh")) {
return "/bin/sh";
}
const shell = env.SHELL?.trim();
return shell && path.isAbsolute(shell) && fsSync.existsSync(shell) ? shell : null;
}
function applyPosixNpmScriptShellEnv(env: Record<string, string>) {
if (hasNpmScriptShellSetting(env)) {
return;
}
const scriptShell = resolvePosixNpmScriptShell(env);
if (scriptShell) {
env.NPM_CONFIG_SCRIPT_SHELL = scriptShell;
}
}
export function resolveGlobalInstallSpec(params: {
packageName: string;
tag: string;
@@ -344,8 +370,13 @@ export async function createGlobalInstallEnv(
const hasCorepackDownloadPromptSetting = Boolean(
sourceEnv.COREPACK_ENABLE_DOWNLOAD_PROMPT?.trim(),
);
const missingPosixScriptShell =
Boolean(resolvePosixNpmScriptShell(sourceEnv)) && !hasNpmScriptShellSetting(sourceEnv);
const requiresMergedEnv =
pathPrepend.length > 0 || process.platform === "win32" || !hasCorepackDownloadPromptSetting;
pathPrepend.length > 0 ||
process.platform === "win32" ||
!hasCorepackDownloadPromptSetting ||
missingPosixScriptShell;
if (!requiresMergedEnv) {
return env;
}
@@ -357,6 +388,7 @@ export async function createGlobalInstallEnv(
applyPathPrepend(merged, pathPrepend);
applyWindowsPackageInstallEnv(merged);
applyCorepackDownloadPromptEnv(merged);
applyPosixNpmScriptShellEnv(merged);
return merged;
}