fix: tolerate pnpm-backed runtime dependency installs

This commit is contained in:
Peter Steinberger
2026-04-21 06:57:45 +01:00
parent 05ba1335d9
commit 494cd78889
5 changed files with 94 additions and 53 deletions

View File

@@ -465,6 +465,19 @@ export function createNestedNpmInstallEnv(env = process.env) {
return nextEnv;
}
export function createBundledRuntimeDependencyInstallEnv(env = process.env) {
return {
...createNestedNpmInstallEnv(env),
npm_config_legacy_peer_deps: "true",
npm_config_package_lock: "false",
npm_config_save: "false",
};
}
export function createBundledRuntimeDependencyInstallArgs(missingSpecs) {
return ["install", "--ignore-scripts", ...missingSpecs];
}
function shouldEagerInstallBundledPluginDeps(env = process.env) {
return env?.[EAGER_BUNDLED_PLUGIN_DEPS_ENV]?.trim() === "1";
}
@@ -756,28 +769,21 @@ export function runBundledPluginPostinstall(params = {}) {
}
try {
const nestedEnv = createNestedNpmInstallEnv(env);
const installEnv = createBundledRuntimeDependencyInstallEnv(env);
const npmRunner =
params.npmRunner ??
resolveNpmRunner({
env: nestedEnv,
env: installEnv,
execPath: params.execPath,
existsSync: pathExists,
platform: params.platform,
comSpec: params.comSpec,
npmArgs: [
"install",
"--omit=dev",
"--no-save",
"--package-lock=false",
"--legacy-peer-deps",
...missingSpecs,
],
npmArgs: createBundledRuntimeDependencyInstallArgs(missingSpecs),
});
const result = spawn(npmRunner.command, npmRunner.args, {
cwd: packageRoot,
encoding: "utf8",
env: npmRunner.env ?? nestedEnv,
env: npmRunner.env ?? installEnv,
stdio: "pipe",
shell: npmRunner.shell,
windowsVerbatimArguments: npmRunner.windowsVerbatimArguments,

View File

@@ -829,8 +829,11 @@ function runNpmInstall(params) {
CI: "1",
npm_config_audit: "false",
npm_config_fund: "false",
npm_config_legacy_peer_deps: "true",
npm_config_loglevel: "error",
npm_config_package_lock: "false",
npm_config_progress: "false",
npm_config_save: "false",
npm_config_yes: "true",
};
const result = spawnSync(params.npmRunner.command, params.npmRunner.args, {
@@ -1045,16 +1048,7 @@ function installPluginRuntimeDeps(params) {
runNpmInstall({
cwd: tempInstallDir,
npmRunner: resolveNpmRunner({
npmArgs: [
"install",
"--omit=dev",
"--no-audit",
"--no-fund",
"--ignore-scripts",
"--legacy-peer-deps",
"--package-lock=false",
"--silent",
],
npmArgs: ["install", "--no-audit", "--no-fund", "--ignore-scripts", "--silent"],
}),
});
}

View File

@@ -4,6 +4,8 @@ import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
createBundledRuntimeDepsInstallArgs,
createBundledRuntimeDepsInstallEnv,
ensureBundledPluginRuntimeDeps,
installBundledRuntimeDeps,
resolveBundledRuntimeDepsNpmRunner,
@@ -47,6 +49,26 @@ describe("resolveBundledRuntimeDepsNpmRunner", () => {
});
});
it("uses package-manager-neutral install args with npm config env", () => {
expect(createBundledRuntimeDepsInstallArgs(["acpx@0.5.3"])).toEqual([
"install",
"--ignore-scripts",
"acpx@0.5.3",
]);
expect(
createBundledRuntimeDepsInstallEnv({
PATH: "/usr/bin:/bin",
npm_config_global: "true",
npm_config_prefix: "/opt/homebrew",
}),
).toEqual({
PATH: "/usr/bin:/bin",
npm_config_legacy_peer_deps: "true",
npm_config_package_lock: "false",
npm_config_save: "false",
});
});
it("uses the Node-adjacent npm CLI on Windows", () => {
const execPath = "C:\\Program Files\\nodejs\\node.exe";
const npmCliPath = path.win32.resolve(
@@ -126,20 +148,21 @@ describe("installBundledRuntimeDeps", () => {
expect(spawnSyncMock).toHaveBeenCalledWith(
"npm.cmd",
[
"install",
"--prefix",
"C:\\openclaw",
"--omit=dev",
"--no-save",
"--package-lock=false",
"--ignore-scripts",
"--legacy-peer-deps",
"acpx@0.5.3",
],
["install", "--ignore-scripts", "acpx@0.5.3"],
expect.objectContaining({
cwd: "C:\\openclaw",
shell: true,
env: expect.objectContaining({
npm_config_legacy_peer_deps: "true",
npm_config_package_lock: "false",
npm_config_save: "false",
}),
}),
);
expect(spawnSyncMock).toHaveBeenCalledWith(
expect.any(String),
expect.any(Array),
expect.objectContaining({
env: expect.not.objectContaining({
npm_config_prefix: expect.any(String),
}),

View File

@@ -187,6 +187,19 @@ function createNestedNpmInstallEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
return nextEnv;
}
export function createBundledRuntimeDepsInstallEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
return {
...createNestedNpmInstallEnv(env),
npm_config_legacy_peer_deps: "true",
npm_config_package_lock: "false",
npm_config_save: "false",
};
}
export function createBundledRuntimeDepsInstallArgs(missingSpecs: readonly string[]): string[] {
return ["install", "--ignore-scripts", ...missingSpecs];
}
function resolvePathEnvKey(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): string {
if (platform !== "win32") {
return "PATH";
@@ -457,24 +470,15 @@ export function installBundledRuntimeDeps(params: {
missingSpecs: string[];
env: NodeJS.ProcessEnv;
}): void {
const installEnv = createBundledRuntimeDepsInstallEnv(params.env);
const npmRunner = resolveBundledRuntimeDepsNpmRunner({
env: params.env,
npmArgs: [
"install",
"--prefix",
params.installRoot,
"--omit=dev",
"--no-save",
"--package-lock=false",
"--ignore-scripts",
"--legacy-peer-deps",
...params.missingSpecs,
],
env: installEnv,
npmArgs: createBundledRuntimeDepsInstallArgs(params.missingSpecs),
});
const result = spawnSync(npmRunner.command, npmRunner.args, {
cwd: params.installRoot,
encoding: "utf8",
env: createNestedNpmInstallEnv(npmRunner.env ?? params.env),
env: npmRunner.env ?? installEnv,
stdio: "pipe",
shell: npmRunner.shell ?? false,
});

View File

@@ -2,6 +2,8 @@ import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import {
createBundledRuntimeDependencyInstallArgs,
createBundledRuntimeDependencyInstallEnv,
createNestedNpmInstallEnv,
pruneInstalledPackageDist,
discoverBundledPluginRuntimeDeps,
@@ -47,14 +49,7 @@ async function writePluginPackage(
describe("bundled plugin postinstall", () => {
function createNpmInstallArgs(...packages: string[]) {
return [
"install",
"--omit=dev",
"--no-save",
"--package-lock=false",
"--legacy-peer-deps",
...packages,
];
return createBundledRuntimeDependencyInstallArgs(packages);
}
function createBareNpmRunner(packages: string[]) {
@@ -122,6 +117,25 @@ describe("bundled plugin postinstall", () => {
});
});
it("uses package-manager-neutral runtime install args with npm config env", () => {
expect(createBundledRuntimeDependencyInstallArgs(["acpx@0.4.1"])).toEqual([
"install",
"--ignore-scripts",
"acpx@0.4.1",
]);
expect(
createBundledRuntimeDependencyInstallEnv({
HOME: "/tmp/home",
npm_config_prefix: "/opt/homebrew",
}),
).toEqual({
HOME: "/tmp/home",
npm_config_legacy_peer_deps: "true",
npm_config_package_lock: "false",
npm_config_save: "false",
});
});
it("does not install bundled plugin deps outside of source checkouts by default", async () => {
const extensionsDir = await createExtensionsDir();
const packageRoot = path.dirname(path.dirname(extensionsDir));