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..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,6 +590,13 @@ class Config { if (this.definitions[key]?.exclusive) { for (const exclusive of this.definitions[key].exclusive) { if (!this.isDefault(exclusive)) { + // 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 3c52042c286f6..fea502d38f767 100644 --- a/workspaces/config/test/index.js +++ b/workspaces/config/test/index.js @@ -1437,6 +1437,80 @@ t.test('exclusive options conflict', async t => { }) }) +t.test('exclusive options both from env still conflict', async t => { + const path = t.testdir() + const config = new Config({ + env: { + npm_config_aaa: 'true', + npm_config_zzz: 'true', + }, + npmPath: __dirname, + argv: [ + process.execPath, + __filename, + ], + 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, + 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, + }) + // 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) => { const path = t.testdir() const opts = {