From 9dbc055f3b4265346cb61ddefa9f9cd61e57b955 Mon Sep 17 00:00:00 2001 From: umeshmore45 Date: Sat, 28 Feb 2026 23:16:36 +0530 Subject: [PATCH 1/2] fix: handle exclusive options from env and cli without conflict Added logic to skip exclusive option checks when an environment variable is set if a sibling option was provided via the command line. This ensures that environment configurations do not conflict with CLI arguments. Additionally, a new test case was introduced to verify this behavior. --- .../config/lib/definitions/definitions.js | 1 + workspaces/config/lib/index.js | 4 +++ workspaces/config/test/index.js | 35 +++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/workspaces/config/lib/definitions/definitions.js b/workspaces/config/lib/definitions/definitions.js index 4c234699e2287..bfc8be15c1c8d 100644 --- a/workspaces/config/lib/definitions/definitions.js +++ b/workspaces/config/lib/definitions/definitions.js @@ -1353,6 +1353,7 @@ const definitions = { hint: '', type: [null, Number], exclusive: ['before'], + envExport: false, description: ` If set, npm will build the npm tree such that only versions that were available more than the given number of days ago will be installed. If diff --git a/workspaces/config/lib/index.js b/workspaces/config/lib/index.js index 8520a02b6ed77..a2cdbf4dccbc0 100644 --- a/workspaces/config/lib/index.js +++ b/workspaces/config/lib/index.js @@ -590,6 +590,10 @@ class Config { if (this.definitions[key]?.exclusive) { for (const exclusive of this.definitions[key].exclusive) { if (!this.isDefault(exclusive)) { + // when loading from env, skip if sibling was set via cli + if (where === 'env' && this.list[0][exclusive] !== undefined) { + continue + } throw new TypeError(`--${key} cannot be provided when using --${exclusive}`) } } diff --git a/workspaces/config/test/index.js b/workspaces/config/test/index.js index 3c52042c286f6..f500a47f9bdfd 100644 --- a/workspaces/config/test/index.js +++ b/workspaces/config/test/index.js @@ -1437,6 +1437,41 @@ t.test('exclusive options conflict', async t => { }) }) +t.test('exclusive options from env do not conflict with cli', async t => { + const path = t.testdir() + const config = new Config({ + env: { + npm_config_lie: 'true', + }, + npmPath: __dirname, + argv: [ + process.execPath, + __filename, + '--truth=true', + ], + cwd: join(`${path}/project`), + shorthands, + definitions: { + ...definitions, + ...createDef('truth', { + default: false, + type: Boolean, + description: 'The Truth', + exclusive: ['lie'], + }), + ...createDef('lie', { + default: false, + type: Boolean, + description: 'A Lie', + exclusive: ['truth'], + }), + }, + flatten, + }) + await config.load() + t.equal(config.get('truth'), true, 'cli value wins') +}) + t.test('env-replaced config from files is not clobbered when saving', async (t) => { const path = t.testdir() const opts = { From 51e09a71755c09dd4aca89231b9c0c442969a95c Mon Sep 17 00:00:00 2001 From: umeshmore45 Date: Wed, 4 Mar 2026 18:46:56 +0530 Subject: [PATCH 2/2] fix: enhance exclusive option handling for env and CLI Refined logic to ensure that exclusive options from the environment are correctly skipped when a sibling option is set via the command line. This change prevents conflicts between environment variables and CLI arguments. Additionally, updated test cases to validate the new behavior and ensure proper functionality. --- workspaces/config/lib/index.js | 11 +++++--- workspaces/config/test/index.js | 49 +++++++++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/workspaces/config/lib/index.js b/workspaces/config/lib/index.js index a2cdbf4dccbc0..a1acb7969b29f 100644 --- a/workspaces/config/lib/index.js +++ b/workspaces/config/lib/index.js @@ -582,7 +582,7 @@ class Config { } } else { conf.raw = obj - for (const [key, value] of Object.entries(obj)) { + outer: for (const [key, value] of Object.entries(obj)) { const k = envReplace(key, this.env) const v = this.parseField(value, k) if (where !== 'default') { @@ -590,9 +590,12 @@ class Config { if (this.definitions[key]?.exclusive) { for (const exclusive of this.definitions[key].exclusive) { if (!this.isDefault(exclusive)) { - // when loading from env, skip if sibling was set via cli - if (where === 'env' && this.list[0][exclusive] !== undefined) { - continue + // when loading from env, skip only if sibling was explicitly set via CLI + if (where === 'env') { + const cliData = this.data.get('cli').data + if (Object.hasOwn(cliData, exclusive)) { + continue outer + } } throw new TypeError(`--${key} cannot be provided when using --${exclusive}`) } diff --git a/workspaces/config/test/index.js b/workspaces/config/test/index.js index f500a47f9bdfd..fea502d38f767 100644 --- a/workspaces/config/test/index.js +++ b/workspaces/config/test/index.js @@ -1437,17 +1437,54 @@ t.test('exclusive options conflict', async t => { }) }) -t.test('exclusive options from env do not conflict with cli', async t => { +t.test('exclusive options both from env still conflict', async t => { const path = t.testdir() const config = new Config({ env: { - npm_config_lie: 'true', + npm_config_aaa: 'true', + npm_config_zzz: 'true', }, npmPath: __dirname, argv: [ process.execPath, __filename, - '--truth=true', + ], + cwd: join(`${path}/project`), + shorthands, + definitions: { + ...definitions, + ...createDef('aaa', { + default: false, + type: Boolean, + description: 'aaa', + exclusive: ['zzz'], + }), + ...createDef('zzz', { + default: false, + type: Boolean, + description: 'zzz', + exclusive: ['aaa'], + }), + }, + flatten, + }) + await t.rejects(config.load(), { + name: 'TypeError', + message: '--zzz cannot be provided when using --aaa', + }) +}) + +t.test('exclusive env option is skipped when sibling is set via CLI', async t => { + const path = t.testdir() + const config = new Config({ + env: { + npm_config_truth: 'true', + }, + npmPath: __dirname, + argv: [ + process.execPath, + __filename, + '--lie=true', ], cwd: join(`${path}/project`), shorthands, @@ -1468,8 +1505,10 @@ t.test('exclusive options from env do not conflict with cli', async t => { }, flatten, }) - await config.load() - t.equal(config.get('truth'), true, 'cli value wins') + // should not throw — env `truth` is skipped because `lie` was set via CLI + await t.resolves(config.load()) + t.equal(config.get('lie'), true, 'CLI lie is set') + t.equal(config.get('truth'), false, 'env truth is skipped, remains default') }) t.test('env-replaced config from files is not clobbered when saving', async (t) => {