npm CLI robot ba22c013a2
deps: upgrade npm to 11.2.0
PR-URL: https://github.com/nodejs/node/pull/57334
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
2025-03-13 12:31:42 +00:00

718 lines
20 KiB
JavaScript

const { join } = require('node:path')
const fs = require('node:fs/promises')
const ini = require('ini')
const tspawk = require('../../fixtures/tspawk')
const t = require('tap')
const { load: _loadMockNpm } = require('../../fixtures/mock-npm')
const { cleanCwd } = require('../../fixtures/clean-snapshot.js')
const spawk = tspawk(t)
const replaceJsonOrIni = (key) => [
new RegExp(`(\\s(?:${key} = |"${key}": )"?)[^"\\n,]+`, 'g'),
`$1{${key.toUpperCase()}}`,
]
const replaceIniComment = (key) => [
new RegExp(`(; ${key} = ).*`, 'g'),
`$1{${key.replaceAll(' ', '-').toUpperCase()}}`,
]
t.cleanSnapshot = (s) => cleanCwd(s)
.replaceAll(...replaceIniComment('node version'))
.replaceAll(...replaceIniComment('npm version'))
.replaceAll(...replaceIniComment('node bin location'))
.replaceAll(...replaceJsonOrIni('npm-version'))
.replaceAll(...replaceJsonOrIni('viewer'))
.replaceAll(...replaceJsonOrIni('shell'))
.replaceAll(...replaceJsonOrIni('editor'))
.replaceAll(...replaceJsonOrIni('progress'))
.replaceAll(...replaceJsonOrIni('color'))
.replaceAll(...replaceJsonOrIni('cache'))
const loadMockNpm = (t, opts = {}) => _loadMockNpm(t, {
...opts,
config: {
...opts.config,
// Reset configs that mock npm sets by default
'fetch-retries': undefined,
loglevel: undefined,
color: false,
},
})
t.test('config no args', async t => {
const { npm } = await loadMockNpm(t)
await t.rejects(
npm.exec('config', []),
{
code: 'EUSAGE',
},
'rejects with usage'
)
})
t.test('config ignores workspaces', async t => {
const { npm } = await loadMockNpm(t, {
config: {
workspaces: true,
},
})
await t.rejects(
npm.exec('config'),
{
code: 'ENOWORKSPACES',
},
'rejects with usage'
)
})
t.test('config list', async t => {
const { npm, joinedOutput } = await loadMockNpm(t, {
prefixDir: {
'.npmrc': 'projectloaded=yes',
},
globalPrefixDir: {
etc: {
npmrc: 'globalloaded=yes',
},
},
homeDir: {
'.npmrc': [
'userloaded=yes',
'auth=bad',
'_auth=bad',
'//nerfdart:auth=bad',
'//nerfdart:_auth=bad',
].join('\n'),
},
})
await npm.exec('config', ['list'])
const output = joinedOutput()
t.match(output, 'projectloaded = "yes"')
t.match(output, 'globalloaded = "yes"')
t.match(output, 'userloaded = "yes"')
t.matchSnapshot(output, 'output matches snapshot')
})
t.test('config list --long', async t => {
const { npm, joinedOutput } = await loadMockNpm(t, {
prefixDir: {
'.npmrc': 'projectloaded=yes',
},
globalPrefixDir: {
etc: {
npmrc: 'globalloaded=yes',
},
},
homeDir: {
'.npmrc': 'userloaded=yes',
},
config: {
long: true,
},
})
await npm.exec('config', ['list'])
const output = joinedOutput()
t.match(output, 'projectloaded = "yes"')
t.match(output, 'globalloaded = "yes"')
t.match(output, 'userloaded = "yes"')
t.matchSnapshot(output, 'output matches snapshot')
})
t.test('config list --json', async t => {
const { npm, joinedOutput } = await loadMockNpm(t, {
prefixDir: {
'.npmrc': 'projectloaded=yes',
},
globalPrefixDir: {
etc: {
npmrc: 'globalloaded=yes',
},
},
homeDir: {
'.npmrc': 'userloaded=yes',
},
config: {
json: true,
},
})
await npm.exec('config', ['list'])
const output = joinedOutput()
t.match(output, '"projectloaded": "yes",')
t.match(output, '"globalloaded": "yes",')
t.match(output, '"userloaded": "yes",')
t.matchSnapshot(output, 'output matches snapshot')
})
t.test('config list with publishConfig', async t => {
const loadMockNpmWithPublishConfig = (t, opts) => loadMockNpm(t, {
prefixDir: {
'package.json': JSON.stringify({
publishConfig: {
other: 'not defined',
registry: 'https://some.registry',
'//some.registry:_authToken': 'mytoken',
},
}),
},
...opts,
})
t.test('local', async t => {
const { npm, logs, joinedOutput } = await loadMockNpmWithPublishConfig(t)
await npm.exec('config', ['list'])
const output = joinedOutput()
t.match(output, 'registry = "https://some.registry"')
t.matchSnapshot(output, 'output matches snapshot')
t.matchSnapshot(logs.warn, 'warns about unknown config')
})
t.test('global', async t => {
const { npm, joinedOutput } = await loadMockNpmWithPublishConfig(t, {
config: {
global: true,
},
})
await npm.exec('config', ['list'])
const output = joinedOutput()
t.notMatch(output, 'registry = "https://some.registry"')
t.matchSnapshot(output, 'output matches snapshot')
})
})
t.test('config delete no args', async t => {
const { npm } = await loadMockNpm(t)
await t.rejects(
npm.exec('config', ['delete']),
{
code: 'EUSAGE',
},
'rejects with usage'
)
})
t.test('config delete single key', async t => {
// location defaults to user, so we work with a userconfig
const { npm, home } = await loadMockNpm(t, {
homeDir: {
'.npmrc': 'access=public\nall=true',
},
})
await npm.exec('config', ['delete', 'access'])
t.equal(npm.config.get('access'), null, 'acces should be defaulted')
const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' })
const rc = ini.parse(contents)
t.not(rc.access, 'access is not set')
})
t.test('config delete multiple keys', async t => {
const { npm, home } = await loadMockNpm(t, {
homeDir: {
'.npmrc': 'access=public\nall=true\naudit=false',
},
})
await npm.exec('config', ['delete', 'access', 'all'])
t.equal(npm.config.get('access'), null, 'access should be defaulted')
t.equal(npm.config.get('all'), false, 'all should be defaulted')
const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' })
const rc = ini.parse(contents)
t.not(rc.access, 'access is not set')
t.not(rc.all, 'all is not set')
})
t.test('config delete key --location=global', async t => {
const { npm, globalPrefix } = await loadMockNpm(t, {
globalPrefixDir: {
etc: {
npmrc: 'access=public\nall=true',
},
},
config: {
location: 'global',
},
})
await npm.exec('config', ['delete', 'access'])
t.equal(npm.config.get('access', 'global'), undefined, 'access should be defaulted')
const contents = await fs.readFile(join(globalPrefix, 'etc/npmrc'), { encoding: 'utf8' })
const rc = ini.parse(contents)
t.not(rc.access, 'access is not set')
})
t.test('config delete key --global', async t => {
const { npm, globalPrefix } = await loadMockNpm(t, {
globalPrefixDir: {
etc: {
npmrc: 'access=public\nall=true',
},
},
config: {
global: true,
},
})
await npm.exec('config', ['delete', 'access'])
t.equal(npm.config.get('access', 'global'), undefined, 'access should no longer be set')
const contents = await fs.readFile(join(globalPrefix, 'etc/npmrc'), { encoding: 'utf8' })
const rc = ini.parse(contents)
t.not(rc.access, 'access is not set')
})
t.test('config set invalid option', async t => {
const { npm } = await loadMockNpm(t)
await t.rejects(
npm.exec('config', ['set', 'nonexistantconfigoption', 'something']),
/not a valid npm option/
)
})
t.test('config set deprecated option', async t => {
const { npm } = await loadMockNpm(t)
await t.rejects(
npm.exec('config', ['set', 'shrinkwrap', 'true']),
/deprecated/
)
})
t.test('config set nerf-darted option', async t => {
const { npm } = await loadMockNpm(t)
await npm.exec('config', ['set', '//npm.pkg.github.com/:_authToken', '0xdeadbeef'])
t.equal(
npm.config.get('//npm.pkg.github.com/:_authToken'),
'0xdeadbeef',
'nerf-darted config is set'
)
})
t.test('config set scoped optoin', async t => {
const { npm } = await loadMockNpm(t)
await npm.exec('config', ['set', '@npm:registry', 'https://registry.npmjs.org'])
t.equal(
npm.config.get('@npm:registry'),
'https://registry.npmjs.org',
'scoped config is set'
)
})
t.test('config set no args', async t => {
const { npm } = await loadMockNpm(t)
await t.rejects(
npm.exec('config', ['set']),
{
code: 'EUSAGE',
},
'rejects with usage'
)
})
t.test('config set key', async t => {
const { npm, home } = await loadMockNpm(t, {
homeDir: {
'.npmrc': 'access=public',
},
})
await npm.exec('config', ['set', 'access'])
t.equal(npm.config.get('access'), null, 'set the value for access')
await t.rejects(fs.stat(join(home, '.npmrc'), { encoding: 'utf8' }), 'removed empty config')
})
t.test('config set key value', async t => {
const { npm, home } = await loadMockNpm(t, {
homeDir: {
'.npmrc': 'access=public',
},
})
await npm.exec('config', ['set', 'access', 'restricted'])
t.equal(npm.config.get('access'), 'restricted', 'set the value for access')
const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' })
const rc = ini.parse(contents)
t.equal(rc.access, 'restricted', 'access is set to restricted')
})
t.test('config set key value with equals', async t => {
const { npm, home } = await loadMockNpm(t, {
homeDir: {
'.npmrc': 'access=public',
},
})
await npm.exec('config', ['set', 'access=restricted'])
t.equal(npm.config.get('access'), 'restricted', 'set the value for access')
const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' })
const rc = ini.parse(contents)
t.equal(rc.access, 'restricted', 'access is set to restricted')
})
t.test('config set key1 value1 key2=value2 key3', async t => {
const { npm, home } = await loadMockNpm(t, {
homeDir: {
'.npmrc': 'access=public\nall=true\naudit=true',
},
})
await npm.exec('config', ['set', 'access', 'restricted', 'all=false', 'audit'])
t.equal(npm.config.get('access'), 'restricted', 'access was set')
t.equal(npm.config.get('all'), false, 'all was set')
t.equal(npm.config.get('audit'), true, 'audit was unset and restored to its default')
const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' })
const rc = ini.parse(contents)
t.equal(rc.access, 'restricted', 'access is set to restricted')
t.equal(rc.all, false, 'all is set to false')
t.not(contents.includes('audit='), 'config file does not set audit')
})
t.test('config set invalid key logs warning', async t => {
const { npm, logs, home } = await loadMockNpm(t)
// this doesn't reject, it only logs a warning
await npm.exec('config', ['set', 'access=foo'])
t.equal(logs.warn[0],
`invalid config access="foo" set in ${join(home, '.npmrc')}`,
'logged warning'
)
})
t.test('config set key=value --location=global', async t => {
const { npm, globalPrefix } = await loadMockNpm(t, {
globalPrefixDir: {
etc: {
npmrc: 'access=public\nall=true',
},
},
config: {
location: 'global',
},
})
await npm.exec('config', ['set', 'access=restricted'])
t.equal(npm.config.get('access', 'global'), 'restricted', 'foo should be set')
const contents = await fs.readFile(join(globalPrefix, 'etc/npmrc'), { encoding: 'utf8' })
const rc = ini.parse(contents)
t.equal(rc.access, 'restricted', 'access is set to restricted')
})
t.test('config set key=value --global', async t => {
const { npm, globalPrefix } = await loadMockNpm(t, {
globalPrefixDir: {
etc: {
npmrc: 'access=public\nall=true',
},
},
config: {
global: true,
},
})
await npm.exec('config', ['set', 'access=restricted'])
t.equal(npm.config.get('access', 'global'), 'restricted', 'access should be set')
const contents = await fs.readFile(join(globalPrefix, 'etc/npmrc'), { encoding: 'utf8' })
const rc = ini.parse(contents)
t.equal(rc.access, 'restricted', 'access is set to restricted')
})
t.test('config get no args', async t => {
const { npm, joinedOutput, clearOutput } = await loadMockNpm(t)
await npm.exec('config', ['get'])
const getOutput = joinedOutput()
clearOutput()
await npm.exec('config', ['list'])
const listOutput = joinedOutput()
t.equal(listOutput, getOutput, 'get with no args outputs list')
})
t.test('config get single key', async t => {
const { npm, joinedOutput } = await loadMockNpm(t)
await npm.exec('config', ['get', 'all'])
t.equal(joinedOutput(), `${npm.config.get('all')}`, 'should get the value')
})
t.test('config get multiple keys', async t => {
const { npm, joinedOutput } = await loadMockNpm(t)
await npm.exec('config', ['get', 'yes', 'all'])
t.equal(joinedOutput(), `yes=${npm.config.get('yes')}\nall=${npm.config.get('all')}`)
})
t.test('config get private key', async t => {
const { npm } = await loadMockNpm(t)
await t.rejects(
npm.exec('config', ['get', '_authToken']),
/_authToken option is protected/,
'rejects with protected string'
)
await t.rejects(
npm.exec('config', ['get', 'authToken']),
/authToken option is protected/,
'rejects with protected string'
)
await t.rejects(
npm.exec('config', ['get', '//localhost:8080/:_password']),
/_password option is protected/,
'rejects with protected string'
)
})
t.test('config redacted values', async t => {
const { npm, joinedOutput, clearOutput } = await loadMockNpm(t)
await npm.exec('config', ['set', 'proxy', 'https://proxy.npmjs.org/'])
await npm.exec('config', ['get', 'proxy'])
t.equal(joinedOutput(), 'https://proxy.npmjs.org/')
clearOutput()
await npm.exec('config', ['set', 'proxy', 'https://u:password@proxy.npmjs.org/'])
await t.rejects(npm.exec('config', ['get', 'proxy']), /proxy option is protected/)
await npm.exec('config', ['ls'])
t.match(joinedOutput(), 'proxy = "https://u:***@proxy.npmjs.org/"')
clearOutput()
})
t.test('config edit', async t => {
const EDITOR = 'vim'
const editor = spawk.spawn(EDITOR).exit(0)
const { npm, home } = await loadMockNpm(t, {
homeDir: {
'.npmrc': 'foo=bar\nbar=baz',
},
config: {
editor: EDITOR,
},
})
await npm.exec('config', ['edit'])
t.ok(editor.called, 'editor was spawned')
t.same(
editor.calledWith.args,
[join(home, '.npmrc')],
'editor opened the user config file'
)
const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' })
t.ok(contents.includes('foo=bar'), 'kept foo')
t.ok(contents.includes('bar=baz'), 'kept bar')
t.ok(contents.includes('shown below with default values'), 'appends defaults to file')
})
t.test('config edit - editor exits non-0', async t => {
const EDITOR = 'vim'
const editor = spawk.spawn(EDITOR).exit(1)
const { npm, home } = await loadMockNpm(t, {
config: {
editor: EDITOR,
},
})
await t.rejects(
npm.exec('config', ['edit']),
{
message: 'editor process exited with code: 1',
},
'rejects with error about editor code'
)
t.ok(editor.called, 'editor was spawned')
t.same(
editor.calledWith.args,
[join(home, '.npmrc')],
'editor opened the user config file'
)
})
t.test('config fix', (t) => {
t.test('no problems', async (t) => {
const { npm, joinedOutput } = await loadMockNpm(t, {
homeDir: {
'.npmrc': '',
},
})
await npm.exec('config', ['fix'])
t.equal(joinedOutput(), '', 'printed nothing')
})
t.test('repairs all configs by default', async (t) => {
const { npm, home, globalPrefix, joinedOutput } = await loadMockNpm(t, {
globalPrefixDir: {
etc: {
npmrc: '_authtoken=notatoken\n_authToken=afaketoken',
},
},
homeDir: {
'.npmrc': '_authtoken=thisisinvalid\n_auth=beef',
},
})
const registry = `//registry.npmjs.org/`
await npm.exec('config', ['fix'])
// global config fixes
t.match(joinedOutput(), '`_authtoken` deleted from global config',
'output has deleted global _authtoken')
t.match(joinedOutput(), `\`_authToken\` renamed to \`${registry}:_authToken\` in global config`,
'output has renamed global _authToken')
t.not(npm.config.get('_authtoken', 'global'), '_authtoken is not set globally')
t.not(npm.config.get('_authToken', 'global'), '_authToken is not set globally')
t.equal(npm.config.get(`${registry}:_authToken`, 'global'), 'afaketoken',
'global _authToken was scoped')
const globalConfig = await fs.readFile(join(globalPrefix, 'etc/npmrc'), { encoding: 'utf8' })
t.equal(globalConfig, `${registry}:_authToken=afaketoken\n`, 'global config was written')
// user config fixes
t.match(joinedOutput(), '`_authtoken` deleted from user config',
'output has deleted user _authtoken')
t.match(joinedOutput(), `\`_auth\` renamed to \`${registry}:_auth\` in user config`,
'output has renamed user _auth')
t.not(npm.config.get('_authtoken', 'user'), '_authtoken is not set in user config')
t.not(npm.config.get('_auth'), '_auth is not set in user config')
t.equal(npm.config.get(`${registry}:_auth`, 'user'), 'beef', 'user _auth was scoped')
const userConfig = await fs.readFile(join(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 { npm, home, globalPrefix, joinedOutput } = await loadMockNpm(t, {
globalPrefixDir: {
etc: {
npmrc: '_authtoken=notatoken\n_authToken=afaketoken',
},
},
homeDir: {
'.npmrc': '_authtoken=thisisinvalid\n_auth=beef',
},
config: {
location: 'user',
},
})
const registry = `//registry.npmjs.org/`
await npm.exec('config', ['fix'])
// global config should be untouched
t.notMatch(joinedOutput(), '`_authtoken` deleted from global',
'output has deleted global _authtoken')
t.notMatch(joinedOutput(), `\`_authToken\` renamed to \`${registry}:_authToken\` in global`,
'output has renamed global _authToken')
t.equal(npm.config.get('_authtoken', 'global'), 'notatoken', 'global _authtoken untouched')
t.equal(npm.config.get('_authToken', 'global'), 'afaketoken', 'global _authToken untouched')
t.not(npm.config.get(`${registry}:_authToken`, 'global'), 'global _authToken not scoped')
const globalConfig = await fs.readFile(join(globalPrefix, 'etc/npmrc'), { encoding: 'utf8' })
t.equal(globalConfig, '_authtoken=notatoken\n_authToken=afaketoken',
'global config was not written')
// user config fixes
t.match(joinedOutput(), '`_authtoken` deleted from user',
'output has deleted user _authtoken')
t.match(joinedOutput(), `\`_auth\` renamed to \`${registry}:_auth\` in user`,
'output has renamed user _auth')
t.not(npm.config.get('_authtoken', 'user'), '_authtoken is not set in user config')
t.not(npm.config.get('_auth', 'user'), '_auth is not set in user config')
t.equal(npm.config.get(`${registry}:_auth`, 'user'), 'beef', 'user _auth was scoped')
const userConfig = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' })
t.equal(userConfig, `${registry}:_auth=beef\n`, 'user config was written')
})
t.end()
})
t.test('completion', async t => {
const { config, npm } = await loadMockNpm(t, { command: 'config' })
const allKeys = Object.keys(npm.config.definitions)
const testComp = async (argv, expect, msg) => {
const options = Array.isArray(argv) ? {
conf: {
argv: {
remain: ['config', ...argv],
},
},
} : argv
options.conf.argv.remain.unshift('npm')
const res = await config.completion(options)
t.strictSame(res, expect, msg ?? argv.join(' '))
}
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'], [])
await testComp({
conf: { argv: { remain: ['get'] } },
}, allKeys, 'also works for just npm get')
await testComp({
partialWord: 'l',
conf: { argv: { remain: ['config'] } },
}, ['get', 'set', 'delete', 'ls', 'rm', 'edit', 'fix'], 'and works on partials')
})