Skip to content

Commit a09e19d

Browse files
nlffritzy
authored andcommitted
feat: introduce the npm config fix command
1 parent d2963c6 commit a09e19d

File tree

3 files changed

+149
-2
lines changed

3 files changed

+149
-2
lines changed

lib/commands/config.js

+49-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class Config extends BaseCommand {
5151
'delete <key> [<key> ...]',
5252
'list [--json]',
5353
'edit',
54+
'fix',
5455
]
5556

5657
static params = [
@@ -72,7 +73,7 @@ class Config extends BaseCommand {
7273
}
7374

7475
if (argv.length === 2) {
75-
const cmds = ['get', 'set', 'delete', 'ls', 'rm', 'edit']
76+
const cmds = ['get', 'set', 'delete', 'ls', 'rm', 'edit', 'fix']
7677
if (opts.partialWord !== 'l') {
7778
cmds.push('list')
7879
}
@@ -97,6 +98,7 @@ class Config extends BaseCommand {
9798
case 'edit':
9899
case 'list':
99100
case 'ls':
101+
case 'fix':
100102
default:
101103
return []
102104
}
@@ -129,6 +131,9 @@ class Config extends BaseCommand {
129131
case 'edit':
130132
await this.edit()
131133
break
134+
case 'fix':
135+
await this.fix()
136+
break
132137
default:
133138
throw this.usageError()
134139
}
@@ -240,6 +245,49 @@ ${defData}
240245
})
241246
}
242247

248+
async fix () {
249+
let problems
250+
251+
try {
252+
this.npm.config.validate()
253+
return // if validate doesn't throw we have nothing to do
254+
} catch (err) {
255+
// coverage skipped because we don't need to test rethrowing errors
256+
// istanbul ignore next
257+
if (err.code !== 'ERR_INVALID_AUTH') {
258+
throw err
259+
}
260+
261+
problems = err.problems
262+
}
263+
264+
if (!this.npm.config.isDefault('location')) {
265+
problems = problems.filter((problem) => {
266+
return problem.where === this.npm.config.get('location')
267+
})
268+
}
269+
270+
this.npm.config.repair(problems)
271+
const locations = []
272+
273+
this.npm.output('The following configuration problems have been repaired:\n')
274+
const summary = problems.map(({ action, from, to, key, where }) => {
275+
// coverage disabled for else branch because it is intentionally omitted
276+
// istanbul ignore else
277+
if (action === 'rename') {
278+
// we keep track of which configs were modified here so we know what to save later
279+
locations.push(where)
280+
return `~ \`${from}\` renamed to \`${to}\` in ${where} config`
281+
} else if (action === 'delete') {
282+
locations.push(where)
283+
return `- \`${key}\` deleted from ${where} config`
284+
}
285+
}).join('\n')
286+
this.npm.output(summary)
287+
288+
return await Promise.all(locations.map((location) => this.npm.config.save(location)))
289+
}
290+
243291
async list () {
244292
const msg = []
245293
// long does not have a flattener

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

+2
Original file line numberDiff line numberDiff line change
@@ -2650,6 +2650,7 @@ npm config get [<key> [<key> ...]]
26502650
npm config delete <key> [<key> ...]
26512651
npm config list [--json]
26522652
npm config edit
2653+
npm config fix
26532654
26542655
Options:
26552656
[--json] [-g|--global] [--editor <editor>] [-L|--location <global|user|project>]
@@ -2665,6 +2666,7 @@ npm config get [<key> [<key> ...]]
26652666
npm config delete <key> [<key> ...]
26662667
npm config list [--json]
26672668
npm config edit
2669+
npm config fix
26682670
26692671
alias: c
26702672
\`\`\`

test/lib/commands/config.js

+98-1
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,102 @@ t.test('config edit - editor exits non-0', async t => {
411411
)
412412
})
413413

414+
t.test('config fix', (t) => {
415+
t.test('no problems', async (t) => {
416+
const home = t.testdir({
417+
'.npmrc': '',
418+
})
419+
420+
const sandbox = new Sandbox(t, { home })
421+
await sandbox.run('config', ['fix'])
422+
t.equal(sandbox.output, '', 'printed nothing')
423+
})
424+
425+
t.test('repairs all configs by default', async (t) => {
426+
const root = t.testdir({
427+
global: {
428+
npmrc: '_authtoken=notatoken\n_authToken=afaketoken',
429+
},
430+
home: {
431+
'.npmrc': '_authtoken=thisisinvalid\n_auth=beef',
432+
},
433+
})
434+
const registry = `//registry.npmjs.org/`
435+
436+
const sandbox = new Sandbox(t, {
437+
global: join(root, 'global'),
438+
home: join(root, 'home'),
439+
})
440+
await sandbox.run('config', ['fix'])
441+
442+
// global config fixes
443+
t.match(sandbox.output, '`_authtoken` deleted from global config',
444+
'output has deleted global _authtoken')
445+
t.match(sandbox.output, `\`_authToken\` renamed to \`${registry}:_authToken\` in global config`,
446+
'output has renamed global _authToken')
447+
t.not(sandbox.config.get('_authtoken', 'global'), '_authtoken is not set globally')
448+
t.not(sandbox.config.get('_authToken', 'global'), '_authToken is not set globally')
449+
t.equal(sandbox.config.get(`${registry}:_authToken`, 'global'), 'afaketoken',
450+
'global _authToken was scoped')
451+
const globalConfig = await readFile(join(root, 'global', 'npmrc'), { encoding: 'utf8' })
452+
t.equal(globalConfig, `${registry}:_authToken=afaketoken\n`, 'global config was written')
453+
454+
// user config fixes
455+
t.match(sandbox.output, '`_authtoken` deleted from user config',
456+
'output has deleted user _authtoken')
457+
t.match(sandbox.output, `\`_auth\` renamed to \`${registry}:_auth\` in user config`,
458+
'output has renamed user _auth')
459+
t.not(sandbox.config.get('_authtoken', 'user'), '_authtoken is not set in user config')
460+
t.not(sandbox.config.get('_auth'), '_auth is not set in user config')
461+
t.equal(sandbox.config.get(`${registry}:_auth`, 'user'), 'beef', 'user _auth was scoped')
462+
const userConfig = await readFile(join(root, 'home', '.npmrc'), { encoding: 'utf8' })
463+
t.equal(userConfig, `${registry}:_auth=beef\n`, 'user config was written')
464+
})
465+
466+
t.test('repairs only the config specified by --location if asked', async (t) => {
467+
const root = t.testdir({
468+
global: {
469+
npmrc: '_authtoken=notatoken\n_authToken=afaketoken',
470+
},
471+
home: {
472+
'.npmrc': '_authtoken=thisisinvalid\n_auth=beef',
473+
},
474+
})
475+
const registry = `//registry.npmjs.org/`
476+
477+
const sandbox = new Sandbox(t, {
478+
global: join(root, 'global'),
479+
home: join(root, 'home'),
480+
})
481+
await sandbox.run('config', ['fix', '--location=user'])
482+
483+
// global config should be untouched
484+
t.notMatch(sandbox.output, '`_authtoken` deleted from global',
485+
'output has deleted global _authtoken')
486+
t.notMatch(sandbox.output, `\`_authToken\` renamed to \`${registry}:_authToken\` in global`,
487+
'output has renamed global _authToken')
488+
t.equal(sandbox.config.get('_authtoken', 'global'), 'notatoken', 'global _authtoken untouched')
489+
t.equal(sandbox.config.get('_authToken', 'global'), 'afaketoken', 'global _authToken untouched')
490+
t.not(sandbox.config.get(`${registry}:_authToken`, 'global'), 'global _authToken not scoped')
491+
const globalConfig = await readFile(join(root, 'global', 'npmrc'), { encoding: 'utf8' })
492+
t.equal(globalConfig, '_authtoken=notatoken\n_authToken=afaketoken',
493+
'global config was not written')
494+
495+
// user config fixes
496+
t.match(sandbox.output, '`_authtoken` deleted from user',
497+
'output has deleted user _authtoken')
498+
t.match(sandbox.output, `\`_auth\` renamed to \`${registry}:_auth\` in user`,
499+
'output has renamed user _auth')
500+
t.not(sandbox.config.get('_authtoken', 'user'), '_authtoken is not set in user config')
501+
t.not(sandbox.config.get('_auth', 'user'), '_auth is not set in user config')
502+
t.equal(sandbox.config.get(`${registry}:_auth`, 'user'), 'beef', 'user _auth was scoped')
503+
const userConfig = await readFile(join(root, 'home', '.npmrc'), { encoding: 'utf8' })
504+
t.equal(userConfig, `${registry}:_auth=beef\n`, 'user config was written')
505+
})
506+
507+
t.end()
508+
})
509+
414510
t.test('completion', async t => {
415511
const sandbox = new Sandbox(t)
416512

@@ -423,13 +519,14 @@ t.test('completion', async t => {
423519
sandbox.reset()
424520
}
425521

426-
await testComp([], ['get', 'set', 'delete', 'ls', 'rm', 'edit', 'list'])
522+
await testComp([], ['get', 'set', 'delete', 'ls', 'rm', 'edit', 'fix', 'list'])
427523
await testComp(['set', 'foo'], [])
428524
await testComp(['get'], allKeys)
429525
await testComp(['set'], allKeys)
430526
await testComp(['delete'], allKeys)
431527
await testComp(['rm'], allKeys)
432528
await testComp(['edit'], [])
529+
await testComp(['fix'], [])
433530
await testComp(['list'], [])
434531
await testComp(['ls'], [])
435532

0 commit comments

Comments
 (0)