Skip to content

Commit

Permalink
feat: introduce the npm config fix command
Browse files Browse the repository at this point in the history
  • Loading branch information
nlf authored and fritzy committed Oct 13, 2022
1 parent d2963c6 commit a09e19d
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 2 deletions.
50 changes: 49 additions & 1 deletion lib/commands/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class Config extends BaseCommand {
'delete <key> [<key> ...]',
'list [--json]',
'edit',
'fix',
]

static params = [
Expand All @@ -72,7 +73,7 @@ class Config extends BaseCommand {
}

if (argv.length === 2) {
const cmds = ['get', 'set', 'delete', 'ls', 'rm', 'edit']
const cmds = ['get', 'set', 'delete', 'ls', 'rm', 'edit', 'fix']
if (opts.partialWord !== 'l') {
cmds.push('list')
}
Expand All @@ -97,6 +98,7 @@ class Config extends BaseCommand {
case 'edit':
case 'list':
case 'ls':
case 'fix':
default:
return []
}
Expand Down Expand Up @@ -129,6 +131,9 @@ class Config extends BaseCommand {
case 'edit':
await this.edit()
break
case 'fix':
await this.fix()
break
default:
throw this.usageError()
}
Expand Down Expand Up @@ -240,6 +245,49 @@ ${defData}
})
}

async fix () {
let problems

try {
this.npm.config.validate()
return // if validate doesn't throw we have nothing to do
} catch (err) {
// coverage skipped because we don't need to test rethrowing errors
// istanbul ignore next
if (err.code !== 'ERR_INVALID_AUTH') {
throw err
}

problems = err.problems
}

if (!this.npm.config.isDefault('location')) {
problems = problems.filter((problem) => {
return problem.where === this.npm.config.get('location')
})
}

this.npm.config.repair(problems)
const locations = []

this.npm.output('The following configuration problems have been repaired:\n')
const summary = problems.map(({ action, from, to, key, where }) => {
// coverage disabled for else branch because it is intentionally omitted
// istanbul ignore else
if (action === 'rename') {
// we keep track of which configs were modified here so we know what to save later
locations.push(where)
return `~ \`${from}\` renamed to \`${to}\` in ${where} config`
} else if (action === 'delete') {
locations.push(where)
return `- \`${key}\` deleted from ${where} config`
}
}).join('\n')
this.npm.output(summary)

return await Promise.all(locations.map((location) => this.npm.config.save(location)))
}

async list () {
const msg = []
// long does not have a flattener
Expand Down
2 changes: 2 additions & 0 deletions tap-snapshots/test/lib/docs.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2650,6 +2650,7 @@ npm config get [<key> [<key> ...]]
npm config delete <key> [<key> ...]
npm config list [--json]
npm config edit
npm config fix
Options:
[--json] [-g|--global] [--editor <editor>] [-L|--location <global|user|project>]
Expand All @@ -2665,6 +2666,7 @@ npm config get [<key> [<key> ...]]
npm config delete <key> [<key> ...]
npm config list [--json]
npm config edit
npm config fix
alias: c
\`\`\`
Expand Down
99 changes: 98 additions & 1 deletion test/lib/commands/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,102 @@ t.test('config edit - editor exits non-0', async t => {
)
})

t.test('config fix', (t) => {
t.test('no problems', async (t) => {
const home = t.testdir({
'.npmrc': '',
})

const sandbox = new Sandbox(t, { home })
await sandbox.run('config', ['fix'])
t.equal(sandbox.output, '', 'printed nothing')
})

t.test('repairs all configs by default', async (t) => {
const root = t.testdir({
global: {
npmrc: '_authtoken=notatoken\n_authToken=afaketoken',
},
home: {
'.npmrc': '_authtoken=thisisinvalid\n_auth=beef',
},
})
const registry = `//registry.npmjs.org/`

const sandbox = new Sandbox(t, {
global: join(root, 'global'),
home: join(root, 'home'),
})
await sandbox.run('config', ['fix'])

// global config fixes
t.match(sandbox.output, '`_authtoken` deleted from global config',
'output has deleted global _authtoken')
t.match(sandbox.output, `\`_authToken\` renamed to \`${registry}:_authToken\` in global config`,
'output has renamed global _authToken')
t.not(sandbox.config.get('_authtoken', 'global'), '_authtoken is not set globally')
t.not(sandbox.config.get('_authToken', 'global'), '_authToken is not set globally')
t.equal(sandbox.config.get(`${registry}:_authToken`, 'global'), 'afaketoken',
'global _authToken was scoped')
const globalConfig = await readFile(join(root, 'global', 'npmrc'), { encoding: 'utf8' })
t.equal(globalConfig, `${registry}:_authToken=afaketoken\n`, 'global config was written')

// user config fixes
t.match(sandbox.output, '`_authtoken` deleted from user config',
'output has deleted user _authtoken')
t.match(sandbox.output, `\`_auth\` renamed to \`${registry}:_auth\` in user config`,
'output has renamed user _auth')
t.not(sandbox.config.get('_authtoken', 'user'), '_authtoken is not set in user config')
t.not(sandbox.config.get('_auth'), '_auth is not set in user config')
t.equal(sandbox.config.get(`${registry}:_auth`, 'user'), 'beef', 'user _auth was scoped')
const userConfig = await readFile(join(root, 'home', '.npmrc'), { encoding: 'utf8' })
t.equal(userConfig, `${registry}:_auth=beef\n`, 'user config was written')
})

t.test('repairs only the config specified by --location if asked', async (t) => {
const root = t.testdir({
global: {
npmrc: '_authtoken=notatoken\n_authToken=afaketoken',
},
home: {
'.npmrc': '_authtoken=thisisinvalid\n_auth=beef',
},
})
const registry = `//registry.npmjs.org/`

const sandbox = new Sandbox(t, {
global: join(root, 'global'),
home: join(root, 'home'),
})
await sandbox.run('config', ['fix', '--location=user'])

// global config should be untouched
t.notMatch(sandbox.output, '`_authtoken` deleted from global',
'output has deleted global _authtoken')
t.notMatch(sandbox.output, `\`_authToken\` renamed to \`${registry}:_authToken\` in global`,
'output has renamed global _authToken')
t.equal(sandbox.config.get('_authtoken', 'global'), 'notatoken', 'global _authtoken untouched')
t.equal(sandbox.config.get('_authToken', 'global'), 'afaketoken', 'global _authToken untouched')
t.not(sandbox.config.get(`${registry}:_authToken`, 'global'), 'global _authToken not scoped')
const globalConfig = await readFile(join(root, 'global', 'npmrc'), { encoding: 'utf8' })
t.equal(globalConfig, '_authtoken=notatoken\n_authToken=afaketoken',
'global config was not written')

// user config fixes
t.match(sandbox.output, '`_authtoken` deleted from user',
'output has deleted user _authtoken')
t.match(sandbox.output, `\`_auth\` renamed to \`${registry}:_auth\` in user`,
'output has renamed user _auth')
t.not(sandbox.config.get('_authtoken', 'user'), '_authtoken is not set in user config')
t.not(sandbox.config.get('_auth', 'user'), '_auth is not set in user config')
t.equal(sandbox.config.get(`${registry}:_auth`, 'user'), 'beef', 'user _auth was scoped')
const userConfig = await readFile(join(root, 'home', '.npmrc'), { encoding: 'utf8' })
t.equal(userConfig, `${registry}:_auth=beef\n`, 'user config was written')
})

t.end()
})

t.test('completion', async t => {
const sandbox = new Sandbox(t)

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

await testComp([], ['get', 'set', 'delete', 'ls', 'rm', 'edit', 'list'])
await testComp([], ['get', 'set', 'delete', 'ls', 'rm', 'edit', 'fix', 'list'])
await testComp(['set', 'foo'], [])
await testComp(['get'], allKeys)
await testComp(['set'], allKeys)
await testComp(['delete'], allKeys)
await testComp(['rm'], allKeys)
await testComp(['edit'], [])
await testComp(['fix'], [])
await testComp(['list'], [])
await testComp(['ls'], [])

Expand Down

0 comments on commit a09e19d

Please sign in to comment.