diff --git a/scripts/postinstall-bundled-plugins.mjs b/scripts/postinstall-bundled-plugins.mjs index 068e21379c4..dd9cbd67afa 100644 --- a/scripts/postinstall-bundled-plugins.mjs +++ b/scripts/postinstall-bundled-plugins.mjs @@ -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, diff --git a/scripts/stage-bundled-plugin-runtime-deps.mjs b/scripts/stage-bundled-plugin-runtime-deps.mjs index e672861fe71..e272cac5906 100644 --- a/scripts/stage-bundled-plugin-runtime-deps.mjs +++ b/scripts/stage-bundled-plugin-runtime-deps.mjs @@ -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"], }), }); } diff --git a/src/plugins/bundled-runtime-deps.test.ts b/src/plugins/bundled-runtime-deps.test.ts index 65ddbf75e31..4a984ee1db1 100644 --- a/src/plugins/bundled-runtime-deps.test.ts +++ b/src/plugins/bundled-runtime-deps.test.ts @@ -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), }), diff --git a/src/plugins/bundled-runtime-deps.ts b/src/plugins/bundled-runtime-deps.ts index d58fcad7800..8c1d1d0a7b2 100644 --- a/src/plugins/bundled-runtime-deps.ts +++ b/src/plugins/bundled-runtime-deps.ts @@ -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, }); diff --git a/test/scripts/postinstall-bundled-plugins.test.ts b/test/scripts/postinstall-bundled-plugins.test.ts index b62d756b71a..cd252304378 100644 --- a/test/scripts/postinstall-bundled-plugins.test.ts +++ b/test/scripts/postinstall-bundled-plugins.test.ts @@ -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));