feat: warn instead of error on unknown .npmrc configs (#9729) · npm/cli@fd75880 · GitHub
Skip to content

Commit fd75880

Browse files
authored
feat: warn instead of error on unknown .npmrc configs (#9729)
Unknown configuration keys in .npmrc files now emit a warning by default, restoring pre-npm-12 behavior, instead of throwing. This reverts the breaking change from 979518d (#9276) for file-based configs. The new `strict-npmrc` config (default false) opts back into treating them as a hard error. Unknown CLI flags and abbreviations continue to error regardless of this setting.
1 parent 42b12c2 commit fd75880

6 files changed

Lines changed: 137 additions & 29 deletions

File tree

‎lib/base-cmd.js‎

Lines changed: 20 additions & 6 deletions

‎tap-snapshots/test/lib/commands/config.js.test.cjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna
181181
"sign-git-tag": false,
182182
"strict-peer-deps": false,
183183
"strict-allow-scripts": false,
184+
"strict-npmrc": false,
184185
"strict-ssl": true,
185186
"tag-version-prefix": "v",
186187
"timing": false,
@@ -376,6 +377,7 @@ shell = "{SHELL}"
376377
sign-git-commit = false
377378
sign-git-tag = false
378379
strict-allow-scripts = false
380+
strict-npmrc = false
379381
strict-peer-deps = false
380382
strict-ssl = true
381383
; tag = "latest" ; overridden by project

‎tap-snapshots/test/lib/docs.js.test.cjs‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2084,6 +2084,19 @@ their install scripts never run.
20842084
20852085
20862086
2087+
#### \`strict-npmrc\`
2088+
2089+
* Default: false
2090+
* Type: Boolean
2091+
2092+
If set to \`true\`, unknown configuration keys found in \`.npmrc\` files are
2093+
treated as a hard error instead of a warning.
2094+
2095+
Unknown command line flags and abbreviated flags always error regardless of
2096+
this setting.
2097+
2098+
2099+
20872100
#### \`strict-peer-deps\`
20882101
20892102
* Default: false
@@ -2753,6 +2766,7 @@ Array [
27532766
"sign-git-tag",
27542767
"strict-peer-deps",
27552768
"strict-allow-scripts",
2769+
"strict-npmrc",
27562770
"strict-ssl",
27572771
"tag",
27582772
"tag-version-prefix",
@@ -2922,6 +2936,7 @@ Array [
29222936
"sign-git-tag",
29232937
"strict-peer-deps",
29242938
"strict-allow-scripts",
2939+
"strict-npmrc",
29252940
"strict-ssl",
29262941
"tag",
29272942
"tag-version-prefix",
@@ -3115,6 +3130,7 @@ Object {
31153130
"signGitTag": false,
31163131
"silent": false,
31173132
"strictAllowScripts": false,
3133+
"strictNpmrc": false,
31183134
"strictPeerDeps": false,
31193135
"strictSSL": true,
31203136
"tagVersionPrefix": "v",

‎test/lib/base-cmd.js‎

Lines changed: 84 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ t.test('flags() throws error for unknown flags', async t => {
146146
command.exec(),
147147
{
148148
code: 'EUNKNOWNCONFIG',
149-
message: /Unknown cli config:[\s\S]*--unknown-flag/,
149+
message: /Unknown cli flag:[\s\S]*--unknown-flag/,
150150
},
151151
'throws EUNKNOWNCONFIG for unknown flag'
152152
)
@@ -448,9 +448,9 @@ t.test('flags() throws error for multiple unknown flags with pluralization', asy
448448
command.exec(),
449449
{
450450
code: 'EUNKNOWNCONFIG',
451-
message: /Unknown cli configs:[\s\S]*--unknown-one[\s\S]*--unknown-two/,
451+
message: /Unknown cli flags:[\s\S]*--unknown-one[\s\S]*--unknown-two/,
452452
},
453-
'throws EUNKNOWNCONFIG with pluralized "configs" for multiple unknown flags'
453+
'throws EUNKNOWNCONFIG with pluralized "flags" for multiple unknown flags'
454454
)
455455
})
456456

@@ -685,14 +685,59 @@ t.test('flags() does not throw when positionals is null (unlimited)', async t =>
685685
t.equal(flags.id, null, 'id flag uses default')
686686
})
687687

688-
t.test('validateCli throws aggregated error for unknown file configs', async t => {
688+
t.test('validateCli warns (does not throw) for unknown file configs', async t => {
689+
const { npm, logs } = await loadMockNpm(t, {
690+
homeDir: {
691+
'.npmrc': [
692+
'bogus-user-key=yes',
693+
'@scope:bogus-scoped=no',
694+
].join('\n'),
695+
},
696+
})
697+
698+
class TestCommand extends BaseCommand {
699+
static name = 'test-command'
700+
static description = 'Test command'
701+
}
702+
703+
const command = new TestCommand(npm)
704+
t.doesNotThrow(() => command.validateCli(), 'unknown file configs do not throw')
705+
706+
const warningLogs = logs.warn
707+
t.ok(warningLogs.some(msg => msg.includes('bogus-user-key') && msg.includes('npm help npmrc')),
708+
'warns about plain unknown key')
709+
t.ok(warningLogs.some(msg => msg.includes('bogus-scoped')),
710+
'warns about scoped unknown key')
711+
})
712+
713+
t.test('validateCli unknown file config warning respects loglevel suppression', async t => {
714+
const { npm, logs } = await loadMockNpm(t, {
715+
homeDir: {
716+
'.npmrc': 'bogus-user-key=yes',
717+
},
718+
config: { loglevel: 'error' },
719+
})
720+
721+
class TestCommand extends BaseCommand {
722+
static name = 'test-command'
723+
static description = 'Test command'
724+
}
725+
726+
const command = new TestCommand(npm)
727+
t.doesNotThrow(() => command.validateCli(), 'still does not throw')
728+
t.notOk(logs.warn.some(msg => msg.includes('bogus-user-key')),
729+
'unknown file config warning is suppressed at loglevel=error')
730+
})
731+
732+
t.test('validateCli errors on unknown file configs when strict-npmrc is set', async t => {
689733
const { npm } = await loadMockNpm(t, {
690734
homeDir: {
691735
'.npmrc': [
692736
'bogus-user-key=yes',
693737
'@scope:bogus-scoped=no',
694738
].join('\n'),
695739
},
740+
config: { 'strict-npmrc': true },
696741
})
697742

698743
class TestCommand extends BaseCommand {
@@ -707,31 +752,46 @@ t.test('validateCli throws aggregated error for unknown file configs', async t =
707752
code: 'EUNKNOWNCONFIG',
708753
message: /Unknown npm configuration keys:[\s\S]*bogus-user-key[\s\S]*bogus-scoped[\s\S]*npm help npmrc/,
709754
},
710-
'throws EUNKNOWNCONFIG aggregating plain and scoped keys'
755+
'throws EUNKNOWNCONFIG aggregating plain and scoped keys under strict-npmrc'
711756
)
712757
})
713758

714-
t.test('validateCli uses singular "key" when only one unknown file config', async t => {
759+
t.test('validateCli combines cli and file unknowns under strict-npmrc', async t => {
715760
const { npm } = await loadMockNpm(t, {
716761
homeDir: {
717-
'.npmrc': 'only-one-bad-key=yes',
762+
'.npmrc': [
763+
'strict-npmrc=true',
764+
'bogus-user-key=yes',
765+
].join('\n'),
718766
},
767+
npm: { argv: ['test-command', '--unknown-cli'] },
719768
})
720769

721770
class TestCommand extends BaseCommand {
722771
static name = 'test-command'
723772
static description = 'Test command'
773+
async exec () {
774+
return this.flags()
775+
}
724776
}
725777

726778
const command = new TestCommand(npm)
727-
await t.rejects(
728-
(async () => command.validateCli())(),
729-
{
730-
code: 'EUNKNOWNCONFIG',
731-
message: /Unknown npm configuration key:\n/,
732-
},
733-
'singular phrasing when only one unknown key'
734-
)
779+
let err
780+
try {
781+
command.validateCli()
782+
t.fail('expected throw')
783+
} catch (e) {
784+
err = e
785+
}
786+
t.equal(err.code, 'EUNKNOWNCONFIG')
787+
t.match(err.message, /Unknown cli flag:[\s\S]*--unknown-cli/,
788+
'message contains the cli section')
789+
t.match(err.message, /Unknown npm configuration key:[\s\S]*bogus-user-key/,
790+
'message contains the singular file section')
791+
t.match(err.unknownConfigs, [
792+
{ where: 'cli', key: 'unknown-cli' },
793+
{ where: 'user', key: 'bogus-user-key' },
794+
], 'structured payload contains both cli and file entries')
735795
})
736796

737797
t.test('validateCli bypasses checks when skipConfigValidation is set', async t => {
@@ -751,8 +811,8 @@ t.test('validateCli bypasses checks when skipConfigValidation is set', async t =
751811
t.doesNotThrow(() => command.validateCli(), 'skipConfigValidation bypasses unknown-file-config check')
752812
})
753813

754-
t.test('validateCli combines cli and file unknowns into a single error', async t => {
755-
const { npm } = await loadMockNpm(t, {
814+
t.test('validateCli throws only on cli unknowns while warning on file unknowns', async t => {
815+
const { npm, logs } = await loadMockNpm(t, {
756816
homeDir: {
757817
'.npmrc': 'bogus-user-key=yes',
758818
},
@@ -776,14 +836,15 @@ t.test('validateCli combines cli and file unknowns into a single error', async t
776836
err = e
777837
}
778838
t.equal(err.code, 'EUNKNOWNCONFIG')
779-
t.match(err.message, /Unknown cli config:[\s\S]*--unknown-cli/,
839+
t.match(err.message, /Unknown cli flag:[\s\S]*--unknown-cli/,
780840
'message contains the cli section')
781-
t.match(err.message, /Unknown npm configuration key:[\s\S]*bogus-user-key/,
782-
'message contains the file section')
841+
t.notMatch(err.message, /bogus-user-key/,
842+
'error message does not contain the file config')
783843
t.match(err.unknownConfigs, [
784844
{ where: 'cli', key: 'unknown-cli' },
785-
{ where: 'user', key: 'bogus-user-key' },
786-
], 'structured payload contains both cli and file entries')
845+
], 'structured payload contains only cli entries')
846+
t.ok(logs.warn.some(msg => msg.includes('bogus-user-key')),
847+
'file unknown is warned, not thrown')
787848
})
788849

789850
t.test('validateCli does not throw on unknown env (npm_config_*) configs', async t => {
@@ -830,7 +891,7 @@ t.test('flags() throws with scoped (nerfdart) display for unknown cli config', a
830891
command.exec(),
831892
{
832893
code: 'EUNKNOWNCONFIG',
833-
message: /Unknown cli config:[\s\S]*--bogus \(@scope:bogus\)/,
894+
message: /Unknown cli flag:[\s\S]*--bogus \(@scope:bogus\)/,
834895
},
835896
'scoped display form uses baseKey (key) layout, throws EUNKNOWNCONFIG'
836897
)

‎workspaces/config/lib/definitions/definitions.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2505,6 +2505,18 @@ const definitions = {
25052505
`,
25062506
flatten,
25072507
}),
2508+
'strict-npmrc': new Definition('strict-npmrc', {
2509+
default: false,
2510+
type: Boolean,
2511+
description: `
2512+
If set to \`true\`, unknown configuration keys found in \`.npmrc\` files
2513+
are treated as a hard error instead of a warning.
2514+
2515+
Unknown command line flags and abbreviated flags always error regardless
2516+
of this setting.
2517+
`,
2518+
flatten,
2519+
}),
25082520
'strict-ssl': new Definition('strict-ssl', {
25092521
default: true,
25102522
type: Boolean,

‎workspaces/config/tap-snapshots/test/type-description.js.test.cjs‎

Lines changed: 3 additions & 0 deletions

0 commit comments

Comments
 (0)