From 74be0d2fcabb393a3f1df62b8bce98953130a4e4 Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Tue, 3 Jan 2023 00:41:18 -0700 Subject: [PATCH 1/9] fix: correctly parse browser as config as string --- lib/utils/config/definitions.js | 3 +- test/lib/commands/get.js | 5 +- test/lib/commands/pack.js | 10 +- test/lib/docs.js | 2 +- test/lib/npm.js | 54 ++++--- test/lib/utils/open-url.js | 233 +++++++++++------------------ workspaces/config/lib/type-defs.js | 17 +++ 7 files changed, 152 insertions(+), 172 deletions(-) diff --git a/lib/utils/config/definitions.js b/lib/utils/config/definitions.js index dd3d9946af819..01c06476f0b60 100644 --- a/lib/utils/config/definitions.js +++ b/lib/utils/config/definitions.js @@ -98,6 +98,7 @@ const { Umask: { type: Umask }, url: { type: url }, path: { type: path }, + BooleanOrString: { type: BooleanOrString }, }, } = Config @@ -278,7 +279,7 @@ define('browser', { defaultDescription: ` OS X: \`"open"\`, Windows: \`"start"\`, Others: \`"xdg-open"\` `, - type: [null, Boolean, String], + type: BooleanOrString, description: ` The browser that is called by npm commands to open websites. diff --git a/test/lib/commands/get.js b/test/lib/commands/get.js index 597cccc3ff0ba..dec634f835172 100644 --- a/test/lib/commands/get.js +++ b/test/lib/commands/get.js @@ -2,10 +2,11 @@ const t = require('tap') const { load: loadMockNpm } = require('../../fixtures/mock-npm') t.test('should retrieve values from config', async t => { - const { joinedOutput, npm } = await loadMockNpm(t) const name = 'editor' const value = 'vigor' - npm.config.set(name, value) + const { joinedOutput, npm } = await loadMockNpm(t, { + config: { [name]: value }, + }) await npm.exec('get', [name]) t.equal( joinedOutput(), diff --git a/test/lib/commands/pack.js b/test/lib/commands/pack.js index 3e7c0225c3068..b97482a7e7c2d 100644 --- a/test/lib/commands/pack.js +++ b/test/lib/commands/pack.js @@ -28,8 +28,10 @@ t.test('follows pack-destination config', async t => { }), 'tar-destination': {}, }, + config: ({ prefix }) => ({ + 'pack-destination': path.join(npm.prefix, 'tar-destination'), + }), }) - npm.config.set('pack-destination', path.join(npm.prefix, 'tar-destination')) await npm.exec('pack', []) const filename = 'test-package-1.0.0.tgz' t.strictSame(outputs, [[filename]]) @@ -59,8 +61,8 @@ t.test('should log output as valid json', async t => { version: '1.0.0', }), }, + config: { json: true }, }) - npm.config.set('json', true) await npm.exec('pack', []) const filename = 'test-package-1.0.0.tgz' t.matchSnapshot(outputs.map(JSON.parse), 'outputs as json') @@ -76,8 +78,8 @@ t.test('should log scoped package output as valid json', async t => { version: '1.0.0', }), }, + config: { json: true }, }) - npm.config.set('json', true) await npm.exec('pack', []) const filename = 'myscope-test-package-1.0.0.tgz' t.matchSnapshot(outputs.map(JSON.parse), 'outputs as json') @@ -93,8 +95,8 @@ t.test('dry run', async t => { version: '1.0.0', }), }, + config: { 'dry-run': true }, }) - npm.config.set('dry-run', true) await npm.exec('pack', []) const filename = 'test-package-1.0.0.tgz' t.strictSame(outputs, [[filename]]) diff --git a/test/lib/docs.js b/test/lib/docs.js index e8a188b6ad8c4..7eb33dd392431 100644 --- a/test/lib/docs.js +++ b/test/lib/docs.js @@ -43,9 +43,9 @@ t.test('basic usage', async t => { mocks: { '{LIB}/utils/cmd-list.js': { commands: [] }, }, + config: { userconfig: '/some/config/file/.npmrc' }, }) - npm.config.set('userconfig', '/some/config/file/.npmrc') t.matchSnapshot(await npm.usage) }) diff --git a/test/lib/npm.js b/test/lib/npm.js index e6936b3e36d5f..dd3ca307c72c9 100644 --- a/test/lib/npm.js +++ b/test/lib/npm.js @@ -664,45 +664,59 @@ t.test('usage', async t => { t.test('with browser', async t => { mockGlobals(t, { process: { platform: 'posix' } }) const { npm } = await loadMockNpm(t) - const usage = await npm.usage - npm.config.set('viewer', 'browser') - const browserUsage = await npm.usage - t.notMatch(usage, '(in a browser)') - t.match(browserUsage, '(in a browser)') + t.notMatch(await npm.usage, '(in a browser)') + }) + + t.test('without browser', async t => { + mockGlobals(t, { process: { platform: 'posix' } }) + const { npm } = await loadMockNpm(t, { config: { viewer: 'browser' } }) + t.match(await npm.usage, '(in a browser)') }) t.test('windows always uses browser', async t => { mockGlobals(t, { process: { platform: 'win32' } }) - const { npm } = await loadMockNpm(t) + const { npm } = await loadMockNpm(t, { config: { viewer: 'browser' } }) const usage = await npm.usage - npm.config.set('viewer', 'browser') const browserUsage = await npm.usage t.match(usage, '(in a browser)') t.match(browserUsage, '(in a browser)') }) t.test('includes commands', async t => { - const { npm } = await loadMockNpm(t) - const usage = await npm.usage - npm.config.set('long', true) - const longUsage = await npm.usage - - const lastCmd = commands[commands.length - 1] - for (const cmd of commands) { + const rCmd = (cmd) => { + const lastCmd = commands[commands.length - 1] const isLast = cmd === lastCmd const shortCmd = new RegExp(`\\s${cmd}${isLast ? '\\n' : ',[\\s\\n]'}`) const longCmd = new RegExp(`^\\s+${cmd}\\s+\\w.*\n\\s+Usage:\\n`, 'm') + return { last: isLast, short: shortCmd, long: longCmd } + } + + t.test('usage', async t => { + const { npm } = await loadMockNpm(t) + const usage = await npm.usage - t.match(usage, shortCmd, `usage includes ${cmd}`) - t.notMatch(usage, longCmd, `usage does not include long ${cmd}`) + for (const cmd of commands) { + const { short, long } = rCmd(cmd) - t.match(longUsage, longCmd, `long usage includes ${cmd}`) - if (!isLast) { + t.match(usage, short, `usage includes ${cmd}`) + t.notMatch(usage, long, `usage does not include long ${cmd}`) + } + }) + + t.test('long usage', async t => { + const { npm } = await loadMockNpm(t, { config: { long: true } }) + const usage = await npm.usage + + for (const cmd of commands) { + const { short, long, last } = rCmd(cmd) + t.match(usage, long, `long usage includes ${cmd}`) + if (!last) { // long usage includes false positives for the last command since it is // not followed by a comma - t.notMatch(longUsage, shortCmd, `long usage does not include short ${cmd}`) + t.notMatch(usage, short, `long usage does not include short ${cmd}`) + } } - } + }) }) t.test('set process.stdout.columns', async t => { diff --git a/test/lib/utils/open-url.js b/test/lib/utils/open-url.js index 28a11b3609c67..48266290261a1 100644 --- a/test/lib/utils/open-url.js +++ b/test/lib/utils/open-url.js @@ -1,197 +1,142 @@ const t = require('tap') const tmock = require('../../fixtures/tmock') +const mockNpm = require('../../fixtures/mock-npm') -const OUTPUT = [] -const output = (...args) => OUTPUT.push(args) -const npm = { - _config: { - json: false, - browser: true, - }, - config: { - get: k => npm._config[k], - set: (k, v) => { - npm._config[k] = v - }, - }, - output, -} +const mockOpenUrl = async (t, args, { openerResult, ...config } = {}) => { + let openerUrl = null + let openerOpts = null + + const open = async (url, options) => { + openerUrl = url + openerOpts = options + if (openerResult) { + throw openerResult + } + } + + const mock = await mockNpm(t, { config }) + + const openUrl = tmock(t, '{LIB}/utils/open-url.js', { + '@npmcli/promise-spawn': { open }, + }) -let openerUrl = null -let openerOpts = null -let openerResult = null + const openWithNpm = (...a) => openUrl(mock.npm, ...a) -const open = async (url, options) => { - openerUrl = url - openerOpts = options - if (openerResult) { - throw openerResult + if (args) { + await openWithNpm(...args) } -} -const openUrl = tmock(t, '{LIB}/utils/open-url.js', { - '@npmcli/promise-spawn': { - open, - }, -}) + return { + ...mock, + openUrl: openWithNpm, + openerUrl: () => openerUrl, + openerOpts: () => openerOpts, + } +} t.test('opens a url', async t => { - t.teardown(() => { - openerUrl = null - openerOpts = null - OUTPUT.length = 0 - }) - await openUrl(npm, 'https://www.npmjs.com', 'npm home') - t.equal(openerUrl, 'https://www.npmjs.com', 'opened the given url') - t.same(openerOpts, { command: null }, 'passed command as null (the default)') - t.same(OUTPUT, [], 'printed no output') + const { openerUrl, openerOpts, joinedOutput } = await mockOpenUrl(t, + ['https://www.npmjs.com', 'npm home']) + t.equal(openerUrl(), 'https://www.npmjs.com', 'opened the given url') + t.same(openerOpts(), { command: null }, 'passed command as null (the default)') + t.same(joinedOutput(), '', 'printed no output') }) t.test('returns error for non-https url', async t => { - t.teardown(() => { - openerUrl = null - openerOpts = null - OUTPUT.length = 0 - }) + const { openUrl, openerUrl, openerOpts, joinedOutput } = await mockOpenUrl(t) await t.rejects( - openUrl(npm, 'ftp://www.npmjs.com', 'npm home'), + openUrl('ftp://www.npmjs.com', 'npm home'), /Invalid URL/, 'got the correct error' ) - t.equal(openerUrl, null, 'did not open') - t.same(openerOpts, null, 'did not open') - t.same(OUTPUT, [], 'printed no output') + t.equal(openerUrl(), null, 'did not open') + t.same(openerOpts(), null, 'did not open') + t.same(joinedOutput(), '', 'printed no output') }) t.test('returns error for file url', async t => { - t.teardown(() => { - openerUrl = null - openerOpts = null - OUTPUT.length = 0 - }) + const { openUrl, openerUrl, openerOpts, joinedOutput } = await mockOpenUrl(t) await t.rejects( - openUrl(npm, 'file:///usr/local/bin/ls', 'npm home'), + openUrl('file:///usr/local/bin/ls', 'npm home'), /Invalid URL/, 'got the correct error' ) - t.equal(openerUrl, null, 'did not open') - t.same(openerOpts, null, 'did not open') - t.same(OUTPUT, [], 'printed no output') + t.equal(openerUrl(), null, 'did not open') + t.same(openerOpts(), null, 'did not open') + t.same(joinedOutput(), '', 'printed no output') }) t.test('file url allowed if explicitly asked for', async t => { - t.teardown(() => { - openerUrl = null - openerOpts = null - OUTPUT.length = 0 - }) - await openUrl(npm, 'file:///man/page/npm-install', 'npm home', true) - t.equal(openerUrl, 'file:///man/page/npm-install', 'opened the given url') - t.same(openerOpts, { command: null }, 'passed command as null (the default)') - t.same(OUTPUT, [], 'printed no output') + const { openerUrl, openerOpts, joinedOutput } = await mockOpenUrl(t, + ['file:///man/page/npm-install', 'npm home', true]) + t.equal(openerUrl(), 'file:///man/page/npm-install', 'opened the given url') + t.same(openerOpts(), { command: null }, 'passed command as null (the default)') + t.same(joinedOutput(), '', 'printed no output') }) t.test('returns error for non-parseable url', async t => { - t.teardown(() => { - openerUrl = null - openerOpts = null - OUTPUT.length = 0 - }) + const { openUrl, openerUrl, openerOpts, joinedOutput } = await mockOpenUrl(t) await t.rejects( - openUrl(npm, 'git+ssh://user@host:repo.git', 'npm home'), + openUrl('git+ssh://user@host:repo.git', 'npm home'), /Invalid URL/, 'got the correct error' ) - t.equal(openerUrl, null, 'did not open') - t.same(openerOpts, null, 'did not open') - t.same(OUTPUT, [], 'printed no output') + t.equal(openerUrl(), null, 'did not open') + t.same(openerOpts(), null, 'did not open') + t.same(joinedOutput(), '', 'printed no output') }) t.test('encodes non-URL-safe characters in url provided', async t => { - t.teardown(() => { - openerUrl = null - openerOpts = null - OUTPUT.length = 0 - }) - await openUrl(npm, 'https://www.npmjs.com/|cat', 'npm home') - t.equal(openerUrl, 'https://www.npmjs.com/%7Ccat', 'opened the encoded url') - t.same(openerOpts, { command: null }, 'passed command as null (the default)') - t.same(OUTPUT, [], 'printed no output') + const { openerUrl, openerOpts, joinedOutput } = await mockOpenUrl(t, + ['https://www.npmjs.com/|cat', 'npm home']) + t.equal(openerUrl(), 'https://www.npmjs.com/%7Ccat', 'opened the encoded url') + t.same(openerOpts(), { command: null }, 'passed command as null (the default)') + t.same(joinedOutput(), '', 'printed no output') }) t.test('opens a url with the given browser', async t => { - npm.config.set('browser', 'chrome') - t.teardown(() => { - openerUrl = null - openerOpts = null - OUTPUT.length = 0 - npm.config.set('browser', true) - }) - await openUrl(npm, 'https://www.npmjs.com', 'npm home') - t.equal(openerUrl, 'https://www.npmjs.com', 'opened the given url') - t.same(openerOpts, { command: 'chrome' }, 'passed the given browser as command') - t.same(OUTPUT, [], 'printed no output') + const { openerUrl, openerOpts, joinedOutput } = await mockOpenUrl(t, + ['https://www.npmjs.com', 'npm home'], { browser: 'chrome' }) + t.equal(openerUrl(), 'https://www.npmjs.com', 'opened the given url') + t.same(openerOpts(), { command: 'chrome' }, 'passed the given browser as command') + t.same(joinedOutput(), '', 'printed no output') }) t.test('prints where to go when browser is disabled', async t => { - npm.config.set('browser', false) - t.teardown(() => { - openerUrl = null - openerOpts = null - OUTPUT.length = 0 - npm.config.set('browser', true) - }) - await openUrl(npm, 'https://www.npmjs.com', 'npm home') - t.equal(openerUrl, null, 'did not open') - t.same(openerOpts, null, 'did not open') - t.equal(OUTPUT.length, 1, 'got one logged message') - t.equal(OUTPUT[0].length, 1, 'logged message had one value') - t.matchSnapshot(OUTPUT[0][0], 'printed expected message') + const { openerUrl, openerOpts, joinedOutput } = await mockOpenUrl(t, + ['https://www.npmjs.com', 'npm home'], { browser: false }) + t.equal(openerUrl(), null, 'did not open') + t.same(openerOpts(), null, 'did not open') + t.matchSnapshot(joinedOutput(), 'printed expected message') }) t.test('prints where to go when browser is disabled and json is enabled', async t => { - npm.config.set('browser', false) - npm.config.set('json', true) - t.teardown(() => { - openerUrl = null - openerOpts = null - OUTPUT.length = 0 - npm.config.set('browser', true) - npm.config.set('json', false) - }) - await openUrl(npm, 'https://www.npmjs.com', 'npm home') - t.equal(openerUrl, null, 'did not open') - t.same(openerOpts, null, 'did not open') - t.equal(OUTPUT.length, 1, 'got one logged message') - t.equal(OUTPUT[0].length, 1, 'logged message had one value') - t.matchSnapshot(OUTPUT[0][0], 'printed expected message') + const { openerUrl, openerOpts, joinedOutput } = await mockOpenUrl(t, + ['https://www.npmjs.com', 'npm home'], { browser: false, json: true }) + t.equal(openerUrl(), null, 'did not open') + t.same(openerOpts(), null, 'did not open') + t.matchSnapshot(joinedOutput(), 'printed expected message') }) t.test('prints where to go when given browser does not exist', async t => { - npm.config.set('browser', 'firefox') - openerResult = Object.assign(new Error('failed'), { code: 'ENOENT' }) - t.teardown(() => { - openerUrl = null - openerOpts = null - OUTPUT.length = 0 - npm.config.set('browser', true) - }) - await openUrl(npm, 'https://www.npmjs.com', 'npm home') - t.equal(openerUrl, 'https://www.npmjs.com', 'tried to open the correct url') - t.same(openerOpts, { command: 'firefox' }, 'tried to use the correct browser') - t.equal(OUTPUT.length, 1, 'got one logged message') - t.equal(OUTPUT[0].length, 1, 'logged message had one value') - t.matchSnapshot(OUTPUT[0][0], 'printed expected message') + const { openerUrl, openerOpts, joinedOutput } = await mockOpenUrl(t, + ['https://www.npmjs.com', 'npm home'], + { + browser: 'firefox', + openerResult: Object.assign(new Error('failed'), { code: 'ENOENT' }), + } + ) + + t.equal(openerUrl(), 'https://www.npmjs.com', 'tried to open the correct url') + t.same(openerOpts(), { command: 'firefox' }, 'tried to use the correct browser') + t.matchSnapshot(joinedOutput(), 'printed expected message') }) t.test('handles unknown opener error', async t => { - npm.config.set('browser', 'firefox') - openerResult = Object.assign(new Error('failed'), { code: 'ENOBRIAN' }) - t.teardown(() => { - openerUrl = null - openerOpts = null - OUTPUT.length = 0 - npm.config.set('browser', true) + const { openUrl } = await mockOpenUrl(t, null, { + browser: 'firefox', + openerResult: Object.assign(new Error('failed'), { code: 'ENOBRIAN' }), }) - t.rejects(openUrl(npm, 'https://www.npmjs.com', 'npm home'), 'failed', 'got the correct error') + + t.rejects(openUrl('https://www.npmjs.com', 'npm home'), 'failed', 'got the correct error') }) diff --git a/workspaces/config/lib/type-defs.js b/workspaces/config/lib/type-defs.js index 20a827c3d164e..2efa941e48488 100644 --- a/workspaces/config/lib/type-defs.js +++ b/workspaces/config/lib/type-defs.js @@ -19,6 +19,18 @@ const validatePath = (data, k, val) => { return noptValidatePath(data, k, val) } +class BooleanOrString {} + +const validateBooleanOrString = (data, k, val) => { + if (typeof val === 'boolean' || val === 'true' || val === 'false') { + return nopt.typeDefs.Boolean.validate(data, k, val) + } + if (typeof val === 'string') { + return nopt.typeDefs.String.validate(data, k, val) + } + return false +} + // add descriptions so we can validate more usefully module.exports = { ...nopt.typeDefs, @@ -53,6 +65,11 @@ module.exports = { ...nopt.typeDefs.Date, description: 'valid Date string', }, + BooleanOrString: { + type: BooleanOrString, + validate: validateBooleanOrString, + description: 'boolean value (true or false) or a string', + }, } // TODO: make nopt less of a global beast so this kludge isn't necessary From 32c3b62977f230f2e086c6f1e2df5b3a5a0c94ac Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Thu, 5 Jan 2023 18:25:08 -0700 Subject: [PATCH 2/9] wip --- DEPENDENCIES.md | 1 + lib/base-command.js | 4 +- lib/cli.js | 6 +- lib/commands/doctor.js | 4 +- lib/npm.js | 82 +- lib/utils/config/definition.js | 208 ++- lib/utils/config/definitions.js | 416 +++--- lib/utils/config/index.js | 5 +- lib/utils/exit-handler.js | 12 +- lib/utils/update-notifier.js | 27 +- lib/utils/wrap.js | 21 + node_modules/nopt/lib/debug.js | 6 + node_modules/nopt/lib/nopt-lib.js | 428 ++++++ node_modules/nopt/lib/nopt.js | 527 +------ node_modules/nopt/lib/type-defs.js | 91 ++ node_modules/nopt/package.json | 11 +- package-lock.json | 12 +- package.json | 2 +- tap-snapshots/test/lib/docs.js.test.cjs | 2 +- test/fixtures/mock-npm.js | 36 +- test/lib/utils/completion/installed-deep.js | 89 +- .../lib/utils/completion/installed-shallow.js | 38 +- test/lib/utils/config/definitions.js | 2 +- test/lib/utils/config/index.js | 2 +- test/lib/utils/update-notifier.js | 193 ++- workspaces/config/lib/config-data.js | 245 ++++ workspaces/config/lib/config-types.js | 147 ++ workspaces/config/lib/credentials.js | 140 ++ workspaces/config/lib/errors.js | 9 +- workspaces/config/lib/index.js | 1215 +++++++---------- workspaces/config/lib/parse-field.js | 81 -- workspaces/config/lib/set-envs.js | 117 +- workspaces/config/lib/type-defs.js | 193 ++- workspaces/config/lib/type-description.js | 21 - workspaces/config/lib/umask.js | 31 - workspaces/config/package.json | 5 +- 36 files changed, 2278 insertions(+), 2151 deletions(-) create mode 100644 lib/utils/wrap.js create mode 100644 node_modules/nopt/lib/debug.js create mode 100644 node_modules/nopt/lib/nopt-lib.js create mode 100644 node_modules/nopt/lib/type-defs.js create mode 100644 workspaces/config/lib/config-data.js create mode 100644 workspaces/config/lib/config-types.js create mode 100644 workspaces/config/lib/credentials.js delete mode 100644 workspaces/config/lib/parse-field.js delete mode 100644 workspaces/config/lib/type-description.js delete mode 100644 workspaces/config/lib/umask.js diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 69b72b640df87..2fa6cce350722 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -631,6 +631,7 @@ graph LR; npmcli-config-->semver; npmcli-config-->tap; npmcli-config-->walk-up-path; + npmcli-config-->which; npmcli-disparity-colors-->ansi-styles; npmcli-docs-->front-matter; npmcli-docs-->ignore-walk; diff --git a/lib/base-command.js b/lib/base-command.js index 0adff8e5d95ea..3b4f7e760fdad 100644 --- a/lib/base-command.js +++ b/lib/base-command.js @@ -2,7 +2,7 @@ const { relative } = require('path') -const ConfigDefinitions = require('./utils/config/definitions.js') +const { definitions } = require('./utils/config/definitions.js') const getWorkspaces = require('./workspaces/get-workspaces.js') const cmdAliases = require('./utils/cmd-list').aliases @@ -83,7 +83,7 @@ class BaseCommand { let line = '' for (const param of this.params) { - const usage = `[${ConfigDefinitions[param].usage}]` + const usage = `[${definitions[param].usage}]` if (line.length && line.length + usage.length > this.wrapWidth) { results = [results, line].filter(Boolean).join('\n') line = '' diff --git a/lib/cli.js b/lib/cli.js index 007778aa4b986..f05c74a08a9a1 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -79,8 +79,12 @@ module.exports = async process => { const satisfies = require('semver/functions/satisfies') const exitHandler = require('./utils/exit-handler.js') + const { dirname } = require('path') const Npm = require('./npm.js') - const npm = new Npm() + const npm = new Npm({ + npmRoot: dirname(__dirname), + argv: process.argv, + }) exitHandler.setNpm(npm) // only log node and npm paths in argv initially since argv can contain diff --git a/lib/commands/doctor.js b/lib/commands/doctor.js index 19262e537dbe0..89aaed2f81d21 100644 --- a/lib/commands/doctor.js +++ b/lib/commands/doctor.js @@ -9,13 +9,11 @@ const semver = require('semver') const { promisify } = require('util') const log = require('../utils/log-shim.js') const ping = require('../utils/ping.js') -const { - registry: { default: defaultRegistry }, -} = require('../utils/config/definitions.js') const lstat = promisify(fs.lstat) const readdir = promisify(fs.readdir) const access = promisify(fs.access) const { R_OK, W_OK, X_OK } = fs.constants +const { registry: defaultRegistry } = require('../utils/config').defaults const maskLabel = mask => { const label = [] diff --git a/lib/npm.js b/lib/npm.js index 841d145ddcbad..d82a283e2da0c 100644 --- a/lib/npm.js +++ b/lib/npm.js @@ -1,15 +1,13 @@ const Arborist = require('@npmcli/arborist') -const EventEmitter = require('events') -const { resolve, dirname, join } = require('path') +const { resolve, join } = require('path') const Config = require('@npmcli/config') const chalk = require('chalk') -const which = require('which') const fs = require('fs/promises') // Patch the global fs module here at the app level require('graceful-fs').gracefulify(require('fs')) -const { definitions, flatten, shorthands } = require('./utils/config/index.js') +const { definitions, flatten, shorthands, derived } = require('./utils/config/index.js') const usage = require('./utils/npm-usage.js') const LogFile = require('./utils/log-file.js') const Timers = require('./utils/timers.js') @@ -20,13 +18,12 @@ const updateNotifier = require('./utils/update-notifier.js') const pkg = require('../package.json') const cmdList = require('./utils/cmd-list.js') -class Npm extends EventEmitter { +class Npm { static get version () { return pkg.version } updateNotification = null - loadErr = null argv = [] #command = null @@ -51,28 +48,16 @@ class Npm extends EventEmitter { }, }) - // all these options are only used by tests in order to make testing more - // closely resemble real world usage. for now, npm has no programmatic API so - // it is ok to add stuff here, but we should not rely on it more than - // necessary. XXX: make these options not necessary by refactoring @npmcli/config - // - npmRoot: this is where npm looks for docs files and the builtin config - // - argv: this allows tests to extend argv in the same way the argv would - // be passed in via a CLI arg. - // - excludeNpmCwd: this is a hack to get @npmcli/config to stop walking up - // dirs to set a local prefix when it encounters the `npmRoot`. this - // allows tests created by tap inside this repo to not set the local - // prefix to `npmRoot` since that is the first dir it would encounter when - // doing implicit detection - constructor ({ npmRoot = dirname(__dirname), argv = [], excludeNpmCwd = false } = {}) { - super() + constructor ({ npmRoot, argv, cwdRoot }) { this.#npmRoot = npmRoot this.config = new Config({ npmPath: this.#npmRoot, + derived, definitions, flatten, shorthands, - argv: [...process.argv, ...argv], - excludeNpmCwd, + argv, + cwdRoot, }) } @@ -151,15 +136,12 @@ class Npm extends EventEmitter { } } - return command.cmdExec(args).finally(timeEnd) + await command.cmdExec(args).finally(timeEnd) } async load () { if (!this.#loadPromise) { - this.#loadPromise = this.time('npm:load', () => this.#load().catch((er) => { - this.loadErr = er - throw er - })) + this.#loadPromise = this.time('npm:load', () => this.#load()) } return this.#loadPromise } @@ -200,16 +182,6 @@ class Npm extends EventEmitter { } async #load () { - await this.time('npm:load:whichnode', async () => { - // TODO should we throw here? - const node = await which(process.argv[0]).catch(() => {}) - if (node && node.toUpperCase() !== process.execPath.toUpperCase()) { - log.verbose('node symlink', node) - process.execPath = node - this.config.execPath = node - } - }) - await this.time('npm:load:configload', () => this.config.load()) // mkdir this separately since the logs dir can be set to @@ -255,6 +227,7 @@ class Npm extends EventEmitter { unicode: this.config.get('unicode'), heading: this.config.get('heading'), }) + // TODO: move to config process.env.COLOR = this.color ? '1' : '0' }) @@ -329,7 +302,7 @@ class Npm extends EventEmitter { } get global () { - return this.config.get('global') || this.config.get('location') === 'global' + return this.config.global } get silent () { @@ -376,34 +349,20 @@ class Npm extends EventEmitter { return this.config.get('cache') } - set cache (r) { - this.config.set('cache', r) - } - get globalPrefix () { return this.config.globalPrefix } - set globalPrefix (r) { - this.config.globalPrefix = r - } - get localPrefix () { return this.config.localPrefix } - set localPrefix (r) { - this.config.localPrefix = r - } - get localPackage () { return this.config.localPackage } get globalDir () { - return process.platform !== 'win32' - ? resolve(this.globalPrefix, 'lib', 'node_modules') - : resolve(this.globalPrefix, 'node_modules') + return resolve(this.globalPrefix, process.platform !== 'win32' ? 'lib' : '', 'node_modules') } get localDir () { @@ -415,8 +374,7 @@ class Npm extends EventEmitter { } get globalBin () { - const b = this.globalPrefix - return process.platform !== 'win32' ? resolve(b, 'bin') : b + return resolve(this.globalPrefix, process.platform !== 'win32' ? 'bin' : '') } get localBin () { @@ -431,24 +389,10 @@ class Npm extends EventEmitter { return this.global ? this.globalPrefix : this.localPrefix } - set prefix (r) { - const k = this.global ? 'globalPrefix' : 'localPrefix' - this[k] = r - } - get usage () { return usage(this) } - // XXX add logging to see if we actually use this - get tmp () { - if (!this.#tmpFolder) { - const rand = require('crypto').randomBytes(4).toString('hex') - this.#tmpFolder = `npm-${process.pid}-${rand}` - } - return resolve(this.config.get('tmp'), this.#tmpFolder) - } - // output to stdout in a progress bar compatible way output (...msg) { log.clearProgress() diff --git a/lib/utils/config/definition.js b/lib/utils/config/definition.js index f88d8334cf01f..52d81a5489113 100644 --- a/lib/utils/config/definition.js +++ b/lib/utils/config/definition.js @@ -6,7 +6,10 @@ // say "these are for registry access", "these are for // version resolution" etc. -const required = ['type', 'description', 'default', 'key'] +const { TypeDefs } = require('@npmcli/config') +const { wrap, unindent } = require('../wrap') + +const required = ['type', 'description', 'default'] const allowed = [ 'default', @@ -15,81 +18,106 @@ const allowed = [ 'description', 'flatten', 'hint', - 'key', 'short', 'type', - 'typeDescription', 'usage', 'envExport', ] -const { - typeDefs: { - semver: { type: semver }, - Umask: { type: Umask }, - url: { type: url }, - path: { type: path }, - }, -} = require('@npmcli/config') - class Definition { + #key = null + #def = null + constructor (key, def) { - this.key = key - // if it's set falsey, don't export it, otherwise we do by default - this.envExport = true - Object.assign(this, def) + this.#key = key + this.#def = def this.validate() - if (!this.defaultDescription) { - this.defaultDescription = describeValue(this.default) - } - if (!this.typeDescription) { - this.typeDescription = describeType(this.type) - } + } + + get default () { + return this.#def.default + } + + get type () { + return this.#def.type + } + + get deprecated () { + return this.#def.deprecated + } + + get envExport () { + // if it's set falsey, don't export it, otherwise we do by default + return this.#def.envExport ?? true + } + + get flatten () { + return this.#def.flatten + } + + get derived () { + return this.#def.derived + } + + // get type () { + // return Array.isArray(this.#def.type) + // ? this.#def.type.map((t) => TypeDefs.getType(t) ?? t) + // : TypeDefs.getType(this.#def.type) ?? this.#def.type + // } + + get hint () { // hint is only used for non-boolean values - if (!this.hint) { - if (this.type === Number) { - this.hint = '' - } else { - this.hint = `<${this.key}>` - } - } - if (!this.usage) { - this.usage = describeUsage(this) - } + return this.#def.hint ?? `<${TypeDefs.isNumber(this.#def.type) ? 'number' : this.#key}>` + } + + get usage () { + return this.#def.usage ?? describeUsage({ ...this.#def, type: this.type }) + } + + get defaultDescription () { + return this.#def.defaultDescription ?? describeValue(this.#def.default) + } + + get typeDescription () { + return describeTypes(this.type) } validate () { for (const req of required) { - if (!Object.prototype.hasOwnProperty.call(this, req)) { - throw new Error(`config lacks ${req}: ${this.key}`) + if (!Object.prototype.hasOwnProperty.call(this.#def, req)) { + throw new Error(`config \`${this.#key}\` lacks key \`${req}\``) } } - if (!this.key) { - throw new Error(`config lacks key: ${this.key}`) + if (!this.#key) { + throw new Error(`config lacks key: ${this.#key}`) } - for (const field of Object.keys(this)) { + for (const field of Object.keys(this.#def)) { if (!allowed.includes(field)) { - throw new Error(`config defines unknown field ${field}: ${this.key}`) + throw new Error(`config defines unknown field ${field}: ${this.#key}`) } } } // a textual description of this config, suitable for help output describe () { - const description = unindent(this.description) const noEnvExport = this.envExport ? '' - : ` -This value is not exported to the environment for child processes. -` - const deprecated = !this.deprecated ? '' : `* DEPRECATED: ${unindent(this.deprecated)}\n` - return wrapAll(`#### \`${this.key}\` - -* Default: ${unindent(this.defaultDescription)} -* Type: ${unindent(this.typeDescription)} -${deprecated} -${description} -${noEnvExport}`) + : 'This value is not exported to the environment for child processes.' + + const sections = [ + ['Default', this.defaultDescription], + ['Type', this.typeDescription], + this.deprecated ? ['DEPRECATED', this.deprecated] : null, + this.description, + noEnvExport, + ].filter(Boolean).map((s) => { + if (Array.isArray(s)) { + return `* ${s[0]}: ${unindent(s[1])}` + } + return unindent(s) + }) + + return wrapAll(`#### \`${this.key}\`\n\n${sections.join('\n')}`) } } @@ -102,13 +130,13 @@ const describeUsage = def => { key = `-${def.short}|` } - if (def.type === Boolean && def.default !== false) { + if (TypeDefs.isBoolean(def.type) && def.default !== false) { key = `${key}--no-${def.key}` } else { key = `${key}--${def.key}` } - if (def.type !== Boolean) { + if (!TypeDefs.isBoolean(def.type)) { key = `${key} ${def.hint}` } @@ -123,11 +151,11 @@ const describeUsage = def => { // Multiple types let types = def.type const multiple = types.includes(Array) - const bool = types.includes(Boolean) + const bool = TypeDefs.isBoolean(types) // null type means optional and doesn't currently affect usage output since // all non-optional params have defaults so we render everything as optional - types = types.filter(t => t !== null && t !== Array && t !== Boolean) + types = types.filter(t => t !== null && t !== Array && !TypeDefs.isBoolean(t)) if (!types.length) { return key @@ -157,68 +185,26 @@ const describeUsage = def => { } } -const describeType = type => { - if (Array.isArray(type)) { - const descriptions = type.filter(t => t !== Array).map(t => describeType(t)) - - // [a] => "a" - // [a, b] => "a or b" - // [a, b, c] => "a, b, or c" - // [a, Array] => "a (can be set multiple times)" - // [a, Array, b] => "a or b (can be set multiple times)" - const last = descriptions.length > 1 ? [descriptions.pop()] : [] - const oxford = descriptions.length > 1 ? ', or ' : ' or ' - const words = [descriptions.join(', ')].concat(last).join(oxford) - const multiple = type.includes(Array) ? ' (can be set multiple times)' : '' - return `${words}${multiple}` - } - - // Note: these are not quite the same as the description printed - // when validation fails. In that case, we want to give the user - // a bit more information to help them figure out what's wrong. - switch (type) { - case String: - return 'String' - case Number: - return 'Number' - case Umask: - return 'Octal numeric string in range 0000..0777 (0..511)' - case Boolean: - return 'Boolean' - case Date: - return 'Date' - case path: - return 'Path' - case semver: - return 'SemVer string' - case url: - return 'URL' - default: - return describeValue(type) - } +const describeTypes = types => { + const descriptions = [].concat(types).filter(t => t !== Array).map(t => describeType(t)) + + // [a] => "a" + // [a, b] => "a or b" + // [a, b, c] => "a, b, or c" + // [a, Array] => "a (can be set multiple times)" + // [a, Array, b] => "a or b (can be set multiple times)" + const last = descriptions.length > 1 ? [descriptions.pop()] : [] + const oxford = descriptions.length > 1 ? ', or ' : ' or ' + const words = [descriptions.join(', ')].concat(last).join(oxford) + const multiple = types.includes(Array) ? ' (can be set multiple times)' : '' + return `${words}${multiple}` } +const describeType = (type) => type.typeDescription || describeValue(type) + // if it's a string, quote it. otherwise, just cast to string. const describeValue = val => (typeof val === 'string' ? JSON.stringify(val) : String(val)) -const unindent = s => { - // get the first \n followed by a bunch of spaces, and pluck off - // that many spaces from the start of every line. - const match = s.match(/\n +/) - return !match ? s.trim() : s.split(match[0]).join('\n').trim() -} - -const wrap = s => { - const cols = Math.min(Math.max(20, process.stdout.columns) || 80, 80) - 5 - return unindent(s) - .split(/[ \n]+/) - .reduce((left, right) => { - const last = left.split('\n').pop() - const join = last.length && last.length + right.length > cols ? '\n' : ' ' - return left + join + right - }) -} - const wrapAll = s => { let inCodeBlock = false return s diff --git a/lib/utils/config/definitions.js b/lib/utils/config/definitions.js index 01c06476f0b60..3a62c1c1eb964 100644 --- a/lib/utils/config/definitions.js +++ b/lib/utils/config/definitions.js @@ -1,16 +1,33 @@ -const definitions = {} -module.exports = definitions - const Definition = require('./definition.js') - const { version: npmVersion } = require('../../../package.json') const ciInfo = require('ci-info') const querystring = require('querystring') const { isWindows } = require('../is-windows.js') const { join } = require('path') +const { tmpdir } = require('os') +const fs = require('fs') + +const { Types } = require('@npmcli/config') + +const editor = process.env.EDITOR || process.env.VISUAL || + (isWindows ? `${process.env.SYSTEMROOT}\\notepad.exe` : 'vi') + +const shell = isWindows + ? process.env.ComSpec || 'cmd' + : process.env.SHELL || 'sh' + +const unicode = /UTF-?8$/i.test( + process.env.LC_ALL || + process.env.LC_CTYPE || + process.env.LANG +) + +// use LOCALAPPDATA on Windows, if set https://github.com/npm/cli/pull/899 +const cacheRoot = (isWindows && process.env.LOCALAPPDATA) || '~' +const cacheExtra = isWindows ? 'npm-cache' : '.npm' +const cache = `${cacheRoot}/${cacheExtra}` // used by cafile flattening to flatOptions.ca -const fs = require('fs') const maybeReadFile = file => { try { return fs.readFileSync(file, 'utf8') @@ -22,85 +39,36 @@ const maybeReadFile = file => { } } -const buildOmitList = obj => { - const include = obj.include || [] - const omit = obj.omit || [] - - const only = obj.only - if (/^prod(uction)?$/.test(only) || obj.production) { +const buildOmitList = ({ include = [], omit = [], only, production, also, dev, optional }) => { + if (/^prod(uction)?$/.test(only) || production) { omit.push('dev') - } else if (obj.production === false) { - include.push('dev') - } - - if (/^dev/.test(obj.also)) { + } else if (production === false) { include.push('dev') } - if (obj.dev) { + if (/^dev/.test(also) || dev) { include.push('dev') } - if (obj.optional === false) { + if (optional === false) { omit.push('optional') - } else if (obj.optional === true) { + } else if (optional === true) { include.push('optional') } - obj.omit = [...new Set(omit)].filter(type => !include.includes(type)) - obj.include = [...new Set(include)] + omit = [...new Set(omit)].filter(type => !include.includes(type)) + include = [...new Set(include)] - if (obj.omit.includes('dev')) { + if (omit.includes('dev')) { process.env.NODE_ENV = 'production' } - return obj.omit -} - -const editor = process.env.EDITOR || - process.env.VISUAL || - (isWindows ? `${process.env.SYSTEMROOT}\\notepad.exe` : 'vi') - -const shell = isWindows ? process.env.ComSpec || 'cmd' - : process.env.SHELL || 'sh' - -const { tmpdir, networkInterfaces } = require('os') -const getLocalAddresses = () => { - try { - return Object.values(networkInterfaces()).map( - int => int.map(({ address }) => address) - ).reduce((set, addrs) => set.concat(addrs), [null]) - } catch (e) { - return [null] - } + return omit } -const unicode = /UTF-?8$/i.test( - process.env.LC_ALL || - process.env.LC_CTYPE || - process.env.LANG -) - -// use LOCALAPPDATA on Windows, if set -// https://github.com/npm/cli/pull/899 -const cacheRoot = (isWindows && process.env.LOCALAPPDATA) || '~' -const cacheExtra = isWindows ? 'npm-cache' : '.npm' -const cache = `${cacheRoot}/${cacheExtra}` - -const Config = require('@npmcli/config') - -// TODO: refactor these type definitions so that they are less -// weird to pull out of the config module. -// TODO: use better type definition/validation API, nopt's is so weird. -const { - typeDefs: { - semver: { type: semver }, - Umask: { type: Umask }, - url: { type: url }, - path: { type: path }, - BooleanOrString: { type: BooleanOrString }, - }, -} = Config +const definitions = {} +const derived = {} +module.exports = { definitions, derived } const define = (key, def) => { /* istanbul ignore if - this should never happen, prevents mistakes below */ @@ -116,6 +84,19 @@ const flatten = (key, obj, flatOptions) => { flatOptions[camel] = obj[key] } +const derive = (key, def) => { + /* istanbul ignore if - this should never happen, prevents mistakes below */ + if (derived[key]) { + throw new Error(`defining key more than once: ${key}`) + } + derived[key] = def +} + +derive('omit', (...args) => { + console.log(args) + throw new Error('x') +}) + // TODO: // Instead of having each definition provide a flatten method, // provide the (?list of?) flat option field(s?) that it impacts. @@ -144,7 +125,7 @@ const flatten = (key, obj, flatOptions) => { define('_auth', { default: null, - type: [null, String], + type: [null, Types.String], description: ` A basic-auth string to use when authenticating against the npm registry. This will ONLY be used to authenticate against the npm registry. For other @@ -179,7 +160,7 @@ define('access', { define('all', { default: false, - type: Boolean, + type: Types.Boolean, short: 'a', description: ` When running \`npm outdated\` and \`npm ls\`, setting \`--all\` will show @@ -191,7 +172,7 @@ define('all', { define('allow-same-version', { default: false, - type: Boolean, + type: Types.Boolean, description: ` Prevents throwing an error when \`npm version\` is used to set the new version to the same value as the current version. @@ -207,6 +188,7 @@ define('also', { \`--include=dev\`. `, deprecated: 'Please use --include=dev instead.', + derived: ['omit'], flatten (key, obj, flatOptions) { definitions.omit.flatten('omit', obj, flatOptions) }, @@ -214,7 +196,7 @@ define('also', { define('audit', { default: true, - type: Boolean, + type: Types.Boolean, description: ` When "true" submit audit reports alongside the current npm command to the default registry and all registries configured for scopes. See the @@ -245,7 +227,7 @@ define('auth-type', { define('before', { default: null, - type: [null, Date], + type: [null, Types.Date], description: ` If passed to \`npm install\`, will rebuild the npm tree such that only versions that were available **on or before** the \`--before\` time get @@ -262,7 +244,7 @@ define('before', { define('bin-links', { default: true, - type: Boolean, + type: Types.Boolean, description: ` Tells npm to create symlinks (or \`.cmd\` shims on Windows) for package executables. @@ -279,11 +261,11 @@ define('browser', { defaultDescription: ` OS X: \`"open"\`, Windows: \`"start"\`, Others: \`"xdg-open"\` `, - type: BooleanOrString, + type: [Types.BooleanOrString, Types.Boolean], description: ` The browser that is called by npm commands to open websites. - Set to \`false\` to suppress browser behavior and instead print urls to + Set to \`false\` to suppress browser behavior and instead print Types.urls to terminal. Set to \`true\` to use default system URL opener. @@ -293,7 +275,7 @@ define('browser', { define('ca', { default: null, - type: [null, String, Array], + type: [null, Types.String, Types.Array], description: ` The Certificate Authority signing certificate that is trusted for SSL connections to the registry. Values should be in PEM format (Windows @@ -324,7 +306,7 @@ define('cache', { defaultDescription: ` Windows: \`%LocalAppData%\\npm-cache\`, Posix: \`~/.npm\` `, - type: path, + type: Types.path, description: ` The location of npm's cache directory. `, @@ -336,7 +318,7 @@ define('cache', { define('cache-max', { default: Infinity, - type: Number, + type: Types.Number, description: ` \`--cache-max=0\` is an alias for \`--prefer-online\` `, @@ -352,7 +334,7 @@ define('cache-max', { define('cache-min', { default: 0, - type: Number, + type: Types.Number, description: ` \`--cache-min=9999 (or bigger)\` is an alias for \`--prefer-offline\`. `, @@ -368,9 +350,9 @@ define('cache-min', { define('cafile', { default: null, - type: path, + type: [null, Types.path], description: ` - A path to a file containing one or multiple Certificate Authority signing + A Types.path to a file containing one or multiple Certificate Authority signing certificates. Similar to the \`ca\` setting, but allows for multiple CA's, as well as for the CA information to be stored in a file on disk. `, @@ -394,7 +376,7 @@ define('cafile', { define('call', { default: '', - type: String, + type: Types.String, short: 'c', description: ` Optional companion option for \`npm exec\`, \`npx\` that allows for @@ -409,7 +391,7 @@ define('call', { define('cert', { default: null, - type: [null, String], + type: [null, Types.String], description: ` A client certificate to pass when accessing the registry. Values should be in PEM format (Windows calls it "Base-64 encoded X.509 (.CER)") with @@ -419,8 +401,8 @@ define('cert', { cert="-----BEGIN CERTIFICATE-----\\nXXXX\\nXXXX\\n-----END CERTIFICATE-----" \`\`\` - It is _not_ the path to a certificate file, though you can set a registry-scoped - "certfile" path like "//other-registry.tld/:certfile=/path/to/cert.pem". + It is _not_ the Types.path to a certificate file, though you can set a registry-scoped + "certfile" Types.path like "//other-registry.tld/:certfile=/path/to/cert.pem". `, deprecated: ` \`key\` and \`cert\` are no longer used for most registry operations. @@ -438,7 +420,7 @@ define('ci-name', { The name of the current CI system, or \`null\` when not on a known CI platform. `, - type: [null, String], + type: [null, Types.String], description: ` The name of a continuous integration system. If not set explicitly, npm will detect the current CI environment using the @@ -449,7 +431,7 @@ define('ci-name', { define('cidr', { default: null, - type: [null, String, Array], + type: [null, Types.String, Types.Array], description: ` This is a list of CIDR address to be used when configuring limited access tokens with the \`npm token create\` command. @@ -466,7 +448,7 @@ define('color', { defaultDescription: ` true unless the NO_COLOR environ is set to something other than '0' `, - type: ['always', Boolean], + type: ['always', Types.Boolean], description: ` If false, never shows colors. If \`"always"\` then always shows colors. If true, then only prints color codes for tty file descriptors. @@ -483,7 +465,7 @@ define('color', { define('commit-hooks', { default: true, - type: Boolean, + type: Types.Boolean, description: ` Run git commit hooks when using the \`npm version\` command. `, @@ -495,7 +477,7 @@ define('depth', { defaultDescription: ` \`Infinity\` if \`--all\` is set, otherwise \`1\` `, - type: [null, Number], + type: [null, Types.Number], description: ` The depth to go when recursing packages for \`npm ls\`. @@ -508,7 +490,7 @@ define('depth', { define('description', { default: true, - type: Boolean, + type: Types.Boolean, usage: '--no-description', description: ` Show the description in \`npm search\` @@ -521,7 +503,7 @@ define('description', { define('dev', { default: false, - type: Boolean, + type: Types.Boolean, description: ` Alias for \`--include=dev\`. `, @@ -534,7 +516,7 @@ define('dev', { define('diff', { default: [], hint: '', - type: [String, Array], + type: [Types.String, Types.Array], description: ` Define arguments to compare in \`npm diff\`. `, @@ -543,7 +525,7 @@ define('diff', { define('diff-ignore-all-space', { default: false, - type: Boolean, + type: Types.Boolean, description: ` Ignore whitespace when comparing lines in \`npm diff\`. `, @@ -552,7 +534,7 @@ define('diff-ignore-all-space', { define('diff-name-only', { default: false, - type: Boolean, + type: Types.Boolean, description: ` Prints only filenames when using \`npm diff\`. `, @@ -561,7 +543,7 @@ define('diff-name-only', { define('diff-no-prefix', { default: false, - type: Boolean, + type: Types.Boolean, description: ` Do not show any source or destination prefix in \`npm diff\` output. @@ -574,7 +556,7 @@ define('diff-no-prefix', { define('diff-dst-prefix', { default: 'b/', hint: '', - type: String, + type: Types.String, description: ` Destination prefix to be used in \`npm diff\` output. `, @@ -584,7 +566,7 @@ define('diff-dst-prefix', { define('diff-src-prefix', { default: 'a/', hint: '', - type: String, + type: Types.String, description: ` Source prefix to be used in \`npm diff\` output. `, @@ -593,7 +575,7 @@ define('diff-src-prefix', { define('diff-text', { default: false, - type: Boolean, + type: Types.Boolean, description: ` Treat all files as text in \`npm diff\`. `, @@ -602,7 +584,7 @@ define('diff-text', { define('diff-unified', { default: 3, - type: Number, + type: Types.Number, description: ` The number of lines of context to print in \`npm diff\`. `, @@ -611,7 +593,7 @@ define('diff-unified', { define('dry-run', { default: false, - type: Boolean, + type: Types.Boolean, description: ` Indicates that you don't want npm to make any changes and that it should only report what it would have done. This can be passed into any of the @@ -631,7 +613,7 @@ define('editor', { The EDITOR or VISUAL environment variables, or '%SYSTEMROOT%\\notepad.exe' on Windows, or 'vi' on Unix systems `, - type: String, + type: Types.String, description: ` The command to run for \`npm edit\` and \`npm config edit\`. `, @@ -640,7 +622,7 @@ define('editor', { define('engine-strict', { default: false, - type: Boolean, + type: Types.Boolean, description: ` If set to true, then npm will stubbornly refuse to install (or even consider installing) any package that claims to not be compatible with @@ -653,7 +635,7 @@ define('engine-strict', { define('fetch-retries', { default: 2, - type: Number, + type: Types.Number, description: ` The "retries" config for the \`retry\` module to use when fetching packages from the registry. @@ -669,7 +651,7 @@ define('fetch-retries', { define('fetch-retry-factor', { default: 10, - type: Number, + type: Types.Number, description: ` The "factor" config for the \`retry\` module to use when fetching packages. @@ -683,7 +665,7 @@ define('fetch-retry-factor', { define('fetch-retry-maxtimeout', { default: 60000, defaultDescription: '60000 (1 minute)', - type: Number, + type: Types.Number, description: ` The "maxTimeout" config for the \`retry\` module to use when fetching packages. @@ -697,7 +679,7 @@ define('fetch-retry-maxtimeout', { define('fetch-retry-mintimeout', { default: 10000, defaultDescription: '10000 (10 seconds)', - type: Number, + type: Types.Number, description: ` The "minTimeout" config for the \`retry\` module to use when fetching packages. @@ -711,7 +693,7 @@ define('fetch-retry-mintimeout', { define('fetch-timeout', { default: 5 * 60 * 1000, defaultDescription: `${5 * 60 * 1000} (5 minutes)`, - type: Number, + type: Types.Number, description: ` The maximum amount of time to wait for HTTP requests to complete. `, @@ -722,7 +704,7 @@ define('fetch-timeout', { define('force', { default: false, - type: Boolean, + type: Types.Boolean, short: 'f', description: ` Removes various protections against unfortunate side effects, common @@ -752,7 +734,7 @@ define('force', { define('foreground-scripts', { default: false, - type: Boolean, + type: Types.Boolean, description: ` Run all build scripts (ie, \`preinstall\`, \`install\`, and \`postinstall\`) scripts for installed packages in the foreground @@ -767,7 +749,7 @@ define('foreground-scripts', { define('format-package-lock', { default: true, - type: Boolean, + type: Types.Boolean, description: ` Format \`package-lock.json\` or \`npm-shrinkwrap.json\` as a human readable file. @@ -777,7 +759,7 @@ define('format-package-lock', { define('fund', { default: true, - type: Boolean, + type: Types.Boolean, description: ` When "true" displays the message at the end of each \`npm install\` acknowledging the number of dependencies looking for funding. @@ -788,10 +770,10 @@ define('fund', { define('git', { default: 'git', - type: String, + type: Types.String, description: ` The command to use for git commands. If git is installed on the - computer, but is not in the \`PATH\`, then set this to the full path to + computer, but is not in the \`PATH\`, then set this to the full Types.path to the git binary. `, flatten, @@ -799,7 +781,7 @@ define('git', { define('git-tag-version', { default: true, - type: Boolean, + type: Types.Boolean, description: ` Tag the commit when using the \`npm version\` command. Setting this to false results in no commit being made at all. @@ -809,7 +791,7 @@ define('git-tag-version', { define('global', { default: false, - type: Boolean, + type: Types.Boolean, short: 'g', description: ` Operates in "global" mode, so that packages are installed into the @@ -832,7 +814,7 @@ define('global', { // the globalconfig has its default defined outside of this module define('globalconfig', { - type: path, + type: Types.path, default: '', defaultDescription: ` The global --prefix setting plus 'etc/npmrc'. For example, @@ -841,12 +823,13 @@ define('globalconfig', { description: ` The config file to read for global config options. `, + flatten, }) define('global-style', { default: false, - type: Boolean, + type: Types.Boolean, description: ` Only install direct dependencies in the top level \`node_modules\`, but hoist on deeper dependendencies. @@ -865,7 +848,7 @@ define('global-style', { define('heading', { default: 'npm', - type: String, + type: Types.String, description: ` The string that starts all the debugging log output. `, @@ -874,7 +857,7 @@ define('heading', { define('https-proxy', { default: null, - type: [null, url], + type: [null, Types.url], description: ` A proxy to use for outgoing https requests. If the \`HTTPS_PROXY\` or \`https_proxy\` or \`HTTP_PROXY\` or \`http_proxy\` environment variables @@ -886,7 +869,7 @@ define('https-proxy', { define('if-present', { default: false, - type: Boolean, + type: Types.Boolean, envExport: false, description: ` If true, npm will not exit with an error code when \`run-script\` is @@ -901,7 +884,7 @@ define('if-present', { define('ignore-scripts', { default: false, - type: Boolean, + type: Types.Boolean, description: ` If true, npm does not run scripts specified in package.json files. @@ -915,7 +898,7 @@ define('ignore-scripts', { define('include', { default: [], - type: [Array, 'prod', 'dev', 'optional', 'peer'], + type: [Types.Array, 'prod', 'dev', 'optional', 'peer'], description: ` Option that allows for defining which types of dependencies to install. @@ -933,7 +916,7 @@ define('include', { define('include-staged', { default: false, - type: Boolean, + type: Types.Boolean, description: ` Allow installing "staged" published packages, as defined by [npm RFC PR #92](https://github.com/npm/rfcs/pull/92). @@ -945,7 +928,7 @@ define('include-staged', { define('include-workspace-root', { default: false, - type: Boolean, + type: Types.Boolean, envExport: false, description: ` Include the workspace root when workspaces are enabled for a command. @@ -959,7 +942,7 @@ define('include-workspace-root', { define('init-author-email', { default: '', - type: String, + type: Types.String, description: ` The value \`npm init\` should use by default for the package author's email. @@ -968,7 +951,7 @@ define('init-author-email', { define('init-author-name', { default: '', - type: String, + type: Types.String, description: ` The value \`npm init\` should use by default for the package author's name. `, @@ -976,7 +959,7 @@ define('init-author-name', { define('init-author-url', { default: '', - type: ['', url], + type: ['', Types.url], description: ` The value \`npm init\` should use by default for the package author's homepage. `, @@ -984,7 +967,7 @@ define('init-author-url', { define('init-license', { default: 'ISC', - type: String, + type: Types.String, description: ` The value \`npm init\` should use by default for the package license. `, @@ -992,7 +975,7 @@ define('init-license', { define('init-module', { default: '~/.npm-init.js', - type: path, + type: Types.path, description: ` A module that will be loaded by the \`npm init\` command. See the documentation for the @@ -1003,7 +986,7 @@ define('init-module', { define('init-version', { default: '1.0.0', - type: semver, + type: Types.semver, description: ` The value that \`npm init\` should use by default for the package version number, if not already set in package.json. @@ -1014,7 +997,7 @@ define('init-version', { // They should be removed in a future npm version. define('init.author.email', { default: '', - type: String, + type: Types.String, deprecated: ` Use \`--init-author-email\` instead.`, description: ` @@ -1024,7 +1007,7 @@ define('init.author.email', { define('init.author.name', { default: '', - type: String, + type: Types.String, deprecated: ` Use \`--init-author-name\` instead. `, @@ -1035,7 +1018,7 @@ define('init.author.name', { define('init.author.url', { default: '', - type: ['', url], + type: ['', Types.url], deprecated: ` Use \`--init-author-url\` instead. `, @@ -1046,7 +1029,7 @@ define('init.author.url', { define('init.license', { default: 'ISC', - type: String, + type: Types.String, deprecated: ` Use \`--init-license\` instead. `, @@ -1057,7 +1040,7 @@ define('init.license', { define('init.module', { default: '~/.npm-init.js', - type: path, + type: Types.path, deprecated: ` Use \`--init-module\` instead. `, @@ -1068,7 +1051,7 @@ define('init.module', { define('init.version', { default: '1.0.0', - type: semver, + type: Types.semver, deprecated: ` Use \`--init-version\` instead. `, @@ -1079,7 +1062,7 @@ define('init.version', { define('install-links', { default: true, - type: Boolean, + type: Types.Boolean, description: ` When set file: protocol dependencies will be packed and installed as regular dependencies instead of creating a symlink. This option has @@ -1105,7 +1088,7 @@ define('install-strategy', { define('json', { default: false, - type: Boolean, + type: Types.Boolean, description: ` Whether or not to output JSON data, rather than the normal output. @@ -1119,7 +1102,7 @@ define('json', { define('key', { default: null, - type: [null, String], + type: [null, Types.String], description: ` A client key to pass when accessing the registry. Values should be in PEM format with newlines replaced by the string "\\n". For example: @@ -1128,8 +1111,8 @@ define('key', { key="-----BEGIN PRIVATE KEY-----\\nXXXX\\nXXXX\\n-----END PRIVATE KEY-----" \`\`\` - It is _not_ the path to a key file, though you can set a registry-scoped - "keyfile" path like "//other-registry.tld/:keyfile=/path/to/key.pem". + It is _not_ the Types.path to a key file, though you can set a registry-scoped + "keyfile" Types.path like "//other-registry.tld/:keyfile=/path/to/key.pem". `, deprecated: ` \`key\` and \`cert\` are no longer used for most registry operations. @@ -1143,7 +1126,7 @@ define('key', { define('legacy-bundling', { default: false, - type: Boolean, + type: Types.Boolean, description: ` Instead of hoisting package installs in \`node_modules\`, install packages in the same manner that they are depended on. This may cause very deep @@ -1164,7 +1147,7 @@ define('legacy-bundling', { define('legacy-peer-deps', { default: false, - type: Boolean, + type: Types.Boolean, description: ` Causes npm to completely ignore \`peerDependencies\` when building a package tree, as in npm versions 3 through 6. @@ -1185,7 +1168,7 @@ define('legacy-peer-deps', { define('link', { default: false, - type: Boolean, + type: Types.Boolean, description: ` Used with \`npm ls\`, limiting output to only those packages that are linked. @@ -1194,8 +1177,7 @@ define('link', { define('local-address', { default: null, - type: getLocalAddresses(), - typeDescription: 'IP Address', + type: Types.IpAddress, description: ` The IP address of the local interface to use when making connections to the npm registry. Must be IPv4 in versions of Node prior to 0.12. @@ -1279,7 +1261,7 @@ define('loglevel', { ], description: ` What level of logs to report. All logs are written to a debug log, - with the path to that file printed if the execution of a command fails. + with the Types.path to that file printed if the execution of a command fails. Any logs of a higher level than the setting are shown. The default is "notice". @@ -1293,7 +1275,7 @@ define('loglevel', { define('logs-dir', { default: null, - type: [null, path], + type: [null, Types.path], defaultDescription: ` A directory named \`_logs\` inside the cache `, @@ -1305,7 +1287,7 @@ define('logs-dir', { define('logs-max', { default: 10, - type: Number, + type: Types.Number, description: ` The maximum number of log files to store. @@ -1315,7 +1297,7 @@ define('logs-max', { define('long', { default: false, - type: Boolean, + type: Types.Boolean, short: 'l', description: ` Show extended information in \`ls\`, \`search\`, and \`help-search\`. @@ -1324,7 +1306,7 @@ define('long', { define('maxsockets', { default: 15, - type: Number, + type: Types.Number, description: ` The maximum number of connections to use per origin (protocol/host/port combination). @@ -1336,7 +1318,7 @@ define('maxsockets', { define('message', { default: '%s', - type: String, + type: Types.String, short: 'm', description: ` Commit message which is used by \`npm version\` when creating version commit. @@ -1348,7 +1330,7 @@ define('message', { define('node-options', { default: null, - type: [null, String], + type: [null, Types.String], description: ` Options to pass through to Node.js via the \`NODE_OPTIONS\` environment variable. This does not impact how npm itself is executed but it does @@ -1361,7 +1343,7 @@ define('noproxy', { defaultDescription: ` The value of the NO_PROXY environment variable `, - type: [String, Array], + type: [Types.String, Types.Array], description: ` Domain extensions that should bypass any proxies. @@ -1378,7 +1360,7 @@ define('noproxy', { define('offline', { default: false, - type: Boolean, + type: Types.Boolean, description: ` Force offline mode: no network requests will be done during install. To allow the CLI to fill in missing cache data, see \`--prefer-offline\`. @@ -1392,7 +1374,7 @@ define('omit', { 'dev' if the \`NODE_ENV\` environment variable is set to 'production', otherwise empty. `, - type: [Array, 'dev', 'optional', 'peer'], + type: [Types.Array, 'prod', 'dev', 'optional', 'peer'], description: ` Dependency types to omit from the installation tree on disk. @@ -1414,7 +1396,7 @@ define('omit', { define('omit-lockfile-registry-resolved', { default: false, - type: Boolean, + type: Types.Boolean, description: ` This option causes npm to create lock files without a \`resolved\` key for registry dependencies. Subsequent installs will need to resolve tarball @@ -1441,7 +1423,7 @@ define('only', { define('optional', { default: null, - type: [null, Boolean], + type: [null, Types.Boolean], deprecated: ` Use \`--omit=optional\` to exclude optional dependencies, or \`--include=optional\` to include them. @@ -1458,7 +1440,7 @@ define('optional', { define('otp', { default: null, - type: [null, String], + type: [null, Types.String], description: ` This is a one-time password from a two-factor authenticator. It's needed when publishing or changing package permissions with \`npm access\`. @@ -1472,7 +1454,7 @@ define('otp', { define('package', { default: [], hint: '', - type: [String, Array], + type: [Types.String, Types.Array], description: ` The package or packages to install for [\`npm exec\`](/commands/npm-exec) `, @@ -1481,7 +1463,7 @@ define('package', { define('package-lock', { default: true, - type: Boolean, + type: Types.Boolean, description: ` If set to false, then ignore \`package-lock.json\` files when installing. This will also prevent _writing_ \`package-lock.json\` if \`save\` is @@ -1499,7 +1481,7 @@ define('package-lock', { define('package-lock-only', { default: false, - type: Boolean, + type: Types.Boolean, description: ` If set to true, the current operation will only use the \`package-lock.json\`, ignoring \`node_modules\`. @@ -1520,7 +1502,7 @@ define('package-lock-only', { define('pack-destination', { default: '.', - type: String, + type: Types.String, description: ` Directory in which \`npm pack\` will save tarballs. `, @@ -1529,7 +1511,7 @@ define('pack-destination', { define('parseable', { default: false, - type: Boolean, + type: Types.Boolean, short: 'p', description: ` Output parseable results from commands that write to standard output. For @@ -1540,7 +1522,7 @@ define('parseable', { define('prefer-offline', { default: false, - type: Boolean, + type: Types.Boolean, description: ` If true, staleness checks for cached data will be bypassed, but missing data will be requested from the server. To force full offline mode, use @@ -1551,7 +1533,7 @@ define('prefer-offline', { define('prefer-online', { default: false, - type: Boolean, + type: Types.Boolean, description: ` If true, staleness checks for cached data will be forced, making the CLI look for updates immediately even for fresh package data. @@ -1561,7 +1543,7 @@ define('prefer-online', { // `prefix` has its default defined outside of this module define('prefix', { - type: path, + type: Types.path, short: 'C', default: '', defaultDescription: ` @@ -1578,17 +1560,17 @@ define('prefix', { define('preid', { default: '', hint: 'prerelease-id', - type: String, + type: Types.String, description: ` The "prerelease identifier" to use as a prefix for the "prerelease" part - of a semver. Like the \`rc\` in \`1.2.0-rc.8\`. + of a Types.semver. Like the \`rc\` in \`1.2.0-rc.8\`. `, flatten, }) define('production', { default: null, - type: [null, Boolean], + type: [null, Types.Boolean], deprecated: 'Use `--omit=dev` instead.', description: 'Alias for `--omit=dev`', flatten (key, obj, flatOptions) { @@ -1601,7 +1583,7 @@ define('progress', { defaultDescription: ` \`true\` unless running in a known CI system `, - type: Boolean, + type: Types.Boolean, description: ` When set to \`true\`, npm will display a progress bar during time intensive operations, if \`process.stderr\` is a TTY. @@ -1616,7 +1598,7 @@ define('progress', { define('proxy', { default: null, - type: [null, false, url], // allow proxy to be disabled explicitly + type: [null, false, Types.url], // allow proxy to be disabled explicitly description: ` A proxy to use for outgoing http requests. If the \`HTTP_PROXY\` or \`http_proxy\` environment variables are set, proxy settings will be @@ -1627,7 +1609,7 @@ define('proxy', { define('read-only', { default: false, - type: Boolean, + type: Types.Boolean, description: ` This is used to mark a token as unable to publish when configuring limited access tokens with the \`npm token create\` command. @@ -1637,7 +1619,7 @@ define('read-only', { define('rebuild-bundle', { default: true, - type: Boolean, + type: Types.Boolean, description: ` Rebuild bundled dependencies after installation. `, @@ -1646,7 +1628,7 @@ define('rebuild-bundle', { define('registry', { default: 'https://registry.npmjs.org/', - type: url, + type: Types.url, description: ` The base URL of the npm registry. `, @@ -1656,7 +1638,7 @@ define('registry', { define('replace-registry-host', { default: 'npmjs', hint: ' | hostname', - type: ['npmjs', 'never', 'always', String], + type: ['npmjs', 'never', 'always', Types.String], description: ` Defines behavior for replacing the registry host in a lockfile with the configured registry. @@ -1676,7 +1658,7 @@ define('save', { defaultDescription: `\`true\` unless when using \`npm update\` where it defaults to \`false\``, usage: '-S|--save|--no-save|--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle', - type: Boolean, + type: Types.Boolean, short: 'S', description: ` Save installed packages to a \`package.json\` file as dependencies. @@ -1691,7 +1673,7 @@ define('save', { define('save-bundle', { default: false, - type: Boolean, + type: Types.Boolean, short: 'B', description: ` If a package would be saved at install time by the use of \`--save\`, @@ -1717,7 +1699,7 @@ define('save-bundle', { define('save-dev', { default: false, - type: Boolean, + type: Types.Boolean, short: 'D', description: ` Save installed packages to a package.json file as \`devDependencies\`. @@ -1736,11 +1718,11 @@ define('save-dev', { define('save-exact', { default: false, - type: Boolean, + type: Types.Boolean, short: 'E', description: ` Dependencies saved to package.json will be configured with an exact - version rather than using npm's default semver range operator. + version rather than using npm's default Types.semver range operator. `, flatten (key, obj, flatOptions) { // just call the save-prefix flattener, it reads from obj['save-exact'] @@ -1750,7 +1732,7 @@ define('save-exact', { define('save-optional', { default: false, - type: Boolean, + type: Types.Boolean, short: 'O', description: ` Save installed packages to a package.json file as @@ -1780,7 +1762,7 @@ define('save-optional', { define('save-peer', { default: false, - type: Boolean, + type: Types.Boolean, description: ` Save installed packages to a package.json file as \`peerDependencies\` `, @@ -1808,7 +1790,7 @@ define('save-peer', { define('save-prefix', { default: '^', - type: String, + type: Types.String, description: ` Configure how versions of packages installed to a package.json file via \`--save\` or \`--save-dev\` get prefixed. @@ -1826,7 +1808,7 @@ define('save-prefix', { define('save-prod', { default: false, - type: Boolean, + type: Types.Boolean, short: 'P', description: ` Save installed packages into \`dependencies\` specifically. This is @@ -1854,7 +1836,7 @@ define('scope', { defaultDescription: ` the scope of the current project, if any, or "" `, - type: String, + type: Types.String, hint: '<@scope>', description: ` Associate an operation with a scope for a scoped registry. @@ -1895,7 +1877,7 @@ define('script-shell', { defaultDescription: ` '/bin/sh' on POSIX systems, 'cmd.exe' on Windows `, - type: [null, String], + type: [null, Types.String], description: ` The shell to use for scripts run with the \`npm exec\`, \`npm run\` and \`npm init \` commands. @@ -1907,7 +1889,7 @@ define('script-shell', { define('searchexclude', { default: '', - type: String, + type: Types.String, description: ` Space-separated options that limit the results from search. `, @@ -1919,9 +1901,9 @@ define('searchexclude', { define('searchlimit', { default: 20, - type: Number, + type: Types.Number, description: ` - Number of items to limit search results to. Will not apply at all to + Types.Number of items to limit search results to. Will not apply at all to legacy searches. `, flatten (key, obj, flatOptions) { @@ -1932,7 +1914,7 @@ define('searchlimit', { define('searchopts', { default: '', - type: String, + type: Types.String, description: ` Space-separated options that are always passed to search. `, @@ -1944,7 +1926,7 @@ define('searchopts', { define('searchstaleness', { default: 15 * 60, - type: Number, + type: Types.Number, description: ` The age of the cache, in seconds, before another registry request is made if using legacy search endpoint. @@ -1960,7 +1942,7 @@ define('shell', { defaultDescription: ` SHELL environment variable, or "bash" on Posix, or "cmd.exe" on Windows `, - type: String, + type: Types.String, description: ` The shell to run for the \`npm explore\` command. `, @@ -1969,7 +1951,7 @@ define('shell', { define('shrinkwrap', { default: true, - type: Boolean, + type: Types.Boolean, deprecated: ` Use the --package-lock setting instead. `, @@ -1984,7 +1966,7 @@ define('shrinkwrap', { define('sign-git-commit', { default: false, - type: Boolean, + type: Types.Boolean, description: ` If set to true, then the \`npm version\` command will commit the new package version using \`-S\` to add a signature. @@ -1997,7 +1979,7 @@ define('sign-git-commit', { define('sign-git-tag', { default: false, - type: Boolean, + type: Types.Boolean, description: ` If set to true, then the \`npm version\` command will tag the version using \`-s\` to add a signature. @@ -2010,7 +1992,7 @@ define('sign-git-tag', { define('strict-peer-deps', { default: false, - type: Boolean, + type: Types.Boolean, description: ` If set to \`true\`, and \`--legacy-peer-deps\` is not set, then _any_ conflicting \`peerDependencies\` will be treated as an install failure, @@ -2031,7 +2013,7 @@ define('strict-peer-deps', { define('strict-ssl', { default: true, - type: Boolean, + type: Types.Boolean, description: ` Whether or not to do SSL key validation when making requests to the registry via https. @@ -2045,7 +2027,7 @@ define('strict-ssl', { define('tag', { default: 'latest', - type: String, + type: Types.String, description: ` If you ask npm to install a package and don't tell it a specific version, then it will install the specified tag. @@ -2063,7 +2045,7 @@ define('tag', { define('tag-version-prefix', { default: 'v', - type: String, + type: Types.String, description: ` If set, alters the prefix used when tagging a new version when performing a version increment using \`npm version\`. To remove the prefix @@ -2078,7 +2060,7 @@ define('tag-version-prefix', { define('timing', { default: false, - type: Boolean, + type: Types.Boolean, description: ` If true, writes timing information to a process specific json file in the cache or \`logs-dir\`. The file name ends with \`-timing.json\`. @@ -2097,7 +2079,7 @@ define('tmp', { The value returned by the Node.js \`os.tmpdir()\` method `, - type: path, + type: Types.path, deprecated: ` This setting is no longer used. npm stores temporary files in a special location in the cache, and they are managed by @@ -2111,7 +2093,7 @@ define('tmp', { define('umask', { default: 0, - type: Umask, + type: Types.Umask, description: ` The "umask" value to use when setting the file creation mode on files and folders. @@ -2137,7 +2119,7 @@ define('unicode', { false on windows, true on mac/unix systems with a unicode locale, as defined by the \`LC_ALL\`, \`LC_CTYPE\`, or \`LANG\` environment variables. `, - type: Boolean, + type: Types.Boolean, description: ` When set to true, npm uses unicode characters in the tree output. When false, it uses ascii characters instead of unicode glyphs. @@ -2147,7 +2129,7 @@ define('unicode', { define('update-notifier', { default: true, - type: Boolean, + type: Types.Boolean, description: ` Set to false to suppress the update notification when using an older version of npm than the latest. @@ -2156,7 +2138,7 @@ define('update-notifier', { define('usage', { default: false, - type: Boolean, + type: Types.Boolean, short: ['?', 'H', 'h'], description: ` Show short usage output about the command specified. @@ -2170,7 +2152,7 @@ define('user-agent', { '{arch} ' + 'workspaces/{workspaces} ' + '{ci}', - type: String, + type: Types.String, description: ` Sets the User-Agent request header. The following fields are replaced with their actual counterparts: @@ -2213,7 +2195,7 @@ define('user-agent', { define('userconfig', { default: '~/.npmrc', - type: path, + type: Types.path, description: ` The location of user-level configuration settings. @@ -2225,7 +2207,7 @@ define('userconfig', { define('version', { default: false, - type: Boolean, + type: Types.Boolean, short: 'v', description: ` If true, output the npm version and exit successfully. @@ -2236,7 +2218,7 @@ define('version', { define('versions', { default: false, - type: Boolean, + type: Types.Boolean, description: ` If true, output the npm version as well as node's \`process.versions\` map and the version in the current working directory's \`package.json\` @@ -2251,7 +2233,7 @@ define('viewer', { defaultDescription: ` "man" on Posix, "browser" on Windows `, - type: String, + type: Types.String, description: ` The program to use to view help content. @@ -2262,7 +2244,7 @@ define('viewer', { define('which', { default: null, hint: '', - type: [null, Number], + type: [null, Types.Number], description: ` If there are multiple funding sources, which 1-indexed source URL to open. `, @@ -2270,7 +2252,7 @@ define('which', { define('workspace', { default: [], - type: [String, Array], + type: [Types.String, Types.Array], hint: '', short: 'w', envExport: false, @@ -2297,7 +2279,7 @@ define('workspace', { define('workspaces', { default: null, - type: [null, Boolean], + type: [null, Types.Boolean], short: 'ws', envExport: false, description: ` @@ -2329,7 +2311,7 @@ define('workspaces', { define('workspaces-update', { default: true, - type: Boolean, + type: Types.Boolean, description: ` If set to true, the npm cli will run an update after operations that may possibly change the workspaces installed to the \`node_modules\` folder. @@ -2339,7 +2321,7 @@ define('workspaces-update', { define('yes', { default: null, - type: [null, Boolean], + type: [null, Types.Boolean], short: 'y', description: ` Automatically answer "yes" to any prompts that npm might print on diff --git a/lib/utils/config/index.js b/lib/utils/config/index.js index d393aec2297d2..18837c9529ba7 100644 --- a/lib/utils/config/index.js +++ b/lib/utils/config/index.js @@ -1,4 +1,4 @@ -const definitions = require('./definitions.js') +const { definitions, derived } = require('./definitions.js') // use the defined flattening function, and copy over any scoped // registries and registry-specific "nerfdart" configs verbatim @@ -12,7 +12,7 @@ const definitions = require('./definitions.js') const flatten = (obj, flat = {}) => { for (const [key, val] of Object.entries(obj)) { const def = definitions[key] - if (def && def.flatten) { + if (def?.flatten) { def.flatten(key, obj, flat) } else if (/@.*:registry$/i.test(key) || /^\/\//.test(key)) { flat[key] = val @@ -75,4 +75,5 @@ module.exports = { definitions, flatten, shorthands, + derived, } diff --git a/lib/utils/exit-handler.js b/lib/utils/exit-handler.js index b5fc7042bd020..05395f04f2568 100644 --- a/lib/utils/exit-handler.js +++ b/lib/utils/exit-handler.js @@ -18,8 +18,6 @@ process.on('exit', code => { // unfinished timer check below process.emit('timeEnd', 'npm') - const hasLoadedNpm = npm?.config.loaded - // Unfinished timers can be read before config load if (npm) { for (const [name, timer] of npm.unfinishedTimers) { @@ -44,7 +42,7 @@ process.on('exit', code => { } // npm must be loaded to know where the log file was written - if (hasLoadedNpm) { + if (npm.loaded) { // write the timing file now, this might do nothing based on the configs set. // we need to call it here in case it errors so we dont tell the user // about a timing file that doesn't exist @@ -110,8 +108,6 @@ const exitHandler = err => { log.disableProgress() - const hasLoadedNpm = npm?.config.loaded - if (!npm) { err = err || new Error('Exit prior to setting npm in exit handler') // eslint-disable-next-line no-console @@ -119,7 +115,7 @@ const exitHandler = err => { return process.exit(1) } - if (!hasLoadedNpm) { + if (!npm.loaded) { err = err || new Error('Exit prior to config file resolving.') // eslint-disable-next-line no-console console.error(err.stack || err.message) @@ -206,13 +202,13 @@ const exitHandler = err => { } } - if (hasLoadedNpm) { + if (npm.loaded) { npm.flushOutput(jsonError) } log.verbose('exit', exitCode || 0) - showLogFileError = (hasLoadedNpm && npm.silent) || noLogMessage + showLogFileError = npm.silent || noLogMessage ? false : !!exitCode diff --git a/lib/utils/update-notifier.js b/lib/utils/update-notifier.js index a7eaaca64747f..8625bb2b82900 100644 --- a/lib/utils/update-notifier.js +++ b/lib/utils/update-notifier.js @@ -6,26 +6,29 @@ const pacote = require('pacote') const ciInfo = require('ci-info') const semver = require('semver') const chalk = require('chalk') -const { promisify } = require('util') -const stat = promisify(require('fs').stat) -const writeFile = promisify(require('fs').writeFile) +const { stat, writeFile } = require('fs/promises') const { resolve } = require('path') const SKIP = Symbol('SKIP') -const isGlobalNpmUpdate = npm => { - return npm.flatOptions.global && - ['install', 'update'].includes(npm.command) && - npm.argv.some(arg => /^npm(@|$)/.test(arg)) +const isGlobalNpmUpdate = npm => npm.flatOptions.global && + ['install', 'update'].includes(npm.command) && + npm.argv.some(arg => /^npm(@|$)/.test(arg)) + +const skipCheck = (npm) => { + // never check for updates in CI, when updating npm already, or opted out + if (!npm.config.get('update-notifier') || isGlobalNpmUpdate(npm) || ciInfo.isCI) { + return true + } } // update check frequency const DAILY = 1000 * 60 * 60 * 24 const WEEKLY = DAILY * 7 +const FILE_NAME = '_update-notifier-last-checked' // don't put it in the _cacache folder, just in npm's cache -const lastCheckedFile = npm => - resolve(npm.flatOptions.cache, '../_update-notifier-last-checked') +const lastCheckedFile = npm => resolve(npm.flatOptions.cache, '..', FILE_NAME) const checkTimeout = async (npm, duration) => { const t = new Date(Date.now() - duration) @@ -36,13 +39,9 @@ const checkTimeout = async (npm, duration) => { } const updateNotifier = async (npm, spec = 'latest') => { - // never check for updates in CI, when updating npm already, or opted out - if (!npm.config.get('update-notifier') || - isGlobalNpmUpdate(npm) || - ciInfo.isCI) { + if (skipCheck(npm)) { return SKIP } - // if we're on a prerelease train, then updates are coming fast // check for a new one daily. otherwise, weekly. const { version } = npm diff --git a/lib/utils/wrap.js b/lib/utils/wrap.js new file mode 100644 index 0000000000000..e01122787dc15 --- /dev/null +++ b/lib/utils/wrap.js @@ -0,0 +1,21 @@ +const unindent = s => { + // get the first \n followed by a bunch of spaces, and pluck off + // that many spaces from the start of every line. + const match = s.match(/\n +/) + return !match ? s.trim() : s.split(match[0]).join('\n').trim() +} + +const wrap = (str, { min = 20, max = 80, padding = 5, columns = process.stdout.columns } = {}) => { + const cols = Math.min(Math.max(min, columns) || max, max) - padding + return unindent(str) + .split(/[ \n]+/) + .reduce((left, right) => { + const last = left.split('\n').pop() + const join = last.length && last.length + right.length > cols ? '\n' : ' ' + return left + join + right + }) +} + +module.exports = wrap +module.exports.wrap = wrap +module.exports.unindent = unindent diff --git a/node_modules/nopt/lib/debug.js b/node_modules/nopt/lib/debug.js new file mode 100644 index 0000000000000..194d0c6881882 --- /dev/null +++ b/node_modules/nopt/lib/debug.js @@ -0,0 +1,6 @@ +/* istanbul ignore next */ +module.exports = process.env.DEBUG_NOPT || process.env.NOPT_DEBUG + ? function () { + console.error.apply(console, arguments) + } + : function () {} diff --git a/node_modules/nopt/lib/nopt-lib.js b/node_modules/nopt/lib/nopt-lib.js new file mode 100644 index 0000000000000..89d269fb43f1a --- /dev/null +++ b/node_modules/nopt/lib/nopt-lib.js @@ -0,0 +1,428 @@ +var abbrev = require('abbrev') +const debug = require('./debug') +const defaultTypeDefs = require('./type-defs') + +function nopt (args, { types, shorthands, typeDefs, invalidHandler }) { + debug(types, shorthands, args, typeDefs) + + var data = {} + var argv = { + remain: [], + cooked: args, + original: args.slice(0), + } + + parse(args, data, argv.remain, { typeDefs, types, shorthands }) + + // now data is full + clean(data, { types, typeDefs, invalidHandler }) + data.argv = argv + + Object.defineProperty(data.argv, 'toString', { + value: function () { + return this.original.map(JSON.stringify).join(' ') + }, + enumerable: false, + }) + + return data +} + +function clean (data, { types, typeDefs, invalidHandler }) { + const StringType = typeDefs.String.type + const NumberType = typeDefs.Number.type + const ArrayType = typeDefs.Array.type + const BooleanType = typeDefs.Boolean.type + const DateType = typeDefs.Date.type + + var remove = {} + var typeDefault = [false, true, null, StringType, ArrayType] + + Object.keys(data).forEach(function (k) { + if (k === 'argv') { + return + } + var val = data[k] + var isArray = Array.isArray(val) + var type = types[k] + if (!isArray) { + val = [val] + } + if (!type) { + type = typeDefault + } + if (type === ArrayType) { + type = typeDefault.concat(ArrayType) + } + if (!Array.isArray(type)) { + type = [type] + } + + debug('val=%j', val) + debug('types=', type) + val = val.map(function (v) { + // if it's an unknown value, then parse false/true/null/numbers/dates + if (typeof v === 'string') { + debug('string %j', v) + v = v.trim() + if ((v === 'null' && ~type.indexOf(null)) + || (v === 'true' && + (~type.indexOf(true) || ~type.indexOf(BooleanType))) + || (v === 'false' && + (~type.indexOf(false) || ~type.indexOf(BooleanType)))) { + v = JSON.parse(v) + debug('jsonable %j', v) + } else if (~type.indexOf(NumberType) && !isNaN(v)) { + debug('convert to number', v) + v = +v + } else if (~type.indexOf(DateType) && !isNaN(Date.parse(v))) { + debug('convert to date', v) + v = new Date(v) + } + } + + if (!Object.prototype.hasOwnProperty.call(types, k)) { + return v + } + + // allow `--no-blah` to set 'blah' to null if null is allowed + if (v === false && ~type.indexOf(null) && + !(~type.indexOf(false) || ~type.indexOf(BooleanType))) { + v = null + } + + var d = {} + d[k] = v + debug('prevalidated val', d, v, types[k]) + if (!validate(d, k, v, types[k], { typeDefs })) { + if (invalidHandler) { + invalidHandler(k, v, types[k], data) + } else if (invalidHandler !== false) { + debug('invalid: ' + k + '=' + v, types[k]) + } + return remove + } + debug('validated v', d, v, types[k]) + return d[k] + }).filter(function (v) { + return v !== remove + }) + + // if we allow Array specifically, then an empty array is how we + // express 'no value here', not null. Allow it. + if (!val.length && type.indexOf(ArrayType) === -1) { + debug('VAL HAS NO LENGTH, DELETE IT', val, k, type.indexOf(ArrayType)) + delete data[k] + } else if (isArray) { + debug(isArray, data[k], val) + data[k] = val + } else { + data[k] = val[0] + } + + debug('k=%s val=%j', k, val, data[k]) + }) +} + +function validate (data, k, val, type, { typeDefs }) { + const ArrayType = typeDefs.Array.type + // arrays are lists of types. + if (Array.isArray(type)) { + for (let i = 0, l = type.length; i < l; i++) { + if (type[i] === ArrayType) { + continue + } + if (validate(data, k, val, type[i], { typeDefs })) { + return true + } + } + delete data[k] + return false + } + + // an array of anything? + if (type === ArrayType) { + return true + } + + // Original comment: + // NaN is poisonous. Means that something is not allowed. + // New comment: Changing this to an isNaN check breaks a lot of tests. + // Something is being assumed here that is not actually what happens in + // practice. Fixing it is outside the scope of getting linting to pass in + // this repo. Leaving as-is for now. + /* eslint-disable-next-line no-self-compare */ + if (type !== type) { + debug('Poison NaN', k, val, type) + delete data[k] + return false + } + + // explicit list of values + if (val === type) { + debug('Explicitly allowed %j', val) + data[k] = val + return true + } + + // now go through the list of typeDefs, validate against each one. + var ok = false + var types = Object.keys(typeDefs) + for (let i = 0, l = types.length; i < l; i++) { + debug('test type %j %j %j', k, val, types[i]) + var t = typeDefs[types[i]] + if (t && ( + (type && type.name && t.type && t.type.name) ? + (type.name === t.type.name) : + (type === t.type) + )) { + var d = {} + ok = t.validate(d, k, val) !== false + val = d[k] + if (ok) { + data[k] = val + break + } + } + } + debug('OK? %j (%j %j %j)', ok, k, val, types[types.length - 1]) + + if (!ok) { + delete data[k] + } + return ok +} + +function parse (args, data, remain, { typeDefs, types, shorthands }) { + const StringType = typeDefs.String.type + const NumberType = typeDefs.String.type + const ArrayType = typeDefs.Array.type + const BooleanType = typeDefs.Boolean.type + + debug('parse', args, data, remain) + + var abbrevs = abbrev(Object.keys(types)) + var shortAbbr = abbrev(Object.keys(shorthands)) + + for (var i = 0; i < args.length; i++) { + var arg = args[i] + debug('arg', arg) + + if (arg.match(/^-{2,}$/)) { + // done with keys. + // the rest are args. + remain.push.apply(remain, args.slice(i + 1)) + args[i] = '--' + break + } + var hadEq = false + if (arg.charAt(0) === '-' && arg.length > 1) { + var at = arg.indexOf('=') + if (at > -1) { + hadEq = true + var v = arg.slice(at + 1) + arg = arg.slice(0, at) + args.splice(i, 1, arg, v) + } + + // see if it's a shorthand + // if so, splice and back up to re-parse it. + var shRes = resolveShort(arg, shortAbbr, abbrevs, { shorthands }) + debug('arg=%j shRes=%j', arg, shRes) + if (shRes) { + debug(arg, shRes) + args.splice.apply(args, [i, 1].concat(shRes)) + if (arg !== shRes[0]) { + i-- + continue + } + } + arg = arg.replace(/^-+/, '') + var no = null + while (arg.toLowerCase().indexOf('no-') === 0) { + no = !no + arg = arg.slice(3) + } + + if (abbrevs[arg]) { + arg = abbrevs[arg] + } + + var argType = types[arg] + var isTypeArray = Array.isArray(argType) + if (isTypeArray && argType.length === 1) { + isTypeArray = false + argType = argType[0] + } + + var isArray = argType === ArrayType || + isTypeArray && argType.indexOf(ArrayType) !== -1 + + // allow unknown things to be arrays if specified multiple times. + if ( + !Object.prototype.hasOwnProperty.call(types, arg) && + Object.prototype.hasOwnProperty.call(data, arg) + ) { + if (!Array.isArray(data[arg])) { + data[arg] = [data[arg]] + } + isArray = true + } + + var val + var la = args[i + 1] + + var isBool = typeof no === 'boolean' || + argType === BooleanType || + isTypeArray && argType.indexOf(BooleanType) !== -1 || + (typeof argType === 'undefined' && !hadEq) || + (la === 'false' && + (argType === null || + isTypeArray && ~argType.indexOf(null))) + + if (isBool) { + // just set and move along + val = !no + // however, also support --bool true or --bool false + if (la === 'true' || la === 'false') { + val = JSON.parse(la) + la = null + if (no) { + val = !val + } + i++ + } + + // also support "foo":[Boolean, "bar"] and "--foo bar" + if (isTypeArray && la) { + if (~argType.indexOf(la)) { + // an explicit type + val = la + i++ + } else if (la === 'null' && ~argType.indexOf(null)) { + // null allowed + val = null + i++ + } else if (!la.match(/^-{2,}[^-]/) && + !isNaN(la) && + ~argType.indexOf(NumberType)) { + // number + val = +la + i++ + } else if (!la.match(/^-[^-]/) && ~argType.indexOf(StringType)) { + // string + val = la + i++ + } + } + + if (isArray) { + (data[arg] = data[arg] || []).push(val) + } else { + data[arg] = val + } + + continue + } + + if (argType === StringType) { + if (la === undefined) { + la = '' + } else if (la.match(/^-{1,2}[^-]+/)) { + la = '' + i-- + } + } + + if (la && la.match(/^-{2,}$/)) { + la = undefined + i-- + } + + val = la === undefined ? true : la + if (isArray) { + (data[arg] = data[arg] || []).push(val) + } else { + data[arg] = val + } + + i++ + continue + } + remain.push(arg) + } +} + +function resolveShort (arg, shortAbbr, abbrevs, { shorthands }) { + // handle single-char shorthands glommed together, like + // npm ls -glp, but only if there is one dash, and only if + // all of the chars are single-char shorthands, and it's + // not a match to some other abbrev. + arg = arg.replace(/^-+/, '') + + // if it's an exact known option, then don't go any further + if (abbrevs[arg] === arg) { + return null + } + + // if it's an exact known shortopt, same deal + if (shorthands[arg]) { + // make it an array, if it's a list of words + if (shorthands[arg] && !Array.isArray(shorthands[arg])) { + shorthands[arg] = shorthands[arg].split(/\s+/) + } + + return shorthands[arg] + } + + // first check to see if this arg is a set of single-char shorthands + var singles = shorthands.___singles + if (!singles) { + singles = Object.keys(shorthands).filter(function (s) { + return s.length === 1 + }).reduce(function (l, r) { + l[r] = true + return l + }, {}) + shorthands.___singles = singles + debug('shorthand singles', singles) + } + + var chrs = arg.split('').filter(function (c) { + return singles[c] + }) + + if (chrs.join('') === arg) { + return chrs.map(function (c) { + return shorthands[c] + }).reduce(function (l, r) { + return l.concat(r) + }, []) + } + + // if it's an arg abbrev, and not a literal shorthand, then prefer the arg + if (abbrevs[arg] && !shorthands[arg]) { + return null + } + + // if it's an abbr for a shorthand, then use that + if (shortAbbr[arg]) { + arg = shortAbbr[arg] + } + + // make it an array, if it's a list of words + if (shorthands[arg] && !Array.isArray(shorthands[arg])) { + shorthands[arg] = shorthands[arg].split(/\s+/) + } + + return shorthands[arg] +} + +module.exports = { + nopt, + clean, + parse, + validate, + resolveShort, + typeDefs: defaultTypeDefs, +} diff --git a/node_modules/nopt/lib/nopt.js b/node_modules/nopt/lib/nopt.js index 5829c2fe0f637..76de480cc3268 100644 --- a/node_modules/nopt/lib/nopt.js +++ b/node_modules/nopt/lib/nopt.js @@ -1,515 +1,30 @@ -// info about each config option. +const lib = require('./nopt-lib') +const defaultTypeDefs = require('./type-defs') -var debug = process.env.DEBUG_NOPT || process.env.NOPT_DEBUG - ? function () { - console.error.apply(console, arguments) - } - : function () {} - -var url = require('url') -var path = require('path') -var Stream = require('stream').Stream -var abbrev = require('abbrev') -var os = require('os') +// This is the version of nopt's API that requires setting typeDefs and invaliHandler +// on the required `nopt` object since it is a singleton. To not do a breaking change +// an API that requires all options be passed in is located in `nopt-lib.js` and +// exported here as lib. +// TODO(breaking): make API only work in non-singleton mode module.exports = exports = nopt exports.clean = clean - -exports.typeDefs = - { String: { type: String, validate: validateString }, - Boolean: { type: Boolean, validate: validateBoolean }, - url: { type: url, validate: validateUrl }, - Number: { type: Number, validate: validateNumber }, - path: { type: path, validate: validatePath }, - Stream: { type: Stream, validate: validateStream }, - Date: { type: Date, validate: validateDate }, - } - -function nopt (types, shorthands, args, slice) { - args = args || process.argv - types = types || {} - shorthands = shorthands || {} - if (typeof slice !== 'number') { - slice = 2 - } - - debug(types, shorthands, args, slice) - - args = args.slice(slice) - var data = {} - var argv = { - remain: [], - cooked: args, - original: args.slice(0), - } - - parse(args, data, argv.remain, types, shorthands) - // now data is full - clean(data, types, exports.typeDefs) - data.argv = argv - Object.defineProperty(data.argv, 'toString', { value: function () { - return this.original.map(JSON.stringify).join(' ') - }, - enumerable: false }) - return data -} - -function clean (data, types, typeDefs) { - typeDefs = typeDefs || exports.typeDefs - var remove = {} - var typeDefault = [false, true, null, String, Array] - - Object.keys(data).forEach(function (k) { - if (k === 'argv') { - return - } - var val = data[k] - var isArray = Array.isArray(val) - var type = types[k] - if (!isArray) { - val = [val] - } - if (!type) { - type = typeDefault - } - if (type === Array) { - type = typeDefault.concat(Array) - } - if (!Array.isArray(type)) { - type = [type] - } - - debug('val=%j', val) - debug('types=', type) - val = val.map(function (v) { - // if it's an unknown value, then parse false/true/null/numbers/dates - if (typeof v === 'string') { - debug('string %j', v) - v = v.trim() - if ((v === 'null' && ~type.indexOf(null)) - || (v === 'true' && - (~type.indexOf(true) || ~type.indexOf(Boolean))) - || (v === 'false' && - (~type.indexOf(false) || ~type.indexOf(Boolean)))) { - v = JSON.parse(v) - debug('jsonable %j', v) - } else if (~type.indexOf(Number) && !isNaN(v)) { - debug('convert to number', v) - v = +v - } else if (~type.indexOf(Date) && !isNaN(Date.parse(v))) { - debug('convert to date', v) - v = new Date(v) - } - } - - if (!Object.prototype.hasOwnProperty.call(types, k)) { - return v - } - - // allow `--no-blah` to set 'blah' to null if null is allowed - if (v === false && ~type.indexOf(null) && - !(~type.indexOf(false) || ~type.indexOf(Boolean))) { - v = null - } - - var d = {} - d[k] = v - debug('prevalidated val', d, v, types[k]) - if (!validate(d, k, v, types[k], typeDefs)) { - if (exports.invalidHandler) { - exports.invalidHandler(k, v, types[k], data) - } else if (exports.invalidHandler !== false) { - debug('invalid: ' + k + '=' + v, types[k]) - } - return remove - } - debug('validated v', d, v, types[k]) - return d[k] - }).filter(function (v) { - return v !== remove - }) - - // if we allow Array specifically, then an empty array is how we - // express 'no value here', not null. Allow it. - if (!val.length && type.indexOf(Array) === -1) { - debug('VAL HAS NO LENGTH, DELETE IT', val, k, type.indexOf(Array)) - delete data[k] - } else if (isArray) { - debug(isArray, data[k], val) - data[k] = val - } else { - data[k] = val[0] - } - - debug('k=%s val=%j', k, val, data[k]) +exports.typeDefs = defaultTypeDefs +exports.lib = lib + +function nopt (types = {}, shorthands = {}, args = process.argv, slice = 2) { + return lib.nopt(args.slice(slice), { + types, + shorthands: shorthands || {}, + typeDefs: exports.typeDefs, + invalidHandler: exports.invalidHandler, }) } -function validateString (data, k, val) { - data[k] = String(val) -} - -function validatePath (data, k, val) { - if (val === true) { - return false - } - if (val === null) { - return true - } - - val = String(val) - - var isWin = process.platform === 'win32' - var homePattern = isWin ? /^~(\/|\\)/ : /^~\// - var home = os.homedir() - - if (home && val.match(homePattern)) { - data[k] = path.resolve(home, val.slice(2)) - } else { - data[k] = path.resolve(val) - } - return true -} - -function validateNumber (data, k, val) { - debug('validate Number %j %j %j', k, val, isNaN(val)) - if (isNaN(val)) { - return false - } - data[k] = +val -} - -function validateDate (data, k, val) { - var s = Date.parse(val) - debug('validate Date %j %j %j', k, val, s) - if (isNaN(s)) { - return false - } - data[k] = new Date(val) -} - -function validateBoolean (data, k, val) { - if (val instanceof Boolean) { - val = val.valueOf() - } else if (typeof val === 'string') { - if (!isNaN(val)) { - val = !!(+val) - } else if (val === 'null' || val === 'false') { - val = false - } else { - val = true - } - } else { - val = !!val - } - data[k] = val -} - -function validateUrl (data, k, val) { - // Changing this would be a breaking change in the npm cli - /* eslint-disable-next-line node/no-deprecated-api */ - val = url.parse(String(val)) - if (!val.host) { - return false - } - data[k] = val.href -} - -function validateStream (data, k, val) { - if (!(val instanceof Stream)) { - return false - } - data[k] = val -} - -function validate (data, k, val, type, typeDefs) { - // arrays are lists of types. - if (Array.isArray(type)) { - for (let i = 0, l = type.length; i < l; i++) { - if (type[i] === Array) { - continue - } - if (validate(data, k, val, type[i], typeDefs)) { - return true - } - } - delete data[k] - return false - } - - // an array of anything? - if (type === Array) { - return true - } - - // Original comment: - // NaN is poisonous. Means that something is not allowed. - // New comment: Changing this to an isNaN check breaks a lot of tests. - // Something is being assumed here that is not actually what happens in - // practice. Fixing it is outside the scope of getting linting to pass in - // this repo. Leaving as-is for now. - /* eslint-disable-next-line no-self-compare */ - if (type !== type) { - debug('Poison NaN', k, val, type) - delete data[k] - return false - } - - // explicit list of values - if (val === type) { - debug('Explicitly allowed %j', val) - // if (isArray) (data[k] = data[k] || []).push(val) - // else data[k] = val - data[k] = val - return true - } - - // now go through the list of typeDefs, validate against each one. - var ok = false - var types = Object.keys(typeDefs) - for (let i = 0, l = types.length; i < l; i++) { - debug('test type %j %j %j', k, val, types[i]) - var t = typeDefs[types[i]] - if (t && ( - (type && type.name && t.type && t.type.name) ? - (type.name === t.type.name) : - (type === t.type) - )) { - var d = {} - ok = t.validate(d, k, val) !== false - val = d[k] - if (ok) { - // if (isArray) (data[k] = data[k] || []).push(val) - // else data[k] = val - data[k] = val - break - } - } - } - debug('OK? %j (%j %j %j)', ok, k, val, types[types.length - 1]) - - if (!ok) { - delete data[k] - } - return ok -} - -function parse (args, data, remain, types, shorthands) { - debug('parse', args, data, remain) - - var abbrevs = abbrev(Object.keys(types)) - var shortAbbr = abbrev(Object.keys(shorthands)) - - for (var i = 0; i < args.length; i++) { - var arg = args[i] - debug('arg', arg) - - if (arg.match(/^-{2,}$/)) { - // done with keys. - // the rest are args. - remain.push.apply(remain, args.slice(i + 1)) - args[i] = '--' - break - } - var hadEq = false - if (arg.charAt(0) === '-' && arg.length > 1) { - var at = arg.indexOf('=') - if (at > -1) { - hadEq = true - var v = arg.slice(at + 1) - arg = arg.slice(0, at) - args.splice(i, 1, arg, v) - } - - // see if it's a shorthand - // if so, splice and back up to re-parse it. - var shRes = resolveShort(arg, shorthands, shortAbbr, abbrevs) - debug('arg=%j shRes=%j', arg, shRes) - if (shRes) { - debug(arg, shRes) - args.splice.apply(args, [i, 1].concat(shRes)) - if (arg !== shRes[0]) { - i-- - continue - } - } - arg = arg.replace(/^-+/, '') - var no = null - while (arg.toLowerCase().indexOf('no-') === 0) { - no = !no - arg = arg.slice(3) - } - - if (abbrevs[arg]) { - arg = abbrevs[arg] - } - - var argType = types[arg] - var isTypeArray = Array.isArray(argType) - if (isTypeArray && argType.length === 1) { - isTypeArray = false - argType = argType[0] - } - - var isArray = argType === Array || - isTypeArray && argType.indexOf(Array) !== -1 - - // allow unknown things to be arrays if specified multiple times. - if ( - !Object.prototype.hasOwnProperty.call(types, arg) && - Object.prototype.hasOwnProperty.call(data, arg) - ) { - if (!Array.isArray(data[arg])) { - data[arg] = [data[arg]] - } - isArray = true - } - - var val - var la = args[i + 1] - - var isBool = typeof no === 'boolean' || - argType === Boolean || - isTypeArray && argType.indexOf(Boolean) !== -1 || - (typeof argType === 'undefined' && !hadEq) || - (la === 'false' && - (argType === null || - isTypeArray && ~argType.indexOf(null))) - - if (isBool) { - // just set and move along - val = !no - // however, also support --bool true or --bool false - if (la === 'true' || la === 'false') { - val = JSON.parse(la) - la = null - if (no) { - val = !val - } - i++ - } - - // also support "foo":[Boolean, "bar"] and "--foo bar" - if (isTypeArray && la) { - if (~argType.indexOf(la)) { - // an explicit type - val = la - i++ - } else if (la === 'null' && ~argType.indexOf(null)) { - // null allowed - val = null - i++ - } else if (!la.match(/^-{2,}[^-]/) && - !isNaN(la) && - ~argType.indexOf(Number)) { - // number - val = +la - i++ - } else if (!la.match(/^-[^-]/) && ~argType.indexOf(String)) { - // string - val = la - i++ - } - } - - if (isArray) { - (data[arg] = data[arg] || []).push(val) - } else { - data[arg] = val - } - - continue - } - - if (argType === String) { - if (la === undefined) { - la = '' - } else if (la.match(/^-{1,2}[^-]+/)) { - la = '' - i-- - } - } - - if (la && la.match(/^-{2,}$/)) { - la = undefined - i-- - } - - val = la === undefined ? true : la - if (isArray) { - (data[arg] = data[arg] || []).push(val) - } else { - data[arg] = val - } - - i++ - continue - } - remain.push(arg) - } -} - -function resolveShort (arg, shorthands, shortAbbr, abbrevs) { - // handle single-char shorthands glommed together, like - // npm ls -glp, but only if there is one dash, and only if - // all of the chars are single-char shorthands, and it's - // not a match to some other abbrev. - arg = arg.replace(/^-+/, '') - - // if it's an exact known option, then don't go any further - if (abbrevs[arg] === arg) { - return null - } - - // if it's an exact known shortopt, same deal - if (shorthands[arg]) { - // make it an array, if it's a list of words - if (shorthands[arg] && !Array.isArray(shorthands[arg])) { - shorthands[arg] = shorthands[arg].split(/\s+/) - } - - return shorthands[arg] - } - - // first check to see if this arg is a set of single-char shorthands - var singles = shorthands.___singles - if (!singles) { - singles = Object.keys(shorthands).filter(function (s) { - return s.length === 1 - }).reduce(function (l, r) { - l[r] = true - return l - }, {}) - shorthands.___singles = singles - debug('shorthand singles', singles) - } - - var chrs = arg.split('').filter(function (c) { - return singles[c] +function clean (data, types, typeDefs = exports.typeDefs) { + return lib.clean(data, { + types, + typeDefs, + invalidHandler: exports.invalidHandler, }) - - if (chrs.join('') === arg) { - return chrs.map(function (c) { - return shorthands[c] - }).reduce(function (l, r) { - return l.concat(r) - }, []) - } - - // if it's an arg abbrev, and not a literal shorthand, then prefer the arg - if (abbrevs[arg] && !shorthands[arg]) { - return null - } - - // if it's an abbr for a shorthand, then use that - if (shortAbbr[arg]) { - arg = shortAbbr[arg] - } - - // make it an array, if it's a list of words - if (shorthands[arg] && !Array.isArray(shorthands[arg])) { - shorthands[arg] = shorthands[arg].split(/\s+/) - } - - return shorthands[arg] } diff --git a/node_modules/nopt/lib/type-defs.js b/node_modules/nopt/lib/type-defs.js new file mode 100644 index 0000000000000..6acf5e0a5b9d4 --- /dev/null +++ b/node_modules/nopt/lib/type-defs.js @@ -0,0 +1,91 @@ +var url = require('url') +var path = require('path') +var Stream = require('stream').Stream +var os = require('os') +const debug = require('./debug') + +function validateString (data, k, val) { + data[k] = String(val) +} + +function validatePath (data, k, val) { + if (val === true) { + return false + } + if (val === null) { + return true + } + + val = String(val) + + var isWin = process.platform === 'win32' + var homePattern = isWin ? /^~(\/|\\)/ : /^~\// + var home = os.homedir() + + if (home && val.match(homePattern)) { + data[k] = path.resolve(home, val.slice(2)) + } else { + data[k] = path.resolve(val) + } + return true +} + +function validateNumber (data, k, val) { + debug('validate Number %j %j %j', k, val, isNaN(val)) + if (isNaN(val)) { + return false + } + data[k] = +val +} + +function validateDate (data, k, val) { + var s = Date.parse(val) + debug('validate Date %j %j %j', k, val, s) + if (isNaN(s)) { + return false + } + data[k] = new Date(val) +} + +function validateBoolean (data, k, val) { + if (typeof val === 'string') { + if (!isNaN(val)) { + val = !!(+val) + } else if (val === 'null' || val === 'false') { + val = false + } else { + val = true + } + } else { + val = !!val + } + data[k] = val +} + +function validateUrl (data, k, val) { + // Changing this would be a breaking change in the npm cli + /* eslint-disable-next-line node/no-deprecated-api */ + val = url.parse(String(val)) + if (!val.host) { + return false + } + data[k] = val.href +} + +function validateStream (data, k, val) { + if (!(val instanceof Stream)) { + return false + } + data[k] = val +} + +module.exports = { + String: { type: String, validate: validateString }, + Boolean: { type: Boolean, validate: validateBoolean }, + url: { type: url, validate: validateUrl }, + Number: { type: Number, validate: validateNumber }, + path: { type: path, validate: validatePath }, + Stream: { type: Stream, validate: validateStream }, + Date: { type: Date, validate: validateDate }, + Array: { type: Array }, +} diff --git a/node_modules/nopt/package.json b/node_modules/nopt/package.json index f12fb3374b322..f03f367b2893b 100644 --- a/node_modules/nopt/package.json +++ b/node_modules/nopt/package.json @@ -26,14 +26,13 @@ }, "devDependencies": { "@npmcli/eslint-config": "^4.0.0", - "@npmcli/template-oss": "4.8.0", + "@npmcli/template-oss": "4.11.0", "tap": "^16.3.0" }, "tap": { - "lines": 87, - "functions": 91, - "branches": 81, - "statements": 87, + "lines": 91, + "branches": 87, + "statements": 91, "nyc-arg": [ "--exclude", "tap-snapshots/**" @@ -49,6 +48,6 @@ "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", "windowsCI": false, - "version": "4.8.0" + "version": "4.11.0" } } diff --git a/package-lock.json b/package-lock.json index a3ca873e9c8f9..905364f98d0b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -125,7 +125,7 @@ "mkdirp": "^1.0.4", "ms": "^2.1.2", "node-gyp": "^9.3.0", - "nopt": "^7.0.0", + "nopt": "file:../nopt/nopt-7.0.0.tgz", "npm-audit-report": "^4.0.0", "npm-install-checks": "^6.0.0", "npm-package-arg": "^10.1.0", @@ -8759,6 +8759,8 @@ }, "node_modules/nopt": { "version": "7.0.0", + "resolved": "file:../nopt/nopt-7.0.0.tgz", + "integrity": "sha512-p9SOrouDUDnMJmstko22XNSUILqpf/wBlG7XAETuEsS71rqLFbSlOPpaFC8xoKIkwVb7EyBP4+qhzsX4XDx5LQ==", "inBundle": true, "license": "ISC", "dependencies": { @@ -13959,8 +13961,9 @@ }, "node_modules/which": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.0.tgz", + "integrity": "sha512-nla//68K9NU6yRiwDY/Q8aU6siKlSs64aEC7+IV56QoAuyQT2ovsJcgGYGyqMOmI/CGN1BOR6mM5EN0FBO+zyQ==", "inBundle": true, - "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -14364,11 +14367,12 @@ "dependencies": { "@npmcli/map-workspaces": "^3.0.0", "ini": "^3.0.0", - "nopt": "^7.0.0", + "nopt": "file:../../../nopt/nopt-7.0.0.tgz", "proc-log": "^3.0.0", "read-package-json-fast": "^3.0.0", "semver": "^7.3.5", - "walk-up-path": "^1.0.0" + "walk-up-path": "^1.0.0", + "which": "^3.0.0" }, "devDependencies": { "@npmcli/eslint-config": "^4.0.0", diff --git a/package.json b/package.json index ea6b3febbf6fa..982428da042c3 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "mkdirp": "^1.0.4", "ms": "^2.1.2", "node-gyp": "^9.3.0", - "nopt": "^7.0.0", + "nopt": "file:../nopt/nopt-7.0.0.tgz", "npm-audit-report": "^4.0.0", "npm-install-checks": "^6.0.0", "npm-package-arg": "^10.1.0", diff --git a/tap-snapshots/test/lib/docs.js.test.cjs b/tap-snapshots/test/lib/docs.js.test.cjs index 08d908688fdd7..6b5040106a4d5 100644 --- a/tap-snapshots/test/lib/docs.js.test.cjs +++ b/tap-snapshots/test/lib/docs.js.test.cjs @@ -686,7 +686,7 @@ systems. #### \`browser\` * Default: OS X: \`"open"\`, Windows: \`"start"\`, Others: \`"xdg-open"\` -* Type: null, Boolean, or String +* Type: class BooleanOrString {} or Boolean The browser that is called by npm commands to open websites. diff --git a/test/fixtures/mock-npm.js b/test/fixtures/mock-npm.js index 2cada1354878c..942298c5a2dc5 100644 --- a/test/fixtures/mock-npm.js +++ b/test/fixtures/mock-npm.js @@ -2,11 +2,13 @@ const os = require('os') const fs = require('fs').promises const path = require('path') const tap = require('tap') +const { EnvKeys, ProcessKeys } = require('@npmcli/config') const errorMessage = require('../../lib/utils/error-message') const mockLogs = require('./mock-logs') const mockGlobals = require('./mock-globals') const tmock = require('./tmock') const defExitCode = process.exitCode +const npmRoot = path.resolve(__dirname, '../../') const changeDir = (dir) => { if (dir) { @@ -149,18 +151,23 @@ const setupMockNpm = async (t, { // These are globals manipulated by npm itself that we need to reset to their // original values between tests - const npmEnvs = Object.keys(process.env).filter(k => k.startsWith('npm_')) mockGlobals(t, { process: { - title: process.title, - execPath: process.execPath, + ...['title', ...ProcessKeys].reduce((acc, k) => { + acc[k] = process[k] + return acc + }, {}), env: { - NODE_ENV: process.env.NODE_ENV, - COLOR: process.env.COLOR, + ...EnvKeys.reduce((acc, k) => { + // XXX: we could ensure an original value for all these configs if we wanted + // to normalize them across all tests and require tests set them explicitly + acc[k] = process.env[k] + return acc + }, {}), // further, these are npm controlled envs that we need to zero out before - // before the test. setting them to undefined ensures they are not set and + // the test. setting them to undefined ensures they are not set and // also returned to their original value after the test - ...npmEnvs.reduce((acc, k) => { + ...Object.keys(process.env).filter(k => k.startsWith('npm_')).reduce((acc, k) => { acc[k] = undefined return acc }, {}), @@ -198,7 +205,7 @@ const setupMockNpm = async (t, { cache: dirs.cache, } - const { argv, env, config } = Object.entries({ ...defaultConfigs, ...withDirs(_config) }) + const { argv, env } = Object.entries({ ...defaultConfigs, ...withDirs(_config) }) .reduce((acc, [key, value]) => { // nerfdart configs passed in need to be set via env var instead of argv // and quoted with `"` so mock globals will ignore that it contains dots @@ -208,9 +215,8 @@ const setupMockNpm = async (t, { const values = [].concat(value) acc.argv.push(...values.flatMap(v => [`--${key}`, v.toString()])) } - acc.config[key] = value return acc - }, { argv: [...rawArgv], env: {}, config: {} }) + }, { argv: [...rawArgv], env: {} }) mockGlobals(t, { 'process.env.HOME': dirs.home, @@ -227,17 +233,9 @@ const setupMockNpm = async (t, { init, load, mocks: withDirs(mocks), - npm: { argv, excludeNpmCwd: true, ...withDirs(npmOpts) }, + npm: { argv, npmRoot, cwdRoot: dir, ...withDirs(npmOpts) }, }) - if (config.omit?.includes('prod')) { - // XXX(HACK): --omit=prod is not a valid config according to the definitions but - // it was being hacked in via flatOptions for older tests so this is to - // preserve that behavior and reduce churn in the snapshots. this should be - // removed or fixed in the future - npm.flatOptions.omit.push('prod') - } - t.teardown(() => { if (npm) { npm.unload() diff --git a/test/lib/utils/completion/installed-deep.js b/test/lib/utils/completion/installed-deep.js index fa39f0f0734b8..31b05738416df 100644 --- a/test/lib/utils/completion/installed-deep.js +++ b/test/lib/utils/completion/installed-deep.js @@ -1,31 +1,6 @@ -const { resolve } = require('path') const t = require('tap') const installedDeep = require('../../../../lib/utils/completion/installed-deep.js') - -let prefix -let globalDir = 'MISSING_GLOBAL_DIR' -const _flatOptions = { - depth: Infinity, - global: false, - workspacesEnabled: true, - get prefix () { - return prefix - }, -} -const npm = { - flatOptions: _flatOptions, - get prefix () { - return _flatOptions.prefix - }, - get globalDir () { - return globalDir - }, - config: { - get (key) { - return _flatOptions[key] - }, - }, -} +const mockNpm = require('../../../fixtures/mock-npm') const fixture = { 'package.json': JSON.stringify({ @@ -152,16 +127,20 @@ const globalFixture = { }, } -t.test('get list of package names', async t => { - const fix = t.testdir({ - local: fixture, - global: globalFixture, +const mockDeep = async (t, config) => { + const mock = await mockNpm(t, { + localPrefixDir: fixture, + globalPrefixDir: globalFixture, + config, }) - prefix = resolve(fix, 'local') - globalDir = resolve(fix, 'global/node_modules') + const res = await installedDeep(mock.npm) + + return res +} - const res = await installedDeep(npm, null) +t.test('get list of package names', async t => { + const res = await mockDeep(t) t.same( res, [ @@ -178,17 +157,7 @@ t.test('get list of package names', async t => { }) t.test('get list of package names as global', async t => { - const fix = t.testdir({ - local: fixture, - global: globalFixture, - }) - - prefix = resolve(fix, 'local') - globalDir = resolve(fix, 'global/node_modules') - - _flatOptions.global = true - - const res = await installedDeep(npm, null) + const res = await mockDeep(t, { global: true }) t.same( res, [ @@ -198,22 +167,10 @@ t.test('get list of package names as global', async t => { ], 'should return list of global packages with no extra flags' ) - _flatOptions.global = false - t.end() }) t.test('limit depth', async t => { - const fix = t.testdir({ - local: fixture, - global: globalFixture, - }) - - prefix = resolve(fix, 'local') - globalDir = resolve(fix, 'global/node_modules') - - _flatOptions.depth = 0 - - const res = await installedDeep(npm, null) + const res = await mockDeep(t, { depth: 0 }) t.same( res, [ @@ -228,23 +185,10 @@ t.test('limit depth', async t => { ], 'should print only packages up to the specified depth' ) - _flatOptions.depth = 0 - t.end() }) t.test('limit depth as global', async t => { - const fix = t.testdir({ - local: fixture, - global: globalFixture, - }) - - prefix = resolve(fix, 'local') - globalDir = resolve(fix, 'global/node_modules') - - _flatOptions.global = true - _flatOptions.depth = 0 - - const res = await installedDeep(npm, null) + const res = await mockDeep(t, { depth: 0, global: true }) t.same( res, [ @@ -255,7 +199,4 @@ t.test('limit depth as global', async t => { ], 'should reorder so that packages above that level depth goes last' ) - _flatOptions.global = false - _flatOptions.depth = 0 - t.end() }) diff --git a/test/lib/utils/completion/installed-shallow.js b/test/lib/utils/completion/installed-shallow.js index 5a65b6b6bfaef..870e4c7c2fcd0 100644 --- a/test/lib/utils/completion/installed-shallow.js +++ b/test/lib/utils/completion/installed-shallow.js @@ -1,13 +1,10 @@ const t = require('tap') -const { resolve } = require('path') const installed = require('../../../../lib/utils/completion/installed-shallow.js') - -const flatOptions = { global: false } -const npm = { flatOptions } +const mockNpm = require('../../../fixtures/mock-npm') t.test('global not set, include globals with -g', async t => { - const dir = t.testdir({ - global: { + const { npm } = await mockNpm(t, { + globalPrefixDir: { node_modules: { x: {}, '@scope': { @@ -15,7 +12,7 @@ t.test('global not set, include globals with -g', async t => { }, }, }, - local: { + localPrefixDir: { node_modules: { a: {}, '@scope': { @@ -23,10 +20,8 @@ t.test('global not set, include globals with -g', async t => { }, }, }, + config: { global: false }, }) - npm.globalDir = resolve(dir, 'global/node_modules') - npm.localDir = resolve(dir, 'local/node_modules') - flatOptions.global = false const opt = { conf: { argv: { remain: [] } } } const res = await installed(npm, opt) t.strictSame(res.sort(), [ @@ -35,12 +30,11 @@ t.test('global not set, include globals with -g', async t => { 'a', '@scope/b', ].sort()) - t.end() }) t.test('global set, include globals and not locals', async t => { - const dir = t.testdir({ - global: { + const { npm } = await mockNpm(t, { + globalPrefixDir: { node_modules: { x: {}, '@scope': { @@ -48,7 +42,7 @@ t.test('global set, include globals and not locals', async t => { }, }, }, - local: { + localPrefixDir: { node_modules: { a: {}, '@scope': { @@ -56,22 +50,19 @@ t.test('global set, include globals and not locals', async t => { }, }, }, + config: { global: true }, }) - npm.globalDir = resolve(dir, 'global/node_modules') - npm.localDir = resolve(dir, 'local/node_modules') - flatOptions.global = true const opt = { conf: { argv: { remain: [] } } } const res = await installed(npm, opt) t.strictSame(res.sort(), [ '@scope/y', 'x', ].sort()) - t.end() }) t.test('more than 3 items in argv, skip it', async t => { - const dir = t.testdir({ - global: { + const { npm } = await mockNpm(t, { + globalPrefixDir: { node_modules: { x: {}, '@scope': { @@ -79,7 +70,7 @@ t.test('more than 3 items in argv, skip it', async t => { }, }, }, - local: { + localPrefixDir: { node_modules: { a: {}, '@scope': { @@ -87,12 +78,9 @@ t.test('more than 3 items in argv, skip it', async t => { }, }, }, + config: { global: false }, }) - npm.globalDir = resolve(dir, 'global/node_modules') - npm.localDir = resolve(dir, 'local/node_modules') - flatOptions.global = false const opt = { conf: { argv: { remain: [1, 2, 3, 4, 5, 6] } } } const res = await installed(npm, opt) t.strictSame(res, null) - t.end() }) diff --git a/test/lib/utils/config/definitions.js b/test/lib/utils/config/definitions.js index 0cae6a6da6bd3..2c10139938266 100644 --- a/test/lib/utils/config/definitions.js +++ b/test/lib/utils/config/definitions.js @@ -7,7 +7,7 @@ const pkg = require('../../../../package.json') // have to fake the node version, or else it'll only pass on this one mockGlobals(t, { 'process.version': 'v14.8.0', 'process.env.NODE_ENV': undefined }) -const mockDefs = (mocks = {}) => tmock(t, '{LIB}/utils/config/definitions.js', mocks) +const mockDefs = (mocks = {}) => tmock(t, '{LIB}/utils/config/definitions.js', mocks).definitions const isWin = (isWindows) => ({ '{LIB}/utils/is-windows.js': { isWindows } }) diff --git a/test/lib/utils/config/index.js b/test/lib/utils/config/index.js index 90931a96d7aa2..cb323e4b716cd 100644 --- a/test/lib/utils/config/index.js +++ b/test/lib/utils/config/index.js @@ -1,6 +1,6 @@ const t = require('tap') const config = require('../../../../lib/utils/config/index.js') -const definitions = require('../../../../lib/utils/config/definitions.js') +const { definitions } = require('../../../../lib/utils/config/definitions.js') const mockGlobals = require('../../../fixtures/mock-globals.js') t.test('defaults', t => { diff --git a/test/lib/utils/update-notifier.js b/test/lib/utils/update-notifier.js index e7830e6d9d66e..3a05f95aa7926 100644 --- a/test/lib/utils/update-notifier.js +++ b/test/lib/utils/update-notifier.js @@ -1,10 +1,8 @@ const t = require('tap') +const { basename } = require('path') const tmock = require('../../fixtures/tmock') +const mockNpm = require('../../../fixtures/mock-npm') -let ciMock = {} -const flatOptions = { global: false, cache: t.testdir() + '/_cacache' } - -const MANIFEST_REQUEST = [] const CURRENT_VERSION = '123.420.69' const CURRENT_MAJOR = '122.420.69' const CURRENT_MINOR = '123.419.69' @@ -15,81 +13,82 @@ const NEXT_PATCH = '123.421.69' const CURRENT_BETA = '124.0.0-beta.99999' const HAVE_BETA = '124.0.0-beta.0' -let PACOTE_ERROR = null -const pacote = { - manifest: async (spec, opts) => { - if (!spec.match(/^npm@/)) { - process.exit(1) - } - MANIFEST_REQUEST.push(spec) - if (PACOTE_ERROR) { - throw PACOTE_ERROR - } - - return { - version: - spec === 'npm@latest' - ? CURRENT_VERSION - : /-/.test(spec) - ? CURRENT_BETA - : NEXT_VERSION, - } - }, -} - -const defaultNpm = { - flatOptions, - version: CURRENT_VERSION, - config: { get: k => k !== 'global' }, - command: 'view', - argv: ['npm'], -} - -const { basename } = require('path') - -let STAT_ERROR = null -let STAT_MTIME = null -let WRITE_ERROR = null -const fs = { - ...require('fs'), - stat: (path, cb) => { - if (basename(path) !== '_update-notifier-last-checked') { - process.exit(1) - } - process.nextTick(() => cb(STAT_ERROR, { mtime: new Date(STAT_MTIME) })) - }, - writeFile: (path, content, cb) => { - if (content !== '') { - process.exit(1) - } - if (basename(path) !== '_update-notifier-last-checked') { - process.exit(1) - } - process.nextTick(() => cb(WRITE_ERROR)) - }, -} - -t.afterEach(() => { - MANIFEST_REQUEST.length = 0 - STAT_ERROR = null - PACOTE_ERROR = null - STAT_MTIME = null - WRITE_ERROR = null -}) - -const runUpdateNotifier = async ({ color = true, ...npmOptions } = {}) => { - const _npm = { ...defaultNpm, ...npmOptions, logColor: color } - return tmock(t, '{LIB}/utils/update-notifier.js', { - 'ci-info': ciMock, - pacote, - fs, - })(_npm) +const runUpdateNotifier = async (t, { + STAT_ERROR, + WRITE_ERROR, + PACOTE_ERROR, + STAT_MTIME, + mocks = {}, + color = true, + command = null, + exec= [], + ...npmOptions +} = {}) => { + const mockFs = { + ...require('fs/promises'), + stat: async (path) => { + if (basename(path) !== '_update-notifier-last-checked') { + t.fail('no stat allowed for non upate notifier files') + } + if (STAT_ERROR) { + throw STAT_ERROR + } + return { mtime: new Date(STAT_MTIME) } + }, + writeFile: (path, content) => { + if (content !== '') { + t.fail('no write file content allowed') + } + if (basename(path) !== '_update-notifier-last-checked') { + t.fail('no writefile allowed for non upate notifier files') + } + if (WRITE_ERROR) { + throw WRITE_ERROR + } + }, + } + + const MANIFEST_REQUEST = [] + const mockPacote = { + manifest: async (spec) => { + if (!spec.match(/^npm@/)) { + t.fail('no pacote manifest allowed for non npm packages') + } + MANIFEST_REQUEST.push(spec) + if (PACOTE_ERROR) { + throw PACOTE_ERROR + } + const version = spec === 'npm@latest' ? CURRENT_VERSION : /-/.test(spec) ? + CURRENT_BETA : + NEXT_VERSION + return { version } + }, + } + + const mock = await mockNpm(t, { + command, + mocks: { + pacote: mockPacote, + fs: mockFs, + ...mocks, + }, + }) + + if (mock[command]) { + await mock[command].exec(exec) + } + + const updateNotifier = tmock(t, '{LIB}/utils/update-notifier.js', { + pacote: mockPacote, + fs: mockFs, + ...mocks, + }) } t.test('situations in which we do not notify', t => { t.test('nothing to do if notifier disabled', async t => { t.equal( - await runUpdateNotifier({ + await runUpdateNotifier(t, { config: { get: k => k !== 'update-notifier' }, }), null @@ -99,7 +98,7 @@ t.test('situations in which we do not notify', t => { t.test('do not suggest update if already updating', async t => { t.equal( - await runUpdateNotifier({ + await runUpdateNotifier(t, { flatOptions: { ...flatOptions, global: true }, command: 'install', argv: ['npm'], @@ -111,7 +110,7 @@ t.test('situations in which we do not notify', t => { t.test('do not suggest update if already updating with spec', async t => { t.equal( - await runUpdateNotifier({ + await runUpdateNotifier(t, { flatOptions: { ...flatOptions, global: true }, command: 'install', argv: ['npm@latest'], @@ -122,31 +121,31 @@ t.test('situations in which we do not notify', t => { }) t.test('do not update if same as latest', async t => { - t.equal(await runUpdateNotifier(), null) + t.equal(await runUpdateNotifier(t), null) t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version') }) t.test('check if stat errors (here for coverage)', async t => { STAT_ERROR = new Error('blorg') - t.equal(await runUpdateNotifier(), null) + t.equal(await runUpdateNotifier(t), null) t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version') }) t.test('ok if write errors (here for coverage)', async t => { WRITE_ERROR = new Error('grolb') - t.equal(await runUpdateNotifier(), null) + t.equal(await runUpdateNotifier(t), null) t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version') }) t.test('ignore pacote failures (here for coverage)', async t => { PACOTE_ERROR = new Error('pah-KO-tchay') - t.equal(await runUpdateNotifier(), null) + t.equal(await runUpdateNotifier(t), null) t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version') }) t.test('do not update if newer than latest, but same as next', async t => { - t.equal(await runUpdateNotifier({ version: NEXT_VERSION }), null) + t.equal(await runUpdateNotifier(t, { version: NEXT_VERSION }), null) const reqs = ['npm@latest', `npm@^${NEXT_VERSION}`] t.strictSame(MANIFEST_REQUEST, reqs, 'requested latest and next versions') }) t.test('do not update if on the latest beta', async t => { - t.equal(await runUpdateNotifier({ version: CURRENT_BETA }), null) + t.equal(await runUpdateNotifier(t, { version: CURRENT_BETA }), null) const reqs = [`npm@^${CURRENT_BETA}`] t.strictSame(MANIFEST_REQUEST, reqs, 'requested latest and next versions') }) @@ -156,21 +155,21 @@ t.test('situations in which we do not notify', t => { ciMock = {} }) ciMock = { isCI: true, name: 'something' } - t.equal(await runUpdateNotifier(), null) + t.equal(await runUpdateNotifier(t), null) t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests') }) t.test('only check weekly for GA releases', async t => { // One week (plus five minutes to account for test environment fuzziness) STAT_MTIME = Date.now() - 1000 * 60 * 60 * 24 * 7 + 1000 * 60 * 5 - t.equal(await runUpdateNotifier(), null) + t.equal(await runUpdateNotifier(t), null) t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests') }) t.test('only check daily for betas', async t => { // One day (plus five minutes to account for test environment fuzziness) STAT_MTIME = Date.now() - 1000 * 60 * 60 * 24 + 1000 * 60 * 5 - t.equal(await runUpdateNotifier({ version: HAVE_BETA }), null) + t.equal(await runUpdateNotifier(t, { version: HAVE_BETA }), null) t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests') }) @@ -180,9 +179,9 @@ t.test('situations in which we do not notify', t => { t.test('notification situations', t => { t.test('new beta available', async t => { const version = HAVE_BETA - t.matchSnapshot(await runUpdateNotifier({ version }), 'color') + t.matchSnapshot(await runUpdateNotifier(t, { version }), 'color') t.matchSnapshot( - await runUpdateNotifier({ version, color: false }), + await runUpdateNotifier(t, { version, color: false }), 'no color' ) t.strictSame(MANIFEST_REQUEST, [`npm@^${version}`, `npm@^${version}`]) @@ -190,9 +189,9 @@ t.test('notification situations', t => { t.test('patch to next version', async t => { const version = NEXT_PATCH - t.matchSnapshot(await runUpdateNotifier({ version }), 'color') + t.matchSnapshot(await runUpdateNotifier(t, { version }), 'color') t.matchSnapshot( - await runUpdateNotifier({ version, color: false }), + await runUpdateNotifier(t, { version, color: false }), 'no color' ) t.strictSame(MANIFEST_REQUEST, [ @@ -205,9 +204,9 @@ t.test('notification situations', t => { t.test('minor to next version', async t => { const version = NEXT_MINOR - t.matchSnapshot(await runUpdateNotifier({ version }), 'color') + t.matchSnapshot(await runUpdateNotifier(t, { version }), 'color') t.matchSnapshot( - await runUpdateNotifier({ version, color: false }), + await runUpdateNotifier(t, { version, color: false }), 'no color' ) t.strictSame(MANIFEST_REQUEST, [ @@ -220,9 +219,9 @@ t.test('notification situations', t => { t.test('patch to current', async t => { const version = CURRENT_PATCH - t.matchSnapshot(await runUpdateNotifier({ version }), 'color') + t.matchSnapshot(await runUpdateNotifier(t, { version }), 'color') t.matchSnapshot( - await runUpdateNotifier({ version, color: false }), + await runUpdateNotifier(t, { version, color: false }), 'no color' ) t.strictSame(MANIFEST_REQUEST, ['npm@latest', 'npm@latest']) @@ -230,9 +229,9 @@ t.test('notification situations', t => { t.test('minor to current', async t => { const version = CURRENT_MINOR - t.matchSnapshot(await runUpdateNotifier({ version }), 'color') + t.matchSnapshot(await runUpdateNotifier(t, { version }), 'color') t.matchSnapshot( - await runUpdateNotifier({ version, color: false }), + await runUpdateNotifier(t, { version, color: false }), 'no color' ) t.strictSame(MANIFEST_REQUEST, ['npm@latest', 'npm@latest']) @@ -240,9 +239,9 @@ t.test('notification situations', t => { t.test('major to current', async t => { const version = CURRENT_MAJOR - t.matchSnapshot(await runUpdateNotifier({ version }), 'color') + t.matchSnapshot(await runUpdateNotifier(t, { version }), 'color') t.matchSnapshot( - await runUpdateNotifier({ version, color: false }), + await runUpdateNotifier(t, { version, color: false }), 'no color' ) t.strictSame(MANIFEST_REQUEST, ['npm@latest', 'npm@latest']) diff --git a/workspaces/config/lib/config-data.js b/workspaces/config/lib/config-data.js new file mode 100644 index 0000000000000..6ee4da8f79151 --- /dev/null +++ b/workspaces/config/lib/config-data.js @@ -0,0 +1,245 @@ +const nopt = require('nopt').lib +const log = require('proc-log') +const envReplace = require('./env-replace') +const nerfDart = require('./nerf-dart') +const { typeDefs, Types, getType } = require('./type-defs') +const ini = require('ini') + +const SYMBOLS = { + set: Symbol('set'), + delete: Symbol('delete'), +} + +const typeDescription = type => { + if (Array.isArray(type)) { + return type.map(t => typeDescription(t)) + } + const def = getType(type) + return def ? def.description ?? def.typeDescription : type +} + +class ConfigData extends Map { + #type = null + + #data = null + #source = null + #valid = true + + #parent = null + #deprecated = null + #shorthands = null + #types = null + #env = null + + static get mutateSymbols () { + return SYMBOLS + } + + constructor (type, { parent, deprecated, shorthands, types, env }) { + super() + this.#type = type + + this.#parent = parent + this.#deprecated = deprecated + this.#shorthands = shorthands + this.#types = types + this.#env = env + + for (const key of Object.keys(SYMBOLS)) { + this[key] = () => { + throw new Error(`attempted to call \`${key}\` directly on ConfigData:${this.#type.where}`) + } + } + } + + get where () { + return this.#type.where + } + + get source () { + return this.#source + } + + get data () { + if (this.#data) { + return this.#data + } + this.#data = Object.fromEntries([...this.entries()]) + return this.#data + } + + toString () { + return ini.stringify(this.data).trim() + } + + [SYMBOLS.set] (key, value) { + // XXX(npm9+) make this throw an error + if (!this.#type.allowDeprecated && this.#deprecated[key]) { + log.warn('config', key, this.#deprecated[key]) + } + // this is now dirty, the next call to this.valid will have to check it + this.#valid = false + // data will also be regnerated + this.#data = null + return super.set(key, value) + } + + [SYMBOLS.delete] (key) { + this.#data = null + return super.delete(key) + } + + clear () { + throw new Error(`attempted to call \`clear\` directly on ConfigData:${this.#type.where}`) + } + + load (data, error, source = this.#type.defaultSource) { + this.#data = null + + if (this.source) { + throw new Error(`Double load ${this.where} ${this.source}`) + } + + if (error) { + if (error.code !== 'ENOENT') { + log.verbose('config', `error loading ${this.where} config`, error) + } + return + } + + const result = {} + let cleanData = null + + if (Array.isArray(data)) { + const { argv, ...parsedData } = nopt.nopt(data, { + typeDefs, + types: this.#types, + shorthands: this.#shorthands, + invalidHandler: (...args) => this.#invalidHandler(...args), + }) + result.argv = argv + cleanData = parsedData + } else if (data) { + if (typeof data === 'string') { + data = ini.parse(data) + } + cleanData = {} + for (const [k, v] of Object.entries(data)) { + cleanData[envReplace(k, this.#env)] = typeof v === 'string' ? envReplace(v, this.#env) : v + } + this.#clean(cleanData) + } + + if (cleanData) { + // this.set reset #valid so we check the status before and reset + // it after setting all the properties because we just validated + // everything with nopt + const isValid = this.#valid + for (const [k, v] of Object.entries(cleanData)) { + this[SYMBOLS.set](k, v) + } + this.#valid = isValid + return result + } + + this.#source = `(${source}, ignored)` + } + + validate () { + if (this.#valid) { + return true + } + + this.#clean(this.data) + + if (this.#type.validateAuth) { + const problems = [] + // after validating everything else, we look for old auth configs we no longer support + // if these keys are found, we build up a list of them and the appropriate action and + // attach it as context on the thrown error + + // first, keys that should be removed + for (const key of ['_authtoken', '-authtoken']) { + if (this.get(key)) { + problems.push({ action: 'delete', key }) + } + } + + // NOTE we pull registry without restricting to the current 'where' because we want to + // suggest scoping things to the registry they would be applied to, which is the default + // regardless of where it was defined + const nerfedReg = nerfDart(this.#parent.getData('registry')) + + // keys that should be nerfed but currently are not + for (const key of ['_auth', '_authToken', 'username', '_password']) { + if (this.get(key)) { + // username and _password must both exist in the same file to be recognized correctly + if (key === 'username' && !this.get('_password')) { + problems.push({ action: 'delete', key }) + } else if (key === '_password' && !this.get('username')) { + problems.push({ action: 'delete', key }) + } else { + problems.push({ action: 'rename', from: key, to: `${nerfedReg}:${key}` }) + } + } + } + + if (problems.length) { + this.#valid = false + return { + problems: { + auth: problems.map((p) => { + p.where = this.#type.where + return p + }), + }, + } + } + } + + return this.#valid + } + + #clean (d) { + nopt.clean(d, { + typeDefs, + types: this.#types, + invalidHandler: (...args) => this.#invalidHandler(...args), + }) + } + + #invalidHandler (k, val, type) { + this.#valid = false + + if (Array.isArray(type)) { + if (type.includes(Types.url)) { + type = Types.url + } else /* istanbul ignore next */ if (type.includes(Types.path)) { + /* no actual configs matching this, but + * path types SHOULD be handled this way, like URLs, for the + * same reason */ + type = Types.path + } + } + + const typeDesc = [].concat(typeDescription(type)) + const oneOrMore = typeDesc.includes(Types.Array) + const mustBe = typeDesc.filter(m => m !== Types.Array) + + const oneOf = mustBe.length === 1 && oneOrMore ? 'one or more' + : mustBe.length > 1 && oneOrMore ? 'one or more of:' + : mustBe.length > 1 ? 'one of:' + : '' + const desc = mustBe.length === 1 ? mustBe[0] : mustBe.filter(m => m !== Types.Array) + .map(n => typeof n === 'string' ? n : JSON.stringify(n)) + .join(', ') + + log.warn('config', + `invalid item \`${k}\`.`, + `Must be ${oneOf}`.trim(), + `${desc}. Received value of \`${val}\`` + ) + } +} + +module.exports = ConfigData diff --git a/workspaces/config/lib/config-types.js b/workspaces/config/lib/config-types.js new file mode 100644 index 0000000000000..ad9597a5aa052 --- /dev/null +++ b/workspaces/config/lib/config-types.js @@ -0,0 +1,147 @@ +const ConfigData = require('./config-data') + +// this is in order from least -> most precedence +const ConfTypesList = [ + { where: 'default', source: 'default values', allowDeprecated: true }, + { where: 'builtin' }, + { where: 'global', validateAuth: true }, + { where: 'user', validateAuth: true }, + { where: 'project', validateAuth: true }, + { where: 'env', source: 'environment' }, + { where: 'cli', source: 'command line options' }, +] + +// an enum to export and use to make using `where` not rely on strings +const ConfTypes = ConfTypesList.reduce((acc, type) => { + acc[type.where] = type.where + return acc +}, {}) + +class ConfigDataTypes extends Map { + #frozen = false + + #list = [] + #indexes = {} + #revList = [] + #revIndexes = {} + + #flat = null + #derived = {} + #effects = {} + #dirty = new Set() + #flatten = null + + static get ConfTypes () { + return ConfTypes + } + + constructor ({ flatten, derived, effects, ...opts }) { + super() + + this.#flatten = flatten + this.#derived = derived + this.#effects = effects + + for (const type of ConfTypesList) { + const data = new ConfigData(type, { ...opts, parent: this }) + this.set(type.where, data) + this.#indexes[type.where] = this.#list.push(type.where) - 1 + this.#revIndexes[type.where] = ConfTypesList.length - this.#revList.unshift(type.where) + } + + this.#frozen = true + + // symbols for mutating config data are shared here so that no method is exposed + // that can mutate a types config data execpt for these + for (const [key, symbol] of Object.entries(ConfigData.mutateSymbols)) { + this[`${key}Data`] = (...args) => this.#mutateData(symbol, ...args) + } + } + + get data () { + if (this.#flat) { + return this.#flat + } + process.emit('time', `config:flatten`) + this.#flat = {} + // walk from least priority to highest + for (const { data } of this.values()) { + //this.#flatten(data, this.#flat) + } + process.emit('timeEnd', `config:flatten`) + // return Object.freeze(this.#flat) + return this.#flat + } + + get (where) { + if (!this.has(where)) { + throw new Error(`Cannot get invalid config type of \`${where}\``) + } + return super.get(where) + } + + set (key, value) { + if (this.#frozen) { + throw new Error(`cannot change config types after init`) + } + return super.set(key, value) + } + + delete () { + throw new Error('cannot change config types after init') + } + + clear () { + throw new Error('cannot change config types after init') + } + + // defaults -> cli + * values (startWhere) { + const index = startWhere ? this.#indexes[startWhere] : 0 + const locations = index ? this.#list.slice(index) : this.#list + for (const where of locations) { + yield this.get(where) + } + } + + // cli -> defaults + * reverseValues (startWhere) { + const index = startWhere ? this.#revIndexes[startWhere] : 0 + const locations = index ? this.#revList.slice(index) : this.#revList + for (const where of locations) { + yield this.get(where) + } + } + + find (key) { + for (const config of this.reverseValues()) { + if (config.has(key)) { + return config.where + } + } + return null + } + + getData (where, key) { + if (where === null) { + for (const config of this.reverseValues()) { + if (config.has(key)) { + return config.get(key) + } + } + return + } + + return this.get(where).get(key) + } + + #mutateData(symbol, where, key) { + const effected = this.#effects(key) + // the flat options are invalidated, regenerate next time they're needed + this.#flat = null + return this.get(where)[symbol](...args) + } + } +} + +module.exports = ConfigDataTypes diff --git a/workspaces/config/lib/credentials.js b/workspaces/config/lib/credentials.js new file mode 100644 index 0000000000000..ed4361705cc12 --- /dev/null +++ b/workspaces/config/lib/credentials.js @@ -0,0 +1,140 @@ +const nerfDart = require('./nerf-dart.js') +const { ConfTypes } = require('./config-data') + +class Credentials { + #config = null + + constructor (config) { + this.#config = config + } + + // all mutations for credentials occur in the user config + #set (k, v) { + this.#set(k, v, ConfTypes.user) + } + + #delete (k) { + this.#delete(k, ConfTypes.user) + } + + clearByURI (uri) { + const nerfed = nerfDart(uri) + const def = nerfDart(this.#config.get('registry')) + if (def === nerfed) { + this.#delete(`-authtoken`) + this.#delete(`_authToken`) + this.#delete(`_authtoken`) + this.#delete(`_auth`) + this.#delete(`_password`) + this.#delete(`username`) + // de-nerf email if it's nerfed to the default registry + const email = this.#config.get(`${nerfed}:email`, ConfTypes.user) + if (email) { + this.#set('email', email) + } + } + this.#delete(`${nerfed}:_authToken`) + this.#delete(`${nerfed}:_auth`) + this.#delete(`${nerfed}:_password`) + this.#delete(`${nerfed}:username`) + this.#delete(`${nerfed}:email`) + this.#delete(`${nerfed}:certfile`) + this.#delete(`${nerfed}:keyfile`) + } + + setByURI (uri, { token, username, password, email, certfile, keyfile }) { + const nerfed = nerfDart(uri) + + // email is either provided, a top level key, or nothing + email = email || this.#config.get('email', ConfTypes.user) + + // field that hasn't been used as documented for a LONG time, + // and as of npm 7.10.0, isn't used at all. We just always + // send auth if we have it, only to the URIs under the nerf dart. + this.#delete(`${nerfed}:always-auth`) + + this.#delete(`${nerfed}:email`) + if (certfile && keyfile) { + this.#set(`${nerfed}:certfile`, certfile) + this.#set(`${nerfed}:keyfile`, keyfile) + // cert/key may be used in conjunction with other credentials, thus no `else` + } + if (token) { + this.#set(`${nerfed}:_authToken`, token) + this.#delete(`${nerfed}:_password`) + this.#delete(`${nerfed}:username`) + } else if (username || password) { + if (!username) { + throw new Error('must include username') + } + if (!password) { + throw new Error('must include password') + } + this.#delete(`${nerfed}:_authToken`) + this.#set(`${nerfed}:username`, username) + // note: not encrypted, no idea why we bothered to do this, but oh well + // protects against shoulder-hacks if password is memorable, I guess? + const encoded = Buffer.from(password, 'utf8').toString('base64') + this.#set(`${nerfed}:_password`, encoded) + } else if (!certfile || !keyfile) { + throw new Error('No credentials to set.') + } + } + + // this has to be a bit more complicated to support legacy data of all forms + getByURI (uri) { + const nerfed = nerfDart(uri) + const def = nerfDart(this.#config.get('registry')) + const creds = {} + + // email is handled differently, it used to always be nerfed and now it never should be + // if it's set nerfed to the default registry, then we copy it to the unnerfed key + // TODO: evaluate removing 'email' from the credentials object returned here + const email = this.#config.get(`${nerfed}:email`) || this.#config.get('email') + if (email) { + if (nerfed === def) { + this.#set('email', email) + } + creds.email = email + } + + const certfileReg = this.#config.get(`${nerfed}:certfile`) + const keyfileReg = this.#config.get(`${nerfed}:keyfile`) + if (certfileReg && keyfileReg) { + creds.certfile = certfileReg + creds.keyfile = keyfileReg + // cert/key may be used in conjunction with other credentials, thus no `return` + } + + const tokenReg = this.#config.get(`${nerfed}:_authToken`) + if (tokenReg) { + creds.token = tokenReg + return creds + } + + const userReg = this.#config.get(`${nerfed}:username`) + const passReg = this.#config.get(`${nerfed}:_password`) + if (userReg && passReg) { + creds.username = userReg + creds.password = Buffer.from(passReg, 'base64').toString('utf8') + const auth = `${creds.username}:${creds.password}` + creds.auth = Buffer.from(auth, 'utf8').toString('base64') + return creds + } + + const authReg = this.#config.get(`${nerfed}:_auth`) + if (authReg) { + const authDecode = Buffer.from(authReg, 'base64').toString('utf8') + const authSplit = authDecode.split(':') + creds.username = authSplit.shift() + creds.password = authSplit.join(':') + creds.auth = authReg + return creds + } + + // at this point, nothing else is usable so just return what we do have + return creds + } +} + +module.exports = Credentials diff --git a/workspaces/config/lib/errors.js b/workspaces/config/lib/errors.js index fa3e20798542a..c0bcc9bee1799 100644 --- a/workspaces/config/lib/errors.js +++ b/workspaces/config/lib/errors.js @@ -2,16 +2,17 @@ class ErrInvalidAuth extends Error { constructor (problems) { - let message = 'Invalid auth configuration found: ' - message += problems.map((problem) => { + const msgs = problems.map((problem) => { if (problem.action === 'delete') { return `\`${problem.key}\` is not allowed in ${problem.where} config` } else if (problem.action === 'rename') { return `\`${problem.from}\` must be renamed to \`${problem.to}\` in ${problem.where} config` } }).join(', ') - message += '\nPlease run `npm config fix` to repair your configuration.`' - super(message) + super( + `Invalid auth configuration found: ${msgs}\n` + + 'Please run `npm config fix` to repair your configuration.' + ) this.code = 'ERR_INVALID_AUTH' this.problems = problems } diff --git a/workspaces/config/lib/index.js b/workspaces/config/lib/index.js index 1ddf267839195..eb194c3d33a7b 100644 --- a/workspaces/config/lib/index.js +++ b/workspaces/config/lib/index.js @@ -1,662 +1,448 @@ // TODO: set the scope config from package.json or explicit cli config const walkUp = require('walk-up-path') -const ini = require('ini') -const nopt = require('nopt') +const which = require('which') const mapWorkspaces = require('@npmcli/map-workspaces') const rpj = require('read-package-json-fast') const log = require('proc-log') - const { resolve, dirname, join } = require('path') const { homedir } = require('os') -const { - readFile, - writeFile, - chmod, - unlink, - stat, - mkdir, -} = require('fs/promises') - -const fileExists = (...p) => stat(resolve(...p)) +const fs = require('fs/promises') +const TypeDefs = require('./type-defs.js') +const nerfDart = require('./nerf-dart.js') +const { setEnv, setProcess, setNpmEnv, sameConfigValue, ...SetEnv } = require('./set-envs.js') +const { ErrInvalidAuth } = require('./errors') +const ConfigTypes = require('./config-types') +const Credentials = require('./credentials.js') +const ConfTypes = ConfigTypes.ConfTypes + +const fileExists = (...p) => fs.stat(resolve(...p)) .then((st) => st.isFile()) .catch(() => false) -const dirExists = (...p) => stat(resolve(...p)) +const dirExists = (...p) => fs.stat(resolve(...p)) .then((st) => st.isDirectory()) .catch(() => false) -const hasOwnProperty = (obj, key) => - Object.prototype.hasOwnProperty.call(obj, key) - // define a custom getter, but turn into a normal prop // if we set it. otherwise it can't be set on child objects -const settableGetter = (obj, key, get) => { - Object.defineProperty(obj, key, { - get, - set (value) { - Object.defineProperty(obj, key, { - value, - configurable: true, - writable: true, - enumerable: true, - }) - }, - configurable: true, - enumerable: true, - }) -} - -const typeDefs = require('./type-defs.js') -const nerfDart = require('./nerf-dart.js') -const envReplace = require('./env-replace.js') -const parseField = require('./parse-field.js') -const typeDescription = require('./type-description.js') -const setEnvs = require('./set-envs.js') - -const { - ErrInvalidAuth, -} = require('./errors.js') - -// types that can be saved back to -const confFileTypes = new Set([ - 'global', - 'user', - 'project', -]) - -const confTypes = new Set([ - 'default', - 'builtin', - ...confFileTypes, - 'env', - 'cli', -]) - -const _loaded = Symbol('loaded') -const _get = Symbol('get') -const _find = Symbol('find') -const _loadObject = Symbol('loadObject') -const _loadFile = Symbol('loadFile') -const _checkDeprecated = Symbol('checkDeprecated') -const _flatten = Symbol('flatten') -const _flatOptions = Symbol('flatOptions') +const settableGetter = (get, ...args) => Object.defineProperty(...args, { + configurable: true, + enumerable: true, + get, + set (value) { + Object.defineProperty(...args, { + value, + configurable: true, + writable: true, + enumerable: true, + }) + }, +}) class Config { - static get typeDefs () { - return typeDefs + static get TypeDefs () { + return TypeDefs + } + + static get Types () { + return TypeDefs.Types } + static get ConfTypes () { + return ConfTypes + } + + static get EnvKeys () { + return [...SetEnv.ALLOWED_ENV_KEYS.values()] + } + + static get ProcessKeys () { + return [...SetEnv.ALLOWED_PROCESS_KEYS.values()] + } + + // required options in constructor + #definitions = null + #npmPath = null + #derived = null + + // options just to override in tests, mostly + #process = null + #argv = null + #env = null + #execPath = null + #platform = null + #cwd = null + #cwdRoot = null + + // set when we load configs + #globalPrefix = null + #localPrefix = null + #localPackage = null + + #loaded = false + #home = null + #parsedArgv = null // from nopt + // built in constructor from definitions + #defaults = {} + // data configs for each config type + #configData = null + #credentials = null + constructor ({ definitions, shorthands, flatten, npmPath, - - // options just to override in tests, mostly - env = process.env, - argv = process.argv, - platform = process.platform, - execPath = process.execPath, - cwd = process.cwd(), - excludeNpmCwd = false, + derived, + + // pass in process to set everything, but also allow + // overriding specific parts of process that are used + process: _process = process, + env = _process.env, + argv = _process.argv, + platform = _process.platform, + execPath = _process.execPath, + cwd = _process.cwd(), + cwdRoot = null, }) { + this.#definitions = definitions + this.#npmPath = npmPath + + this.#process = _process + this.#env = env + this.#argv = argv + this.#platform = platform + this.#execPath = execPath + this.#cwd = cwd + this.#cwdRoot = cwdRoot + + this.#home = this.#env.HOME || homedir() + TypeDefs.typeDefs.path.HOME = this.#home + TypeDefs.typeDefs.path.PLATFORM = this.#platform + // turn the definitions into nopt's weirdo syntax - this.definitions = definitions const types = {} - const defaults = {} - this.deprecated = {} - for (const [key, def] of Object.entries(definitions)) { - defaults[key] = def.default + const deprecated = {} + const effects = {} + for (const [key, def] of Object.entries(this.#definitions)) { + this.#defaults[key] = def.default types[key] = def.type if (def.deprecated) { - this.deprecated[key] = def.deprecated.trim().replace(/\n +/, '\n') + deprecated[key] = def.deprecated.trim().replace(/\n +/, '\n') + } + if (def.derived) { + effects[key] = def.derived } } - - // populated the first time we flatten the object - this[_flatOptions] = null - this[_flatten] = flatten - this.types = types - this.shorthands = shorthands - this.defaults = defaults - - this.npmPath = npmPath - this.argv = argv - this.env = env - this.execPath = execPath - this.platform = platform - this.cwd = cwd - this.excludeNpmCwd = excludeNpmCwd - - // set when we load configs - this.globalPrefix = null - this.localPrefix = null - this.localPackage = null - - // defaults to env.HOME, but will always be *something* - this.home = null - - // set up the prototype chain of config objects - const wheres = [...confTypes] - this.data = new Map() - let parent = null - for (const where of wheres) { - this.data.set(where, parent = new ConfigData(parent)) - } - - this.data.set = () => { - throw new Error('cannot change internal config data structure') - } - this.data.delete = () => { - throw new Error('cannot change internal config data structure') - } - - this.sources = new Map([]) - - this.list = [] - for (const { data } of this.data.values()) { - this.list.unshift(data) - } - Object.freeze(this.list) - - this[_loaded] = false - } - + Object.freeze(this.#definitions) + Object.freeze(this.#defaults) + Object.freeze(types) + Object.freeze(deprecated) + + this.#configData = new ConfigTypes({ + flatten, + deprecated, + shorthands, + effects, + derived, + types, + env: this.#env, + }) + + this.#credentials = new Credentials(this) + } + + // ============================================= + // + // Public/Private Getters + // + // ============================================= get loaded () { - return this[_loaded] + return this.#loaded } get prefix () { - return this[_get]('global') ? this.globalPrefix : this.localPrefix + this.#assertLoaded() + return this.#global ? this.globalPrefix : this.localPrefix } - // return the location where key is found. - find (key) { - if (!this.loaded) { - throw new Error('call config.load() before reading values') - } - return this[_find](key) + get globalPrefix () { + this.#assertLoaded() + return this.#globalPrefix } - [_find] (key) { - // have to look in reverse order - const entries = [...this.data.entries()] - for (let i = entries.length - 1; i > -1; i--) { - const [where, { data }] = entries[i] - if (hasOwnProperty(data, key)) { - return where - } - } - return null + get localPrefix () { + this.#assertLoaded() + return this.#localPrefix } - get (key, where) { - if (!this.loaded) { - throw new Error('call config.load() before reading values') - } - return this[_get](key, where) + get localPackage () { + this.#assertLoaded() + return this.#localPackage } - // we need to get values sometimes, so use this internal one to do so - // while in the process of loading. - [_get] (key, where = null) { - if (where !== null && !confTypes.has(where)) { - throw new Error('invalid config location param: ' + where) - } - const { data } = this.data.get(where || 'cli') - return where === null || hasOwnProperty(data, key) ? data[key] : undefined - } - - set (key, val, where = 'cli') { - if (!this.loaded) { - throw new Error('call config.load() before setting values') - } - if (!confTypes.has(where)) { - throw new Error('invalid config location param: ' + where) - } - this[_checkDeprecated](key) - const { data } = this.data.get(where) - data[key] = val - - // this is now dirty, the next call to this.valid will have to check it - this.data.get(where)[_valid] = null - - // the flat options are invalidated, regenerate next time they're needed - this[_flatOptions] = null + get parsedArgv () { + this.#assertLoaded() + return this.#parsedArgv } get flat () { - if (this[_flatOptions]) { - return this[_flatOptions] - } - - // create the object for flat options passed to deps - process.emit('time', 'config:load:flatten') - this[_flatOptions] = {} - // walk from least priority to highest - for (const { data } of this.data.values()) { - this[_flatten](data, this[_flatOptions]) - } - process.emit('timeEnd', 'config:load:flatten') - - return this[_flatOptions] + this.#assertLoaded() + return this.#configData.data } - delete (key, where = 'cli') { - if (!this.loaded) { - throw new Error('call config.load() before deleting values') - } - if (!confTypes.has(where)) { - throw new Error('invalid config location param: ' + where) + get valid () { + this.#assertLoaded() + for (const conf of this.#configData.values()) { + if (!conf.validate()) { + return false + } } - delete this.data.get(where).data[key] + return true } - async load () { - if (this.loaded) { - throw new Error('attempting to load npm config multiple times') - } - - process.emit('time', 'config:load') - // first load the defaults, which sets the global prefix - process.emit('time', 'config:load:defaults') - this.loadDefaults() - process.emit('timeEnd', 'config:load:defaults') - - // next load the builtin config, as this sets new effective defaults - process.emit('time', 'config:load:builtin') - await this.loadBuiltinConfig() - process.emit('timeEnd', 'config:load:builtin') - - // cli and env are not async, and can set the prefix, relevant to project - process.emit('time', 'config:load:cli') - this.loadCLI() - process.emit('timeEnd', 'config:load:cli') - process.emit('time', 'config:load:env') - this.loadEnv() - process.emit('timeEnd', 'config:load:env') - - // next project config, which can affect userconfig location - process.emit('time', 'config:load:project') - await this.loadProjectConfig() - process.emit('timeEnd', 'config:load:project') - // then user config, which can affect globalconfig location - process.emit('time', 'config:load:user') - await this.loadUserConfig() - process.emit('timeEnd', 'config:load:user') - // last but not least, global config file - process.emit('time', 'config:load:global') - await this.loadGlobalConfig() - process.emit('timeEnd', 'config:load:global') - - // set this before calling setEnvs, so that we don't have to share - // symbols, as that module also does a bunch of get operations - this[_loaded] = true - - // set proper globalPrefix now that everything is loaded - this.globalPrefix = this.get('prefix') - - process.emit('time', 'config:load:setEnvs') - this.setEnvs() - process.emit('timeEnd', 'config:load:setEnvs') - - process.emit('timeEnd', 'config:load') + get credentials () { + return this.#credentials } - loadDefaults () { - this.loadGlobalPrefix() - this.loadHome() - - this[_loadObject]({ - ...this.defaults, - prefix: this.globalPrefix, - }, 'default', 'default values') - - const { data } = this.data.get('default') - - // the metrics-registry defaults to the current resolved value of - // the registry, unless overridden somewhere else. - settableGetter(data, 'metrics-registry', () => this[_get]('registry')) - - // if the prefix is set on cli, env, or userconfig, then we need to - // default the globalconfig file to that location, instead of the default - // global prefix. It's weird that `npm get globalconfig --prefix=/foo` - // returns `/foo/etc/npmrc`, but better to not change it at this point. - settableGetter(data, 'globalconfig', () => resolve(this[_get]('prefix'), 'etc/npmrc')) + get #global () { + return this.#get('global') === true || this.#get('location') === 'global' } - loadHome () { - this.home = this.env.HOME || homedir() + // ============================================= + // + // Get/Set/Find/Delete, etc. + // + // ============================================= + find (key) { + this.#assertLoaded() + return this.#find(key) } - loadGlobalPrefix () { - if (this.globalPrefix) { - throw new Error('cannot load default global prefix more than once') - } - - if (this.env.PREFIX) { - this.globalPrefix = this.env.PREFIX - } else if (this.platform === 'win32') { - // c:\node\node.exe --> prefix=c:\node\ - this.globalPrefix = dirname(this.execPath) - } else { - // /usr/local/bin/node --> prefix=/usr/local - this.globalPrefix = dirname(dirname(this.execPath)) - - // destdir only is respected on Unix - if (this.env.DESTDIR) { - this.globalPrefix = join(this.env.DESTDIR, this.globalPrefix) - } - } + #find (key) { + return this.#configData.find(key) } - loadEnv () { - const conf = Object.create(null) - for (const [envKey, envVal] of Object.entries(this.env)) { - if (!/^npm_config_/i.test(envKey) || envVal === '') { - continue - } - let key = envKey.slice('npm_config_'.length) - if (!key.startsWith('//')) { // don't normalize nerf-darted keys - key = key.replace(/(?!^)_/g, '-') // don't replace _ at the start of the key - .toLowerCase() - } - conf[key] = envVal - } - this[_loadObject](conf, 'env', 'environment') + get (key, where) { + this.#assertLoaded() + return this.#get(key, where) } - loadCLI () { - nopt.invalidHandler = (k, val, type) => - this.invalidHandler(k, val, type, 'command line options', 'cli') - const conf = nopt(this.types, this.shorthands, this.argv) - nopt.invalidHandler = null - this.parsedArgv = conf.argv - delete conf.argv - this[_loadObject](conf, 'cli', 'command line options') + #get (key, where = null) { + return this.#configData.getData(where, key) } - get valid () { - for (const [where, { valid }] of this.data.entries()) { - if (valid === false || valid === null && !this.validate(where)) { - return false - } - } - return true + set (key, val, where) { + this.#assertLoaded() + return this.#set(key, val, where) } - validate (where) { - if (!where) { - let valid = true - const authProblems = [] - - for (const entryWhere of this.data.keys()) { - // no need to validate our defaults, we know they're fine - // cli was already validated when parsed the first time - if (entryWhere === 'default' || entryWhere === 'builtin' || entryWhere === 'cli') { - continue - } - const ret = this.validate(entryWhere) - valid = valid && ret - - if (['global', 'user', 'project'].includes(entryWhere)) { - // after validating everything else, we look for old auth configs we no longer support - // if these keys are found, we build up a list of them and the appropriate action and - // attach it as context on the thrown error - - // first, keys that should be removed - for (const key of ['_authtoken', '-authtoken']) { - if (this.get(key, entryWhere)) { - authProblems.push({ action: 'delete', key, where: entryWhere }) - } - } - - // NOTE we pull registry without restricting to the current 'where' because we want to - // suggest scoping things to the registry they would be applied to, which is the default - // regardless of where it was defined - const nerfedReg = nerfDart(this.get('registry')) - // keys that should be nerfed but currently are not - for (const key of ['_auth', '_authToken', 'username', '_password']) { - if (this.get(key, entryWhere)) { - // username and _password must both exist in the same file to be recognized correctly - if (key === 'username' && !this.get('_password', entryWhere)) { - authProblems.push({ action: 'delete', key, where: entryWhere }) - } else if (key === '_password' && !this.get('username', entryWhere)) { - authProblems.push({ action: 'delete', key, where: entryWhere }) - } else { - authProblems.push({ - action: 'rename', - from: key, - to: `${nerfedReg}:${key}`, - where: entryWhere, - }) - } - } - } - } - } - - if (authProblems.length) { - throw new ErrInvalidAuth(authProblems) - } - - return valid - } else { - const obj = this.data.get(where) - obj[_valid] = true - - nopt.invalidHandler = (k, val, type) => - this.invalidHandler(k, val, type, obj.source, where) - - nopt.clean(obj.data, this.types, this.typeDefs) - - nopt.invalidHandler = null - return obj[_valid] - } + #set (key, val, where = ConfTypes.cli) { + return this.#configData.setData(where, key, val) } - // fixes problems identified by validate(), accepts the 'problems' property from a thrown - // ErrInvalidAuth to avoid having to check everything again - repair (problems) { - if (!problems) { - try { - this.validate() - } catch (err) { - // coverage skipped here because we don't need to test re-throwing an error - // istanbul ignore next - if (err.code !== 'ERR_INVALID_AUTH') { - throw err - } - - problems = err.problems - } finally { - if (!problems) { - problems = [] - } - } - } + delete (key, where) { + this.#assertLoaded() + return this.#delete(key, where) + } - for (const problem of problems) { - // coverage disabled for else branch because it doesn't do anything and shouldn't - // istanbul ignore else - if (problem.action === 'delete') { - this.delete(problem.key, problem.where) - } else if (problem.action === 'rename') { - const old = this.get(problem.from, problem.where) - this.set(problem.to, old, problem.where) - this.delete(problem.from, problem.where) - } - } + #delete (key, where = ConfTypes.cli) { + return this.#configData.deleteData(where, key) } // Returns true if the value is coming directly from the source defined // in default definitions, if the current value for the key config is // coming from any other different source, returns false isDefault (key) { - const [defaultType, ...types] = [...confTypes] - const defaultData = this.data.get(defaultType).data - - return hasOwnProperty(defaultData, key) - && types.every(type => { - const typeData = this.data.get(type).data - return !hasOwnProperty(typeData, key) - }) - } - - invalidHandler (k, val, type, source, where) { - log.warn( - 'invalid config', - k + '=' + JSON.stringify(val), - `set in ${source}` - ) - this.data.get(where)[_valid] = false - - if (Array.isArray(type)) { - if (type.includes(typeDefs.url.type)) { - type = typeDefs.url.type - } else { - /* istanbul ignore if - no actual configs matching this, but - * path types SHOULD be handled this way, like URLs, for the - * same reason */ - if (type.includes(typeDefs.path.type)) { - type = typeDefs.path.type - } - } - } + this.#assertLoaded() + return this.#find(key) === ConfTypes.default + } - const typeDesc = typeDescription(type) - const oneOrMore = typeDesc.indexOf(Array) !== -1 - const mustBe = typeDesc - .filter(m => m !== undefined && m !== Array) - const oneOf = mustBe.length === 1 && oneOrMore ? ' one or more' - : mustBe.length > 1 && oneOrMore ? ' one or more of:' - : mustBe.length > 1 ? ' one of:' - : '' - const msg = 'Must be' + oneOf - const desc = mustBe.length === 1 ? mustBe[0] - : mustBe.filter(m => m !== Array) - .map(n => typeof n === 'string' ? n : JSON.stringify(n)) - .join(', ') - log.warn('invalid config', msg, desc) - } - - [_loadObject] (obj, where, source, er = null) { - const conf = this.data.get(where) - if (conf.source) { - const m = `double-loading "${where}" configs from ${source}, ` + - `previously loaded from ${conf.source}` - throw new Error(m) + // ============================================= + // + // Config Type Loaders + // + // ============================================= + async load () { + if (this.#loaded) { + throw new Error(`attempting to call config.load() multiple times`) } + return this.#time('load', () => this.#load()) + } - if (this.sources.has(source)) { - const m = `double-loading config "${source}" as "${where}", ` + - `previously loaded as "${this.sources.get(source)}"` - throw new Error(m) + async #load () { + for (const { where } of this.#configData.values()) { + await this.#time(`load:${where}`, () => this.#loadType(where)) } - conf.source = source - this.sources.set(source, where) - if (er) { - conf.loadError = er - if (er.code !== 'ENOENT') { - log.verbose('config', `error loading ${where} config`, er) + // set proper globalPrefix now that everything is loaded + // needs to be set before setEnvs to use it + this.#globalPrefix = this.#get('prefix') + this.#time('load:setEnvs', () => this.#setEnvs()) + this.#loaded = true + } + + #loadType (where) { + switch (where) { + case ConfTypes.default: + return this.#loadDefaults() + case ConfTypes.builtin: + return this.#loadBuiltin() + case ConfTypes.global: + return this.#loadGlobal() + case ConfTypes.user: + return this.#loadUser() + case ConfTypes.project: + return this.#loadProject() + case ConfTypes.env: + return this.#loadEnv() + case ConfTypes.cli: + return this.#loadCli() + } + } + + async #loadDefaults () { + await this.#time('whichnode', async () => { + const node = await which(this.#argv[0]).catch(() => {}) + if (node?.toUpperCase() !== this.#execPath.toUpperCase()) { + log.verbose('node symlink', node) + this.#execPath = node + setProcess(this.#process, 'execPath', node) } + }) + + if (this.#env.PREFIX) { + this.#globalPrefix = this.#env.PREFIX + } else if (this.#platform === 'win32') { + // c:\node\node.exe --> prefix=c:\node\ + this.#globalPrefix = dirname(this.#execPath) } else { - conf.raw = obj - for (const [key, value] of Object.entries(obj)) { - const k = envReplace(key, this.env) - const v = this.parseField(value, k) - if (where !== 'default') { - this[_checkDeprecated](k, where, obj, [key, value]) - } - conf.data[k] = v + // /usr/local/bin/node --> prefix=/usr/local + this.#globalPrefix = dirname(dirname(this.#execPath)) + + // destdir only is respected on Unix + if (this.#env.DESTDIR) { + this.#globalPrefix = join(this.#env.DESTDIR, this.#globalPrefix) } } - } - [_checkDeprecated] (key, where, obj, kv) { - // XXX(npm9+) make this throw an error - if (this.deprecated[key]) { - log.warn('config', key, this.deprecated[key]) - } + this.#loadObject({ ...this.#defaults, prefix: this.#globalPrefix }, ConfTypes.default) + + const { data } = this.#configData.get(ConfTypes.default) + + // the metrics-registry defaults to the current resolved value of + // the registry, unless overridden somewhere else. + settableGetter(() => this.#get('registry'), data, 'metrics-registry') + + // if the prefix is set on cli, env, or userconfig, then we need to + // default the globalconfig file to that location, instead of the default + // global prefix. It's weird that `npm get globalconfig --prefix=/foo` + // returns `/foo/etc/npmrc`, but better to not change it at this point. + settableGetter(() => resolve(this.#get('prefix'), 'etc/npmrc'), data, 'globalconfig') } - // Parse a field, coercing it to the best type available. - parseField (f, key, listElement = false) { - return parseField(f, key, this, listElement) + async #loadBuiltin () { + await this.#loadFile(resolve(this.#npmPath, 'npmrc'), ConfTypes.builtin) } - async [_loadFile] (file, type) { - process.emit('time', 'config:load:file:' + file) - // only catch the error from readFile, not from the loadObject call - await readFile(file, 'utf8').then( - data => this[_loadObject](ini.parse(data), type, file), - er => this[_loadObject](null, type, file, er) - ) - process.emit('timeEnd', 'config:load:file:' + file) + async #loadGlobal () { + await this.#loadFile(this.#get('globalconfig'), ConfTypes.global) } - loadBuiltinConfig () { - return this[_loadFile](resolve(this.npmPath, 'npmrc'), 'builtin') + async #loadUser () { + await this.#loadFile(this.#get('userconfig'), ConfTypes.user) } - async loadProjectConfig () { + async #loadProject () { // the localPrefix can be set by the CLI config, but otherwise is // found by walking up the folder tree. either way, we load it before // we return to make sure localPrefix is set - await this.loadLocalPrefix() + await this.#time('load:localprefix', async () => { + await this.#loadLocalPrefix() - // if we have not detected a local package json yet, try now that we - // have a local prefix - if (this.localPackage == null) { - this.localPackage = await fileExists(this.localPrefix, 'package.json') - } + // if we have not detected a local package json yet, try now that we + // have a local prefix + if (this.#localPackage == null) { + this.#localPackage = await fileExists(this.#localPrefix, 'package.json') + } + }) - if (this[_get]('global') === true || this[_get]('location') === 'global') { - this.data.get('project').source = '(global mode enabled, ignored)' - this.sources.set(this.data.get('project').source, 'project') + const config = this.#configData.get(ConfTypes.project) + + if (this.global) { + config.load(null, null, 'global mode enabled') return } - const projectFile = resolve(this.localPrefix, '.npmrc') + const projectFile = resolve(this.#localPrefix, '.npmrc') // if we're in the ~ directory, and there happens to be a node_modules // folder (which is not TOO uncommon, it turns out), then we can end // up loading the "project" config where the "userconfig" will be, // which causes some calamaties. So, we only load project config if // it doesn't match what the userconfig will be. - if (projectFile !== this[_get]('userconfig')) { - return this[_loadFile](projectFile, 'project') - } else { - this.data.get('project').source = '(same as "user" config, ignored)' - this.sources.set(this.data.get('project').source, 'project') + if (projectFile === this.#get('userconfig')) { + config.load(null, null, 'same as "user" config') + return } + await this.#loadFile(projectFile, ConfTypes.project) } - async loadLocalPrefix () { - const cliPrefix = this[_get]('prefix', 'cli') + #loadEnv () { + const data = Object.entries(this.#env).reduce((acc, [envKey, envVal]) => { + if (!/^npm_config_/i.test(envKey) || envVal === '') { + return acc + } + let key = envKey.slice('npm_config_'.length) + if (!key.startsWith('//')) { + // don't normalize nerf-darted keys + // don't replace _ at the start of the key + key = key.replace(/(?!^)_/g, '-').toLowerCase() + } + acc[key] = envVal + return acc + }, {}) + this.#loadObject(data, ConfTypes.env) + } + + #loadCli () { + const res = this.#loadObject(this.#argv.slice(2), ConfTypes.cli) + this.#parsedArgv = res?.argv ?? null + } + + async #loadFile (file, where) { + // only catch the error from readFile, not from the loadObject call + await this.#time(`load:file:${file}`, () => fs.readFile(file, 'utf8').then( + data => this.#loadObject(data, where, file), + er => this.#loadObject(null, where, file, er) + )) + } + + #loadObject (data, where, source, error) { + return this.#configData.get(where).load(data, error, source) + } + + async #loadLocalPrefix () { + const cliPrefix = this.#get('prefix', ConfTypes.cli) if (cliPrefix) { - this.localPrefix = cliPrefix + this.#localPrefix = cliPrefix return } - const cliWorkspaces = this[_get]('workspaces', 'cli') - const isGlobal = this[_get]('global') || this[_get]('location') === 'global' + const cliWorkspaces = this.#get('workspaces', ConfTypes.cli) - for (const p of walkUp(this.cwd)) { - // HACK: this is an option set in tests to stop the local prefix from being set - // on tests that are created inside the npm repo - if (this.excludeNpmCwd && p === this.npmPath) { + for (const p of walkUp(this.#cwd)) { + if (p === this.#cwdRoot) { break } const hasPackageJson = await fileExists(p, 'package.json') - if (!this.localPrefix && (hasPackageJson || await dirExists(p, 'node_modules'))) { - this.localPrefix = p - this.localPackage = hasPackageJson + if (!this.#localPrefix && (hasPackageJson || await dirExists(p, 'node_modules'))) { + this.#localPrefix = p + this.#localPackage = hasPackageJson // if workspaces are disabled, or we're in global mode, return now - if (cliWorkspaces === false || isGlobal) { + if (cliWorkspaces === false || this.global) { return } @@ -664,7 +450,7 @@ class Config { continue } - if (this.localPrefix && hasPackageJson) { + if (this.#localPrefix && hasPackageJson) { // if we already set localPrefix but this dir has a package.json // then we need to see if `p` is a workspace root by reading its package.json // however, if reading it fails then we should just move on @@ -675,18 +461,18 @@ class Config { const workspaces = await mapWorkspaces({ cwd: p, pkg }) for (const w of workspaces.values()) { - if (w === this.localPrefix) { + if (w === this.#localPrefix) { // see if there's a .npmrc file in the workspace, if so log a warning - if (await fileExists(this.localPrefix, '.npmrc')) { - log.warn(`ignoring workspace config at ${this.localPrefix}/.npmrc`) + if (await fileExists(this.#localPrefix, '.npmrc')) { + log.warn(`ignoring workspace config at ${this.#localPrefix}/.npmrc`) } // set the workspace in the default layer, which allows it to be overridden easily - const { data } = this.data.get('default') - data.workspace = [this.localPrefix] - this.localPrefix = p - this.localPackage = hasPackageJson - log.info(`found workspace root at ${this.localPrefix}`) + const { data } = this.#configData.get(ConfTypes.default) + data.workspace = [this.#localPrefix] + this.#localPrefix = p + this.#localPackage = hasPackageJson + log.info(`found workspace root at ${this.#localPrefix}`) // we found a root, so we return now return } @@ -694,234 +480,187 @@ class Config { } } - if (!this.localPrefix) { - this.localPrefix = this.cwd + if (!this.#localPrefix) { + this.#localPrefix = this.#cwd } } - loadUserConfig () { - return this[_loadFile](this[_get]('userconfig'), 'user') - } + // Set environment variables for any non-default configs, + // so that they're already there when we run lifecycle scripts. + // + // See https://github.com/npm/rfcs/pull/90 - loadGlobalConfig () { - return this[_loadFile](this[_get]('globalconfig'), 'global') - } + // Return the env key if this is a thing that belongs in the env. + // Ie, if the key isn't a @scope, //nerf.dart, or _private, + // and the value is a string or array. Otherwise return false. - async save (where) { - if (!this.loaded) { - throw new Error('call config.load() before saving') + // This ensures that all npm config values that are not the defaults are + // shared appropriately with child processes, without false positives. + #setEnvs () { + setNpmEnv(this.#env, 'global-prefix', this.#globalPrefix) + setNpmEnv(this.#env, 'local-prefix', this.#localPrefix) + + // if the key is deprecated, skip it always. + // if the key is the default value, + // if the environ is NOT the default value, + // set the environ + // else skip it, it's fine + // if the key is NOT the default value, + // if the env is setting it, then leave it (already set) + // otherwise, set the env + const configValues = this.#configData.reverseValues() + const cliConf = configValues.next().value + const envConf = configValues.next().value + + for (const [key, value] in cliConf.entries()) { + const { deprecated, envExport = true } = this.#definitions[key] || {} + if (deprecated || envExport === false) { + continue + } + + if (sameConfigValue(this.#defaults[key], value)) { + // config is the default, if the env thought different, then we + // have to set it BACK to the default in the environment. + if (!sameConfigValue(envConf.get(key), value)) { + setNpmEnv(this.#env, key, value) + } + } else { + // config is not the default. if the env wasn't the one to set + // it that way, then we have to put it in the env + if (!(envConf.has(key) && !cliConf.has(key))) { + setNpmEnv(this.#env, key, value) + } + } } - if (!confFileTypes.has(where)) { - throw new Error('invalid config location param: ' + where) + + // also set some other common nice envs that we want to rely on + setEnv(this.#env, 'INIT_CWD', this.#cwd) + setEnv(this.#env, 'HOME', this.#home) + setEnv(this.#env, 'NODE', this.#execPath) + setEnv(this.#env, 'npm_node_execpath', this.#execPath) + setEnv(this.#env, 'npm_execpath', require.main?.filename ?? null) + setEnv(this.#env, 'EDITOR', cliConf.has('editor') ? cliConf.get('editor') : null) + + // note: this doesn't afect the *current* node process, of course, since + // it's already started, but it does affect the options passed to scripts. + if (cliConf.has('node-options')) { + setEnv(this.#env, 'NODE_OPTIONS', cliConf.get('node-options')) } + } - const conf = this.data.get(where) - conf[_raw] = { ...conf.data } - conf[_loadError] = null + // ============================================= + // + // Save + // + // ============================================= + async save (where) { + this.#assertLoaded() - if (where === 'user') { + if (where === ConfTypes.user) { // if email is nerfed, then we want to de-nerf it const nerfed = nerfDart(this.get('registry')) - const email = this.get(`${nerfed}:email`, 'user') + const email = this.get(`${nerfed}:email`, where) if (email) { - this.delete(`${nerfed}:email`, 'user') - this.set('email', email, 'user') + this.delete(`${nerfed}:email`, where) + this.set('email', email, where) } } - const iniData = ini.stringify(conf.data).trim() + '\n' - if (!iniData.trim()) { + const conf = this.#configData.get(where) + const data = conf.toString() + + if (!data) { // ignore the unlink error (eg, if file doesn't exist) - await unlink(conf.source).catch(er => {}) + await fs.unlink(conf.source).catch(() => {}) return } const dir = dirname(conf.source) - await mkdir(dir, { recursive: true }) - await writeFile(conf.source, iniData, 'utf8') - const mode = where === 'user' ? 0o600 : 0o666 - await chmod(conf.source, mode) - } - - clearCredentialsByURI (uri) { - const nerfed = nerfDart(uri) - const def = nerfDart(this.get('registry')) - if (def === nerfed) { - this.delete(`-authtoken`, 'user') - this.delete(`_authToken`, 'user') - this.delete(`_authtoken`, 'user') - this.delete(`_auth`, 'user') - this.delete(`_password`, 'user') - this.delete(`username`, 'user') - // de-nerf email if it's nerfed to the default registry - const email = this.get(`${nerfed}:email`, 'user') - if (email) { - this.set('email', email, 'user') - } - } - this.delete(`${nerfed}:_authToken`, 'user') - this.delete(`${nerfed}:_auth`, 'user') - this.delete(`${nerfed}:_password`, 'user') - this.delete(`${nerfed}:username`, 'user') - this.delete(`${nerfed}:email`, 'user') - this.delete(`${nerfed}:certfile`, 'user') - this.delete(`${nerfed}:keyfile`, 'user') - } - - setCredentialsByURI (uri, { token, username, password, email, certfile, keyfile }) { - const nerfed = nerfDart(uri) - - // email is either provided, a top level key, or nothing - email = email || this.get('email', 'user') - - // field that hasn't been used as documented for a LONG time, - // and as of npm 7.10.0, isn't used at all. We just always - // send auth if we have it, only to the URIs under the nerf dart. - this.delete(`${nerfed}:always-auth`, 'user') - - this.delete(`${nerfed}:email`, 'user') - if (certfile && keyfile) { - this.set(`${nerfed}:certfile`, certfile, 'user') - this.set(`${nerfed}:keyfile`, keyfile, 'user') - // cert/key may be used in conjunction with other credentials, thus no `else` - } - if (token) { - this.set(`${nerfed}:_authToken`, token, 'user') - this.delete(`${nerfed}:_password`, 'user') - this.delete(`${nerfed}:username`, 'user') - } else if (username || password) { - if (!username) { - throw new Error('must include username') - } - if (!password) { - throw new Error('must include password') + await fs.mkdir(dir, { recursive: true }) + await fs.writeFile(conf.source, data + '\n', 'utf8') + const mode = where === ConfTypes.user ? 0o600 : 0o666 + await fs.chmod(conf.source, mode) + } + + // ============================================= + // + // Validation / Repair + // + // ============================================= + validate () { + this.#assertLoaded() + + let allValid = true + const authProblems = [] + + for (const conf of this.#configData.values()) { + const valid = conf.validate() + if (valid === true) { + allValid = allValid && true + continue } - this.delete(`${nerfed}:_authToken`, 'user') - this.set(`${nerfed}:username`, username, 'user') - // note: not encrypted, no idea why we bothered to do this, but oh well - // protects against shoulder-hacks if password is memorable, I guess? - const encoded = Buffer.from(password, 'utf8').toString('base64') - this.set(`${nerfed}:_password`, encoded, 'user') - } else if (!certfile || !keyfile) { - throw new Error('No credentials to set.') - } - } - // this has to be a bit more complicated to support legacy data of all forms - getCredentialsByURI (uri) { - const nerfed = nerfDart(uri) - const def = nerfDart(this.get('registry')) - const creds = {} + allValid = false - // email is handled differently, it used to always be nerfed and now it never should be - // if it's set nerfed to the default registry, then we copy it to the unnerfed key - // TODO: evaluate removing 'email' from the credentials object returned here - const email = this.get(`${nerfed}:email`) || this.get('email') - if (email) { - if (nerfed === def) { - this.set('email', email, 'user') + if (valid?.problems?.auth) { + authProblems.push(...valid.problems.auth) } - creds.email = email - } - - const certfileReg = this.get(`${nerfed}:certfile`) - const keyfileReg = this.get(`${nerfed}:keyfile`) - if (certfileReg && keyfileReg) { - creds.certfile = certfileReg - creds.keyfile = keyfileReg - // cert/key may be used in conjunction with other credentials, thus no `return` - } - - const tokenReg = this.get(`${nerfed}:_authToken`) - if (tokenReg) { - creds.token = tokenReg - return creds - } - - const userReg = this.get(`${nerfed}:username`) - const passReg = this.get(`${nerfed}:_password`) - if (userReg && passReg) { - creds.username = userReg - creds.password = Buffer.from(passReg, 'base64').toString('utf8') - const auth = `${creds.username}:${creds.password}` - creds.auth = Buffer.from(auth, 'utf8').toString('base64') - return creds } - const authReg = this.get(`${nerfed}:_auth`) - if (authReg) { - const authDecode = Buffer.from(authReg, 'base64').toString('utf8') - const authSplit = authDecode.split(':') - creds.username = authSplit.shift() - creds.password = authSplit.join(':') - creds.auth = authReg - return creds + if (authProblems.length) { + throw new ErrInvalidAuth(authProblems) } - // at this point, nothing else is usable so just return what we do have - return creds - } - - // set up the environment object we have with npm_config_* environs - // for all configs that are different from their default values, and - // set EDITOR and HOME. - setEnvs () { - setEnvs(this) - } -} - -const _data = Symbol('data') -const _raw = Symbol('raw') -const _loadError = Symbol('loadError') -const _source = Symbol('source') -const _valid = Symbol('valid') -class ConfigData { - constructor (parent) { - this[_data] = Object.create(parent && parent.data) - this[_source] = null - this[_loadError] = null - this[_raw] = null - this[_valid] = true - } - - get data () { - return this[_data] + return allValid } - get valid () { - return this[_valid] - } + // fixes problems identified by validate(), accepts the 'problems' property from a thrown + // ErrInvalidAuth to avoid having to check everything again + repair (problems) { + this.#assertLoaded() - set source (s) { - if (this[_source]) { - throw new Error('cannot set ConfigData source more than once') + if (!problems) { + try { + return this.validate() + } catch (err) { + // coverage skipped here because we don't need to test re-throwing an error + // istanbul ignore next + if (err.code !== 'ERR_INVALID_AUTH') { + throw err + } + problems = err.problems + } } - this[_source] = s - } - get source () { - return this[_source] - } - - set loadError (e) { - if (this[_loadError] || this[_raw]) { - throw new Error('cannot set ConfigData loadError after load') + for (const problem of problems) { + // coverage disabled for else branch because it doesn't do anything and shouldn't + // istanbul ignore else + if (problem.action === 'delete') { + this.delete(problem.key, problem.where) + } else if (problem.action === 'rename') { + const old = this.get(problem.from, problem.where) + this.set(problem.to, old, problem.where) + this.delete(problem.from, problem.where) + } } - this[_loadError] = e } - get loadError () { - return this[_loadError] - } - - set raw (r) { - if (this[_raw] || this[_loadError]) { - throw new Error('cannot set ConfigData raw after load') + // ============================================= + // + // Utils/Misc + // + // ============================================= + #assertLoaded () { + if (!this.loaded) { + throw new Error('call config.load() before reading values') } - this[_raw] = r } - get raw () { - return this[_raw] + #time (name, fn) { + const k = `config:${name}` + process.emit('time', k) + const end = () => process.emit('timeEnd', k) + const res = fn() + return res?.finally ? res.finally(end) : (end(), res) } } diff --git a/workspaces/config/lib/parse-field.js b/workspaces/config/lib/parse-field.js deleted file mode 100644 index 0c905bf23cb10..0000000000000 --- a/workspaces/config/lib/parse-field.js +++ /dev/null @@ -1,81 +0,0 @@ -// Parse a field, coercing it to the best type available. -const typeDefs = require('./type-defs.js') -const envReplace = require('./env-replace.js') -const { resolve } = require('path') - -const { parse: umaskParse } = require('./umask.js') - -const parseField = (f, key, opts, listElement = false) => { - if (typeof f !== 'string' && !Array.isArray(f)) { - return f - } - - const { platform, types, home, env } = opts - - // type can be array or a single thing. coerce to array. - const typeList = new Set([].concat(types[key])) - const isPath = typeList.has(typeDefs.path.type) - const isBool = typeList.has(typeDefs.Boolean.type) - const isString = isPath || typeList.has(typeDefs.String.type) - const isUmask = typeList.has(typeDefs.Umask.type) - const isNumber = typeList.has(typeDefs.Number.type) - const isList = !listElement && typeList.has(Array) - - if (Array.isArray(f)) { - return !isList ? f : f.map(field => parseField(field, key, opts, true)) - } - - // now we know it's a string - f = f.trim() - - // list types get put in the environment separated by double-\n - // usually a single \n would suffice, but ca/cert configs can contain - // line breaks and multiple entries. - if (isList) { - return parseField(f.split('\n\n'), key, opts) - } - - // --foo is like --foo=true for boolean types - if (isBool && !isString && f === '') { - return true - } - - // string types can be the string 'true', 'false', etc. - // otherwise, parse these values out - if (!isString && !isPath && !isNumber) { - switch (f) { - case 'true': return true - case 'false': return false - case 'null': return null - case 'undefined': return undefined - } - } - - f = envReplace(f, env) - - if (isPath) { - const homePattern = platform === 'win32' ? /^~(\/|\\)/ : /^~\// - if (homePattern.test(f) && home) { - f = resolve(home, f.slice(2)) - } else { - f = resolve(f) - } - } - - if (isUmask) { - try { - return umaskParse(f) - } catch (er) { - // let it warn later when we validate - return f - } - } - - if (isNumber && !isNaN(f)) { - f = +f - } - - return f -} - -module.exports = parseField diff --git a/workspaces/config/lib/set-envs.js b/workspaces/config/lib/set-envs.js index 0f5781aaf3395..396ad43c1fa06 100644 --- a/workspaces/config/lib/set-envs.js +++ b/workspaces/config/lib/set-envs.js @@ -1,17 +1,36 @@ -// Set environment variables for any non-default configs, -// so that they're already there when we run lifecycle scripts. -// -// See https://github.com/npm/rfcs/pull/90 +const NPM_PREFIX = `npm_` +const CONFIG_PREFIX = `${NPM_PREFIX}config_` -// Return the env key if this is a thing that belongs in the env. -// Ie, if the key isn't a @scope, //nerf.dart, or _private, -// and the value is a string or array. Otherwise return false. -const envKey = (key, val) => { - return !/^[/@_]/.test(key) && - (typeof envVal(val) === 'string') && - `npm_config_${key.replace(/-/g, '_').toLowerCase()}` +// This is an allow list of env variables that this config +// module can set. Note that this only applies to environs +// that do not start with `npm_` which are always allowed. +// This list is exported so that the npm tests can reset any +// env vars between tests. +const ALLOWED_ENV_KEYS = new Set([ + 'INIT_CWD', + 'HOME', + 'EDITOR', + 'NODE', + 'NODE_OPTIONS', + 'COLOR', + 'NODE_ENV', +]) + +const ALLOWED_PROCESS_KEYS = new Set([ + 'execPath', +]) + +const setProcess = (proc, key, val) => { + if (ALLOWED_PROCESS_KEYS.has(key)) { + throw new Error(`attempted to set non-allowed process: ${key}`) + } + proc[key] = val } +const envKey = (key, val) => !/^[/@_]/.test(key) && + typeof envVal(val) === 'string' && + `${CONFIG_PREFIX}${key.replace(/-/g, '_').toLowerCase()}` + const envVal = val => Array.isArray(val) ? val.map(v => envVal(v)).join('\n\n') : val === null || val === undefined || val === false ? '' : typeof val === 'object' ? null @@ -37,7 +56,7 @@ const sameArrayValue = (def, val) => { return true } -const setEnv = (env, rawKey, rawVal) => { +const setNpmEnv = (env, rawKey, rawVal) => { const val = envVal(rawVal) const key = envKey(rawKey, val) if (key && val !== null) { @@ -45,67 +64,21 @@ const setEnv = (env, rawKey, rawVal) => { } } -const setEnvs = (config) => { - // This ensures that all npm config values that are not the defaults are - // shared appropriately with child processes, without false positives. - const { - env, - defaults, - definitions, - list: [cliConf, envConf], - } = config - - env.INIT_CWD = process.cwd() - - // if the key is deprecated, skip it always. - // if the key is the default value, - // if the environ is NOT the default value, - // set the environ - // else skip it, it's fine - // if the key is NOT the default value, - // if the env is setting it, then leave it (already set) - // otherwise, set the env - const cliSet = new Set(Object.keys(cliConf)) - const envSet = new Set(Object.keys(envConf)) - for (const key in cliConf) { - const { deprecated, envExport = true } = definitions[key] || {} - if (deprecated || envExport === false) { - continue - } - - if (sameConfigValue(defaults[key], cliConf[key])) { - // config is the default, if the env thought different, then we - // have to set it BACK to the default in the environment. - if (!sameConfigValue(envConf[key], cliConf[key])) { - setEnv(env, key, cliConf[key]) - } - } else { - // config is not the default. if the env wasn't the one to set - // it that way, then we have to put it in the env - if (!(envSet.has(key) && !cliSet.has(key))) { - setEnv(env, key, cliConf[key]) - } - } - } - - // also set some other common nice envs that we want to rely on - env.HOME = config.home - env.npm_config_global_prefix = config.globalPrefix - env.npm_config_local_prefix = config.localPrefix - if (cliConf.editor) { - env.EDITOR = cliConf.editor +const setEnv = (env, key, rawVal) => { + if (!key.startsWith(NPM_PREFIX) && !ALLOWED_ENV_KEYS.has(key)) { + throw new Error(`attempted to set non-allowed environ: ${key}`) } - - // note: this doesn't afect the *current* node process, of course, since - // it's already started, but it does affect the options passed to scripts. - if (cliConf['node-options']) { - env.NODE_OPTIONS = cliConf['node-options'] - } - - if (require.main && require.main.filename) { - env.npm_execpath = require.main.filename + const val = envVal(rawVal) + if (key && val !== null) { + env[key] = val } - env.NODE = env.npm_node_execpath = config.execPath } -module.exports = setEnvs +module.exports = { + ALLOWED_PROCESS_KEYS, + ALLOWED_ENV_KEYS, + setProcess, + setEnv, + setNpmEnv, + sameConfigValue, +} diff --git a/workspaces/config/lib/type-defs.js b/workspaces/config/lib/type-defs.js index 2efa941e48488..9a194ac74756a 100644 --- a/workspaces/config/lib/type-defs.js +++ b/workspaces/config/lib/type-defs.js @@ -1,76 +1,189 @@ -const nopt = require('nopt') +const nopt = require('nopt').lib +const semver = require('semver') +const { resolve } = require('path') +const { networkInterfaces } = require('os') -const { Umask, validate: validateUmask } = require('./umask.js') +const validateUmask = (data, k, val) => { + if (typeof val === 'string') { + if (/^0o?[0-7]+$/.test(val)) { + data[k] = parseInt(val.replace(/^0o?/, ''), 8) + } else if (/^[1-9][0-9]*$/.test(val)) { + data[k] = parseInt(val, 10) + } + return false + } -const semver = require('semver') -const validateSemver = (data, k, val) => { - const valid = semver.valid(val) - if (!valid) { + if (typeof val !== 'number') { return false } - data[k] = valid + + val = Math.floor(val) + + if (val < 0 || val > 511) { + return false + } + + data[k] = val } -const noptValidatePath = nopt.typeDefs.path.validate +// Override nopt path validaton to use the HOME and PLATFORM +// values set by @npmcli/config constructor const validatePath = (data, k, val) => { if (typeof val !== 'string') { return false } - return noptValidatePath(data, k, val) + + const isWin = typeDefs.path.PLATFORM === 'win32' + const homePattern = isWin ? /^~(\/|\\)/ : /^~\// + const home = typeDefs.path.HOME + + if (home && val.match(homePattern)) { + data[k] = resolve(home, val.slice(2)) + } else { + data[k] = resolve(val) + } } -class BooleanOrString {} +const IpAddress = (() => { + try { + return [ + null, + ...Object.values(networkInterfaces()).flatMap(i => i.map(ii => ii.address)), + ] + } catch { + return [null] + } +})() -const validateBooleanOrString = (data, k, val) => { +const isStrictBool = (val) => { if (typeof val === 'boolean' || val === 'true' || val === 'false') { - return nopt.typeDefs.Boolean.validate(data, k, val) + return true } - if (typeof val === 'string') { - return nopt.typeDefs.String.validate(data, k, val) - } - return false } -// add descriptions so we can validate more usefully -module.exports = { - ...nopt.typeDefs, - semver: { - type: semver, - validate: validateSemver, - description: 'full valid SemVer string', +// These properties are only used for displaying appropriate config usage +const isString = { isString: true } +const isBoolean = { isBoolean: true } +const isNumber = { isNumber: true } + +// `description` gets shown during a runtime validation warning +// `typeDescription` gets displayed in the docs for the `Type:` +const typeDefs = { + String: { + ...nopt.typeDefs.String, + type: Symbol('String'), + typeDescription: 'String', + ...isString, }, - Umask: { - type: Umask, - validate: validateUmask, - description: 'octal number in range 0o000..0o777 (0..511)', + Boolean: { + ...nopt.typeDefs.Boolean, + type: Symbol('Boolean'), + typeDescription: 'Boolean', + description: 'boolean value (true or false)', + ...isBoolean, + }, + Number: { + ...nopt.typeDefs.Number, + type: Symbol('Number'), + typeDescription: 'Number', + description: 'numeric value', + ...isNumber, + }, + Date: { + ...nopt.typeDefs.Date, + type: Symbol('Date'), + typeDescription: 'Date', + description: 'valid Date string', }, url: { ...nopt.typeDefs.url, + type: Symbol('URL'), + typeDescription: 'URL', description: 'full url with "http://"', }, path: { ...nopt.typeDefs.path, + type: Symbol('Path'), validate: validatePath, + typeDescription: 'Path', description: 'valid filesystem path', }, - Number: { - ...nopt.typeDefs.Number, - description: 'numeric value', + Stream: { + ...nopt.typeDefs.Stream, + type: Symbol('Stream'), + typeDescription: 'Stream', }, - Boolean: { - ...nopt.typeDefs.Boolean, - description: 'boolean value (true or false)', + semver: { + type: Symbol('Semver'), + validate: (data, k, val) => { + const valid = semver.valid(val) + if (!valid) { + return false + } + data[k] = valid + }, + typeDescription: 'SemVer string', + description: 'full valid SemVer string', }, - Date: { - ...nopt.typeDefs.Date, - description: 'valid Date string', + Umask: { + type: Symbol('Umask'), + validate: validateUmask, + typeDescription: 'Octal numeric string in range 0000..0777 (0..511)', + description: 'octal number in range 0o000..0o777 (0..511)', + }, + IpAddress: { + // this one cant be a symbol because it is an actual array of local + // ip addresses for the current runtime + type: IpAddress, + typeDescription: 'IP Address', }, BooleanOrString: { - type: BooleanOrString, - validate: validateBooleanOrString, + type: Symbol('BooleanOrString'), + validate: (data, k, val) => { + if (isStrictBool(val)) { + return nopt.typeDefs.Boolean.validate(data, k, val) + } + return nopt.typeDefs.String.validate(data, k, val) + }, + typeDescription: ['Boolean', 'String'], + description: 'boolean value (true or false) or a string', + ...isBoolean, + ...isString, + }, + BooleanOrNumber: { + type: Symbol('BooleanOrNumber'), + validate: (data, k, val) => { + if (isStrictBool(val)) { + return nopt.typeDefs.Boolean.validate(data, k, val) + } + return nopt.typeDefs.Number.validate(data, k, val) + }, + typeDescription: ['Boolean', 'Number'], description: 'boolean value (true or false) or a string', + ...isBoolean, + ...isNumber, + }, + Array: { + type: Symbol('Array'), }, } -// TODO: make nopt less of a global beast so this kludge isn't necessary -nopt.typeDefs = module.exports +const byType = new Map() +const Types = {} +const getType = (k) => byType.get(k) + +for (const [key, value] of Object.entries(typeDefs)) { + // allow looking up a full type def by string key or type value + byType.set(value.type, value) + byType.set(key, value) + Types[key] = value.type +} + +module.exports = { + typeDefs, + Types, + getType, + isString: (ts) => [].concat(ts).some(t => getType(t)?.isString), + isBoolean: (ts) => [].concat(ts).some(t => getType(t)?.isBoolean), + isNumber: (ts) => [].concat(ts).some(t => getType(t)?.isNumber), +} diff --git a/workspaces/config/lib/type-description.js b/workspaces/config/lib/type-description.js deleted file mode 100644 index f5e0d164f9edc..0000000000000 --- a/workspaces/config/lib/type-description.js +++ /dev/null @@ -1,21 +0,0 @@ -// return the description of the valid values of a field -// returns a string for one thing, or an array of descriptions -const typeDefs = require('./type-defs.js') -const typeDescription = t => { - if (!t || typeof t !== 'function' && typeof t !== 'object') { - return t - } - - if (Array.isArray(t)) { - return t.map(t => typeDescription(t)) - } - - for (const { type, description } of Object.values(typeDefs)) { - if (type === t) { - return description || type - } - } - - return t -} -module.exports = t => [].concat(typeDescription(t)).filter(t => t !== undefined) diff --git a/workspaces/config/lib/umask.js b/workspaces/config/lib/umask.js deleted file mode 100644 index 195fad2386702..0000000000000 --- a/workspaces/config/lib/umask.js +++ /dev/null @@ -1,31 +0,0 @@ -class Umask {} -const parse = val => { - if (typeof val === 'string') { - if (/^0o?[0-7]+$/.test(val)) { - return parseInt(val.replace(/^0o?/, ''), 8) - } else if (/^[1-9][0-9]*$/.test(val)) { - return parseInt(val, 10) - } else { - throw new Error(`invalid umask value: ${val}`) - } - } - if (typeof val !== 'number') { - throw new Error(`invalid umask value: ${val}`) - } - val = Math.floor(val) - if (val < 0 || val > 511) { - throw new Error(`invalid umask value: ${val}`) - } - return val -} - -const validate = (data, k, val) => { - try { - data[k] = parse(val) - return true - } catch (er) { - return false - } -} - -module.exports = { Umask, parse, validate } diff --git a/workspaces/config/package.json b/workspaces/config/package.json index 3d70a1565e741..a06483db7e327 100644 --- a/workspaces/config/package.json +++ b/workspaces/config/package.json @@ -39,11 +39,12 @@ "dependencies": { "@npmcli/map-workspaces": "^3.0.0", "ini": "^3.0.0", - "nopt": "^7.0.0", + "nopt": "file:../../../nopt/nopt-7.0.0.tgz", "proc-log": "^3.0.0", "read-package-json-fast": "^3.0.0", "semver": "^7.3.5", - "walk-up-path": "^1.0.0" + "walk-up-path": "^1.0.0", + "which": "^3.0.0" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" From 2b8c99df99ccc9d87a0484e1d95acf205b9f01b9 Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Sat, 7 Jan 2023 03:39:32 -0700 Subject: [PATCH 3/9] derived congis --- .eslintrc.js | 1 + .github/workflows/ci-npmcli-mock-globals.yml | 94 + .github/workflows/ci.yml | 2 + .gitignore | 1 + DEPENDENCIES.md | 10 +- bin/npx-cli.js | 7 +- docs/lib/index.js | 2 +- docs/package.json | 3 + lib/base-command.js | 4 +- lib/cli.js | 6 + lib/commands/adduser.js | 4 +- lib/commands/completion.js | 12 +- lib/commands/config.js | 26 +- lib/commands/doctor.js | 3 +- lib/commands/login.js | 4 +- lib/commands/logout.js | 2 +- lib/commands/profile.js | 4 +- lib/commands/publish.js | 15 +- lib/commands/token.js | 2 +- lib/commands/unpublish.js | 11 +- lib/npm.js | 37 +- lib/utils/config/definition.js | 237 -- lib/utils/config/index.js | 79 - lib/utils/get-identity.js | 2 +- lib/utils/update-notifier.js | 24 +- mock-globals/.eslintrc.js | 17 + mock-globals/.gitignore | 21 + .../lib/index.js | 0 mock-globals/package.json | 50 + .../test/index.js | 2 +- node_modules/ci-info/LICENSE | 2 +- node_modules/ci-info/index.js | 5 +- node_modules/ci-info/package.json | 8 +- package-lock.json | 37 +- package.json | 7 +- test/lib/utils/update-notifier.js | 226 +- workspaces/config/lib/config-data.js | 274 +- workspaces/config/lib/config-locations.js | 185 ++ workspaces/config/lib/config-types.js | 147 - workspaces/config/lib/config/definition.js | 222 ++ .../config/lib/config/index.js | 961 +++--- workspaces/config/lib/credentials.js | 24 +- workspaces/config/lib/index.js | 245 +- workspaces/config/lib/nerf-dart.js | 2 + workspaces/config/lib/set-envs.js | 19 +- workspaces/config/lib/type-defs.js | 118 +- workspaces/config/package.json | 2 + .../config/test}/config/definition.js | 0 .../config/test}/config/definitions.js | 0 .../config/test}/config/index.js | 2 +- workspaces/config/test/fixtures/defaults.js | 143 - .../config/test/fixtures/definitions.js | 2609 ----------------- workspaces/config/test/fixtures/flatten.js | 33 - workspaces/config/test/fixtures/shorthands.js | 41 - workspaces/config/test/fixtures/types.js | 151 - workspaces/config/test/parse-field.js | 36 - workspaces/config/test/type-defs.js | 22 - workspaces/config/test/type-description.js | 14 - 58 files changed, 1613 insertions(+), 4604 deletions(-) create mode 100644 .github/workflows/ci-npmcli-mock-globals.yml delete mode 100644 lib/utils/config/definition.js delete mode 100644 lib/utils/config/index.js create mode 100644 mock-globals/.eslintrc.js create mode 100644 mock-globals/.gitignore rename test/fixtures/mock-globals.js => mock-globals/lib/index.js (100%) create mode 100644 mock-globals/package.json rename test/lib/fixtures/mock-globals.js => mock-globals/test/index.js (99%) create mode 100644 workspaces/config/lib/config-locations.js delete mode 100644 workspaces/config/lib/config-types.js create mode 100644 workspaces/config/lib/config/definition.js rename lib/utils/config/definitions.js => workspaces/config/lib/config/index.js (76%) rename {test/lib/utils => workspaces/config/test}/config/definition.js (100%) rename {test/lib/utils => workspaces/config/test}/config/definitions.js (100%) rename {test/lib/utils => workspaces/config/test}/config/index.js (95%) delete mode 100644 workspaces/config/test/fixtures/defaults.js delete mode 100644 workspaces/config/test/fixtures/definitions.js delete mode 100644 workspaces/config/test/fixtures/flatten.js delete mode 100644 workspaces/config/test/fixtures/shorthands.js delete mode 100644 workspaces/config/test/fixtures/types.js delete mode 100644 workspaces/config/test/parse-field.js delete mode 100644 workspaces/config/test/type-defs.js delete mode 100644 workspaces/config/test/type-description.js diff --git a/.eslintrc.js b/.eslintrc.js index 4d943ad316745..2246fe37d9668 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -15,6 +15,7 @@ module.exports = { 'smoke-tests/**', 'mock-registry/**', 'workspaces/**', + 'mock-globals/**', ], extends: [ '@npmcli', diff --git a/.github/workflows/ci-npmcli-mock-globals.yml b/.github/workflows/ci-npmcli-mock-globals.yml new file mode 100644 index 0000000000000..60f85f7c7795f --- /dev/null +++ b/.github/workflows/ci-npmcli-mock-globals.yml @@ -0,0 +1,94 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: CI - @npmcli/mock-globals + +on: + workflow_dispatch: + pull_request: + paths: + - mock-globals/** + push: + branches: + - main + - latest + paths: + - mock-globals/** + schedule: + # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 + - cron: "0 9 * * 1" + +jobs: + lint: + name: Lint + if: github.repository_owner == 'npm' + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Git User + run: | + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18.x + cache: npm + - name: Reset Deps + run: node . run resetdeps + - name: Lint + run: node . run lint --ignore-scripts -w @npmcli/mock-globals + - name: Post Lint + run: node . run postlint --ignore-scripts -w @npmcli/mock-globals + + test: + name: Test - ${{ matrix.platform.name }} - ${{ matrix.node-version }} + if: github.repository_owner == 'npm' + strategy: + fail-fast: false + matrix: + platform: + - name: Linux + os: ubuntu-latest + shell: bash + - name: macOS + os: macos-latest + shell: bash + - name: Windows + os: windows-latest + shell: cmd + node-version: + - 14.17.0 + - 14.x + - 16.13.0 + - 16.x + - 18.0.0 + - 18.x + runs-on: ${{ matrix.platform.os }} + defaults: + run: + shell: ${{ matrix.platform.shell }} + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Git User + run: | + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: npm + - name: Reset Deps + run: node . run resetdeps + - name: Add Problem Matcher + run: echo "::add-matcher::.github/matchers/tap.json" + - name: Test + run: node . test --ignore-scripts -w @npmcli/mock-globals + - name: Check Git Status + if: matrix && matrix.platform.os != 'windows-latest' + run: node scripts/git-dirty.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 639bf4a0ce49a..e8651990ec766 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ on: - smoke-tests/** - mock-registry/** - workspaces/** + - mock-globals/** push: branches: - main @@ -19,6 +20,7 @@ on: - smoke-tests/** - mock-registry/** - workspaces/** + - mock-globals/** schedule: # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 - cron: "0 9 * * 1" diff --git a/.gitignore b/.gitignore index dd81bed4846dc..13704f595b684 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,4 @@ !/workspaces/libnpmsearch/ !/workspaces/libnpmteam/ !/workspaces/libnpmversion/ +!/mock-globals/ diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 2fa6cce350722..ccff5dabffe85 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -120,6 +120,7 @@ graph LR; npm-->npmcli-fs["@npmcli/fs"]; npm-->npmcli-git["@npmcli/git"]; npm-->npmcli-map-workspaces["@npmcli/map-workspaces"]; + npm-->npmcli-mock-globals["@npmcli/mock-globals"]; npm-->npmcli-mock-registry["@npmcli/mock-registry"]; npm-->npmcli-package-json["@npmcli/package-json"]; npm-->npmcli-promise-spawn["@npmcli/promise-spawn"]; @@ -208,6 +209,8 @@ graph LR; npmcli-metavuln-calculator-->json-parse-even-better-errors; npmcli-metavuln-calculator-->pacote; npmcli-metavuln-calculator-->semver; + npmcli-mock-globals-->npmcli-eslint-config["@npmcli/eslint-config"]; + npmcli-mock-globals-->npmcli-template-oss["@npmcli/template-oss"]; npmcli-mock-registry-->npm-package-arg; npmcli-mock-registry-->npmcli-arborist["@npmcli/arborist"]; npmcli-mock-registry-->npmcli-eslint-config["@npmcli/eslint-config"]; @@ -530,6 +533,7 @@ graph LR; npm-->npmcli-fs["@npmcli/fs"]; npm-->npmcli-git["@npmcli/git"]; npm-->npmcli-map-workspaces["@npmcli/map-workspaces"]; + npm-->npmcli-mock-globals["@npmcli/mock-globals"]; npm-->npmcli-mock-registry["@npmcli/mock-registry"]; npm-->npmcli-package-json["@npmcli/package-json"]; npm-->npmcli-promise-spawn["@npmcli/promise-spawn"]; @@ -621,6 +625,7 @@ graph LR; npmcli-arborist-->tcompare; npmcli-arborist-->treeverse; npmcli-arborist-->walk-up-path; + npmcli-config-->ci-info; npmcli-config-->ini; npmcli-config-->nopt; npmcli-config-->npmcli-eslint-config["@npmcli/eslint-config"]; @@ -668,6 +673,9 @@ graph LR; npmcli-metavuln-calculator-->json-parse-even-better-errors; npmcli-metavuln-calculator-->pacote; npmcli-metavuln-calculator-->semver; + npmcli-mock-globals-->npmcli-eslint-config["@npmcli/eslint-config"]; + npmcli-mock-globals-->npmcli-template-oss["@npmcli/template-oss"]; + npmcli-mock-globals-->tap; npmcli-mock-registry-->nock; npmcli-mock-registry-->npm-package-arg; npmcli-mock-registry-->npmcli-arborist["@npmcli/arborist"]; @@ -786,5 +794,5 @@ packages higher up the chain. - npm-registry-fetch, libnpmversion - @npmcli/git, make-fetch-happen, @npmcli/config, init-package-json - @npmcli/installed-package-contents, @npmcli/map-workspaces, cacache, npm-pick-manifest, @npmcli/run-script, read-package-json, promzard - - @npmcli/docs, @npmcli/fs, npm-bundled, read-package-json-fast, unique-filename, npm-install-checks, npm-package-arg, npm-packlist, normalize-package-data, @npmcli/package-json, bin-links, nopt, npmlog, parse-conflict-json, read + - @npmcli/docs, @npmcli/fs, npm-bundled, read-package-json-fast, unique-filename, npm-install-checks, npm-package-arg, npm-packlist, normalize-package-data, @npmcli/package-json, bin-links, nopt, npmlog, parse-conflict-json, read, @npmcli/mock-globals - @npmcli/eslint-config, @npmcli/template-oss, ignore-walk, semver, npm-normalize-package-bin, @npmcli/name-from-folder, json-parse-even-better-errors, fs-minipass, ssri, unique-slug, @npmcli/promise-spawn, hosted-git-info, proc-log, validate-npm-package-name, @npmcli/node-gyp, minipass-fetch, @npmcli/query, cmd-shim, read-cmd-shim, write-file-atomic, abbrev, are-we-there-yet, gauge, treeverse, minify-registry-metadata, ini, @npmcli/disparity-colors, mute-stream, npm-audit-report, npm-user-validate diff --git a/bin/npx-cli.js b/bin/npx-cli.js index 75090aed41f1f..683f0f2b451a5 100755 --- a/bin/npx-cli.js +++ b/bin/npx-cli.js @@ -24,11 +24,8 @@ const removed = new Set([ ...removedOpts, ]) -const { definitions, shorthands } = require('../lib/utils/config/index.js') -const npmSwitches = Object.entries(definitions) - .filter(([key, { type }]) => type === Boolean || - (Array.isArray(type) && type.includes(Boolean))) - .map(([key]) => key) +const { definitions, shorthands, keys } = require('@npmcli/config') +const npmSwitches = keys.filter(k => definitions[k]?.isBoolean) // things that don't take a value const switches = new Set([ diff --git a/docs/lib/index.js b/docs/lib/index.js index deb715b38107a..9cca1f13f8552 100644 --- a/docs/lib/index.js +++ b/docs/lib/index.js @@ -3,7 +3,7 @@ const { join, basename, resolve } = require('path') const transformHTML = require('./transform-html.js') const { version } = require('../../lib/npm.js') const { aliases } = require('../../lib/utils/cmd-list') -const { shorthands, definitions } = require('../../lib/utils/config/index.js') +const { shorthands, definitions } = require('@npmcli/config') const DOC_EXT = '.md' diff --git a/docs/package.json b/docs/package.json index 704ff75bd24eb..168c4b2f6f262 100644 --- a/docs/package.json +++ b/docs/package.json @@ -63,5 +63,8 @@ ".github/workflows/ci-{{ pkgNameFs }}.yml": "ci-npmcli-docs.yml" } } + }, + "dependencies": { + "@npmcli/config": "^6.1.0" } } diff --git a/lib/base-command.js b/lib/base-command.js index 3b4f7e760fdad..46f193606256d 100644 --- a/lib/base-command.js +++ b/lib/base-command.js @@ -2,7 +2,7 @@ const { relative } = require('path') -const { definitions } = require('./utils/config/definitions.js') +const { definitions } = require('@npmcli/config') const getWorkspaces = require('./workspaces/get-workspaces.js') const cmdAliases = require('./utils/cmd-list').aliases @@ -83,7 +83,7 @@ class BaseCommand { let line = '' for (const param of this.params) { - const usage = `[${definitions[param].usage}]` + const usage = `[${definitions[param].describeUsage()}]` if (line.length && line.length + usage.length > this.wrapWidth) { results = [results, line].filter(Boolean).join('\n') line = '' diff --git a/lib/cli.js b/lib/cli.js index f05c74a08a9a1..c548ab38c4469 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -84,6 +84,12 @@ module.exports = async process => { const npm = new Npm({ npmRoot: dirname(__dirname), argv: process.argv, + // This is to prevent the config module from walking past this + // directory when looking for a parent package.json. This is currently + // only used in tests but if this feature would be added in the future + // it would need to be implemented something like this since the + // directory would need to be available before configs are parsed + cwdRoot: null, }) exitHandler.setNpm(npm) diff --git a/lib/commands/adduser.js b/lib/commands/adduser.js index cd4cba60511cb..1b46e0178b6a9 100644 --- a/lib/commands/adduser.js +++ b/lib/commands/adduser.js @@ -25,7 +25,7 @@ class AddUser extends BaseCommand { } } - const creds = this.npm.config.getCredentialsByURI(registry) + const creds = this.npm.config.credentials.getByURI(registry) log.disableProgress() log.notice('', `Log in on ${replaceInfo(registry)}`) @@ -37,7 +37,7 @@ class AddUser extends BaseCommand { }) this.npm.config.delete('_token', 'user') // prevent legacy pollution - this.npm.config.setCredentialsByURI(registry, newCreds) + this.npm.config.credentials.setByURI(registry, newCreds) if (scope) { this.npm.config.set(scope + ':registry', registry, 'user') diff --git a/lib/commands/completion.js b/lib/commands/completion.js index f5604e099f9a2..862043e9ab0fb 100644 --- a/lib/commands/completion.js +++ b/lib/commands/completion.js @@ -33,13 +33,11 @@ const fs = require('fs/promises') const nopt = require('nopt') const { resolve } = require('path') -const { definitions, shorthands } = require('../utils/config/index.js') +const { definitions, shorthands, types, shortKeys, keys } = require('@npmcli/config') const { aliases, commands, plumbing } = require('../utils/cmd-list.js') const aliasNames = Object.keys(aliases) const fullList = commands.concat(aliasNames).filter(c => !plumbing.includes(c)) -const configNames = Object.keys(definitions) -const shorthandNames = Object.keys(shorthands) -const allConfs = configNames.concat(shorthandNames) +const allConfs = keys.concat(shortKeys) const { isWindowsShell } = require('../utils/is-windows.js') const fileExists = (file) => fs.stat(file).then(s => s.isFile()).catch(() => false) @@ -142,10 +140,6 @@ class Completion extends BaseCommand { // take a little shortcut and use npm's arg parsing logic. // don't have to worry about the last arg being implicitly // boolean'ed, since the last block will catch that. - const types = Object.entries(definitions).reduce((acc, [key, def]) => { - acc[key] = def.type - return acc - }, {}) const parsed = opts.conf = nopt(types, shorthands, partialWords.slice(0, -1), 0) // check if there's a command already. @@ -239,7 +233,7 @@ const configCompl = opts => { const split = word.match(/^(-+)((?:no-)*)(.*)$/) const dashes = split[1] const no = split[2] - const flags = configNames.filter(isFlag) + const flags = keys.filter(isFlag) return allConfs.map(c => dashes + c) .concat(flags.map(f => dashes + (no || 'no-') + f)) } diff --git a/lib/commands/config.js b/lib/commands/config.js index ac5a74d01f7de..16cec085f3aff 100644 --- a/lib/commands/config.js +++ b/lib/commands/config.js @@ -1,5 +1,4 @@ -// don't expand so that we only assemble the set of defaults when needed -const configDefs = require('../utils/config/index.js') +const { keys: configKeys, nerfDarts, definitions } = require('@npmcli/config') const { mkdir, readFile, writeFile } = require('fs/promises') const { dirname, resolve } = require('path') @@ -10,18 +9,6 @@ const localeCompare = require('@isaacs/string-locale-compare')('en') const rpj = require('read-package-json-fast') const log = require('../utils/log-shim.js') -// These are the configs that we can nerf-dart. Not all of them currently even -// *have* config definitions so we have to explicitly validate them here -const nerfDarts = [ - '_auth', - '_authToken', - 'username', - '_password', - 'email', - 'certfile', - 'keyfile', -] - // take an array of `[key, value, k2=v2, k3, v3, ...]` and turn into // { key: value, k2: v2, k3: v3 } const keyValues = args => { @@ -102,7 +89,7 @@ class Config extends BaseCommand { case 'get': case 'delete': case 'rm': - return Object.keys(configDefs.definitions) + return configKeys case 'edit': case 'list': case 'ls': @@ -154,10 +141,11 @@ class Config extends BaseCommand { for (const [key, val] of Object.entries(keyValues(args))) { log.info('config', 'set %j %j', key, val) const baseKey = key.split(':').pop() - if (!this.npm.config.definitions[baseKey] && !nerfDarts.includes(baseKey)) { + const def = definitions[baseKey] + if (!def && !nerfDarts.includes(baseKey)) { throw new Error(`\`${baseKey}\` is not a valid npm option`) } - const deprecated = this.npm.config.definitions[baseKey]?.deprecated + const deprecated = def[baseKey]?.deprecated if (deprecated) { throw new Error( `The \`${baseKey}\` option is deprecated, and can not be set in this way${deprecated}` @@ -204,7 +192,7 @@ class Config extends BaseCommand { async edit () { const e = this.npm.flatOptions.editor const where = this.npm.flatOptions.location - const file = this.npm.config.data.get(where).source + const { file } = this.npm.config.data.get(where) // save first, just to make sure it's synced up // this also removes all the comments from the last time we edited it. @@ -213,7 +201,7 @@ class Config extends BaseCommand { const data = ( await readFile(file, 'utf8').catch(() => '') ).replace(/\r\n/g, '\n') - const entries = Object.entries(configDefs.defaults) + const entries = Object.entries(this.npm.config.defaults) const defData = entries.reduce((str, [key, val]) => { const obj = { [key]: val } const i = ini.stringify(obj) diff --git a/lib/commands/doctor.js b/lib/commands/doctor.js index 89aaed2f81d21..75a0eb18e5202 100644 --- a/lib/commands/doctor.js +++ b/lib/commands/doctor.js @@ -7,13 +7,13 @@ const pacote = require('pacote') const { resolve } = require('path') const semver = require('semver') const { promisify } = require('util') +const { Types } = require('@npmcli/configs') const log = require('../utils/log-shim.js') const ping = require('../utils/ping.js') const lstat = promisify(fs.lstat) const readdir = promisify(fs.readdir) const access = promisify(fs.access) const { R_OK, W_OK, X_OK } = fs.constants -const { registry: defaultRegistry } = require('../utils/config').defaults const maskLabel = mask => { const label = [] @@ -362,6 +362,7 @@ class Doctor extends BaseCommand { } async checkNpmRegistry () { + const defaultRegistry = this.npm.config.get('registry', Types.default) if (this.npm.flatOptions.registry !== defaultRegistry) { throw `Try \`npm config set registry=${defaultRegistry}\`` } else { diff --git a/lib/commands/login.js b/lib/commands/login.js index dc4ed8a67acd9..cf1cae49dee6e 100644 --- a/lib/commands/login.js +++ b/lib/commands/login.js @@ -25,7 +25,7 @@ class Login extends BaseCommand { } } - const creds = this.npm.config.getCredentialsByURI(registry) + const creds = this.npm.config.credentials.getByURI(registry) log.disableProgress() log.notice('', `Log in on ${replaceInfo(registry)}`) @@ -37,7 +37,7 @@ class Login extends BaseCommand { }) this.npm.config.delete('_token', 'user') // prevent legacy pollution - this.npm.config.setCredentialsByURI(registry, newCreds) + this.npm.config.credentials.setByURI(registry, newCreds) if (scope) { this.npm.config.set(scope + ':registry', registry, 'user') diff --git a/lib/commands/logout.js b/lib/commands/logout.js index aea5e93652b0e..a50afe2718a44 100644 --- a/lib/commands/logout.js +++ b/lib/commands/logout.js @@ -37,7 +37,7 @@ class Logout extends BaseCommand { this.npm.config.delete(regRef, 'user') } - this.npm.config.clearCredentialsByURI(reg) + this.npm.config.credentials.clearByURI(reg) await this.npm.config.save('user') } diff --git a/lib/commands/profile.js b/lib/commands/profile.js index e42ebb276d202..c0981beb50158 100644 --- a/lib/commands/profile.js +++ b/lib/commands/profile.js @@ -263,7 +263,7 @@ class Profile extends BaseCommand { // if they're using legacy auth currently then we have to // update them to a bearer token before continuing. - const creds = this.npm.config.getCredentialsByURI(this.npm.config.get('registry')) + const creds = this.npm.config.credentials.getByURI(this.npm.config.get('registry')) const auth = {} if (creds.token) { @@ -296,7 +296,7 @@ class Profile extends BaseCommand { ) } - this.npm.config.setCredentialsByURI( + this.npm.config.credentials.setByURI( this.npm.config.get('registry'), { token: result.token } ) diff --git a/lib/commands/publish.js b/lib/commands/publish.js index 76faea9457f74..5965064771880 100644 --- a/lib/commands/publish.js +++ b/lib/commands/publish.js @@ -8,16 +8,9 @@ const pacote = require('pacote') const npa = require('npm-package-arg') const npmFetch = require('npm-registry-fetch') const replaceInfo = require('../utils/replace-info.js') - const otplease = require('../utils/otplease.js') const { getContents, logTar } = require('../utils/tar.js') -// for historical reasons, publishConfig in package.json can contain ANY config -// keys that npm supports in .npmrc files and elsewhere. We *may* want to -// revisit this at some point, and have a minimal set that's a SemVer-major -// change that ought to get a RFC written on it. -const { flatten } = require('../utils/config/index.js') - // this is the only case in the CLI where we want to use the old full slow // 'read-package-json' module, because we want to pull in all the defaults and // metadata, like git sha's and default scripts and all that. @@ -102,7 +95,7 @@ class Publish extends BaseCommand { const resolved = npa.resolve(manifest.name, manifest.version) const registry = npmFetch.pickRegistry(resolved, opts) - const creds = this.npm.config.getCredentialsByURI(registry) + const creds = this.npm.config.credentials.getByURI(registry) const noCreds = !(creds.token || creds.username || creds.certfile && creds.keyfile) const outputRegistry = replaceInfo(registry) @@ -212,7 +205,11 @@ class Publish extends BaseCommand { }) } if (manifest.publishConfig) { - flatten(manifest.publishConfig, opts) + // for historical reasons, publishConfig in package.json can contain ANY config + // keys that npm supports in .npmrc files and elsewhere. We *may* want to + // revisit this at some point, and have a minimal set that's a SemVer-major + // change that ought to get a RFC written on it. + this.npm.config.load('publishConfig', manifest.publishConfig) } return manifest } diff --git a/lib/commands/token.js b/lib/commands/token.js index 8da8311875714..021a708508a32 100644 --- a/lib/commands/token.js +++ b/lib/commands/token.js @@ -160,7 +160,7 @@ class Token extends BaseCommand { config () { const conf = { ...this.npm.flatOptions } - const creds = this.npm.config.getCredentialsByURI(conf.registry) + const creds = this.npm.config.credentials.getByURI(conf.registry) if (creds.token) { conf.auth = { token: creds.token } } else if (creds.username) { diff --git a/lib/commands/unpublish.js b/lib/commands/unpublish.js index 9985e2e39f140..e2a523dc3ed7a 100644 --- a/lib/commands/unpublish.js +++ b/lib/commands/unpublish.js @@ -6,7 +6,6 @@ const path = require('path') const util = require('util') const readJson = util.promisify(require('read-package-json')) -const { flatten } = require('../utils/config/index.js') const getIdentity = require('../utils/get-identity.js') const log = require('../utils/log-shim') const otplease = require('../utils/otplease.js') @@ -86,8 +85,6 @@ class Unpublish extends BaseCommand { ) } - const opts = { ...this.npm.flatOptions } - let pkgName let pkgVersion let manifest @@ -102,9 +99,9 @@ class Unpublish extends BaseCommand { // If cwd has a package.json with a name that matches the package being // unpublished, load up the publishConfig if (manifest && manifest.name === spec.name && manifest.publishConfig) { - flatten(manifest.publishConfig, opts) + this.npm.config.load('publishConfig', manifest.publishConfig) } - const versions = await this.getKeysOfVersions(spec.name, opts) + const versions = await this.getKeysOfVersions(spec.name, { ...this.npm.flatOptions }) if (versions.length === 1 && !force) { throw this.usageError(LAST_REMAINING_VERSION_ERROR) } @@ -123,13 +120,15 @@ class Unpublish extends BaseCommand { spec = npa.resolve(manifest.name, manifest.version) if (manifest.publishConfig) { - flatten(manifest.publishConfig, opts) + this.npm.config.load('publishConfig', manifest.publishConfig) } pkgName = manifest.name pkgVersion = manifest.version ? `@${manifest.version}` : '' } + const opts = { ...this.npm.flatOptions } + if (!dryRun) { await otplease(this.npm, opts, o => libunpub(spec, o)) } diff --git a/lib/npm.js b/lib/npm.js index d82a283e2da0c..9b2e9fba22067 100644 --- a/lib/npm.js +++ b/lib/npm.js @@ -1,5 +1,4 @@ -const Arborist = require('@npmcli/arborist') -const { resolve, join } = require('path') +const { resolve } = require('path') const Config = require('@npmcli/config') const chalk = require('chalk') const fs = require('fs/promises') @@ -7,7 +6,6 @@ const fs = require('fs/promises') // Patch the global fs module here at the app level require('graceful-fs').gracefulify(require('fs')) -const { definitions, flatten, shorthands, derived } = require('./utils/config/index.js') const usage = require('./utils/npm-usage.js') const LogFile = require('./utils/log-file.js') const Timers = require('./utils/timers.js') @@ -29,7 +27,6 @@ class Npm { #command = null #runId = new Date().toISOString().replace(/[.:]/g, '_') #loadPromise = null - #tmpFolder = null #title = 'npm' #argvClean = [] #chalk = null @@ -50,15 +47,7 @@ class Npm { constructor ({ npmRoot, argv, cwdRoot }) { this.#npmRoot = npmRoot - this.config = new Config({ - npmPath: this.#npmRoot, - derived, - definitions, - flatten, - shorthands, - argv, - cwdRoot, - }) + this.config = new Config({ npmRoot, argv, cwdRoot }) } get version () { @@ -195,7 +184,7 @@ class Npm { // its ok if this fails. user might have specified an invalid dir // which we will tell them about at the end await this.time('npm:load:mkdirplogs', () => - fs.mkdir(this.logsDir, { recursive: true }) + fs.mkdir(this.flatOptions.logsDir, { recursive: true }) .catch((e) => log.verbose('logfile', `could not create logs-dir: ${e}`))) // note: this MUST be shorter than the actual argv length, because it @@ -227,8 +216,6 @@ class Npm { unicode: this.config.get('unicode'), heading: this.config.get('heading'), }) - // TODO: move to config - process.env.COLOR = this.color ? '1' : '0' }) this.time('npm:load:logFile', () => { @@ -245,12 +232,10 @@ class Npm { }) ) - this.time('npm:load:configScope', () => { - const configScope = this.config.get('scope') - if (configScope && !/^@/.test(configScope)) { - this.config.set('scope', `@${configScope}`, this.config.find('scope')) - } - }) + const configScope = this.config.get('scope') + if (!/^@/.test(configScope)) { + this.config.set('scope', `@${configScope}`, this.config.find('scope')) + } if (this.config.get('force')) { log.warn('using --force', 'Recommended protections disabled.') @@ -267,12 +252,6 @@ class Npm { get flatOptions () { const { flat } = this.config - // the Arborist constructor is used almost everywhere we call pacote, it's - // easiest to attach it to flatOptions so it goes everywhere without having - // to touch every call - flat.Arborist = Arborist - flat.nodeVersion = process.version - flat.npmVersion = pkg.version if (this.command) { flat.npmCommand = this.command } @@ -330,7 +309,7 @@ class Npm { } get logsDir () { - return this.config.get('logs-dir') || join(this.cache, '_logs') + return this.flatOptions.logsDir } get logPath () { diff --git a/lib/utils/config/definition.js b/lib/utils/config/definition.js deleted file mode 100644 index 52d81a5489113..0000000000000 --- a/lib/utils/config/definition.js +++ /dev/null @@ -1,237 +0,0 @@ -// class that describes a config key we know about -// this keeps us from defining a config key and not -// providing a default, description, etc. -// -// TODO: some kind of categorization system, so we can -// say "these are for registry access", "these are for -// version resolution" etc. - -const { TypeDefs } = require('@npmcli/config') -const { wrap, unindent } = require('../wrap') - -const required = ['type', 'description', 'default'] - -const allowed = [ - 'default', - 'defaultDescription', - 'deprecated', - 'description', - 'flatten', - 'hint', - 'short', - 'type', - 'usage', - 'envExport', -] - -class Definition { - #key = null - #def = null - - constructor (key, def) { - this.#key = key - this.#def = def - this.validate() - } - - get default () { - return this.#def.default - } - - get type () { - return this.#def.type - } - - get deprecated () { - return this.#def.deprecated - } - - get envExport () { - // if it's set falsey, don't export it, otherwise we do by default - return this.#def.envExport ?? true - } - - get flatten () { - return this.#def.flatten - } - - get derived () { - return this.#def.derived - } - - // get type () { - // return Array.isArray(this.#def.type) - // ? this.#def.type.map((t) => TypeDefs.getType(t) ?? t) - // : TypeDefs.getType(this.#def.type) ?? this.#def.type - // } - - get hint () { - // hint is only used for non-boolean values - return this.#def.hint ?? `<${TypeDefs.isNumber(this.#def.type) ? 'number' : this.#key}>` - } - - get usage () { - return this.#def.usage ?? describeUsage({ ...this.#def, type: this.type }) - } - - get defaultDescription () { - return this.#def.defaultDescription ?? describeValue(this.#def.default) - } - - get typeDescription () { - return describeTypes(this.type) - } - - validate () { - for (const req of required) { - if (!Object.prototype.hasOwnProperty.call(this.#def, req)) { - throw new Error(`config \`${this.#key}\` lacks key \`${req}\``) - } - } - if (!this.#key) { - throw new Error(`config lacks key: ${this.#key}`) - } - for (const field of Object.keys(this.#def)) { - if (!allowed.includes(field)) { - throw new Error(`config defines unknown field ${field}: ${this.#key}`) - } - } - } - - // a textual description of this config, suitable for help output - describe () { - const noEnvExport = this.envExport - ? '' - : 'This value is not exported to the environment for child processes.' - - const sections = [ - ['Default', this.defaultDescription], - ['Type', this.typeDescription], - this.deprecated ? ['DEPRECATED', this.deprecated] : null, - this.description, - noEnvExport, - ].filter(Boolean).map((s) => { - if (Array.isArray(s)) { - return `* ${s[0]}: ${unindent(s[1])}` - } - return unindent(s) - }) - - return wrapAll(`#### \`${this.key}\`\n\n${sections.join('\n')}`) - } -} - -const describeUsage = def => { - let key = '' - - // Single type - if (!Array.isArray(def.type)) { - if (def.short) { - key = `-${def.short}|` - } - - if (TypeDefs.isBoolean(def.type) && def.default !== false) { - key = `${key}--no-${def.key}` - } else { - key = `${key}--${def.key}` - } - - if (!TypeDefs.isBoolean(def.type)) { - key = `${key} ${def.hint}` - } - - return key - } - - key = `--${def.key}` - if (def.short) { - key = `-${def.short}|--${def.key}` - } - - // Multiple types - let types = def.type - const multiple = types.includes(Array) - const bool = TypeDefs.isBoolean(types) - - // null type means optional and doesn't currently affect usage output since - // all non-optional params have defaults so we render everything as optional - types = types.filter(t => t !== null && t !== Array && !TypeDefs.isBoolean(t)) - - if (!types.length) { - return key - } - - let description - if (!types.some(t => typeof t !== 'string')) { - // Specific values, use specifics given - description = `<${types.filter(d => d).join('|')}>` - } else { - // Generic values, use hint - description = def.hint - } - - if (bool) { - // Currently none of our multi-type configs with boolean values default to - // false so all their hints should show `--no-`, if we ever add ones that - // default to false we can branch the logic here - key = `--no-${def.key}|${key}` - } - - const usage = `${key} ${description}` - if (multiple) { - return `${usage} [${usage} ...]` - } else { - return usage - } -} - -const describeTypes = types => { - const descriptions = [].concat(types).filter(t => t !== Array).map(t => describeType(t)) - - // [a] => "a" - // [a, b] => "a or b" - // [a, b, c] => "a, b, or c" - // [a, Array] => "a (can be set multiple times)" - // [a, Array, b] => "a or b (can be set multiple times)" - const last = descriptions.length > 1 ? [descriptions.pop()] : [] - const oxford = descriptions.length > 1 ? ', or ' : ' or ' - const words = [descriptions.join(', ')].concat(last).join(oxford) - const multiple = types.includes(Array) ? ' (can be set multiple times)' : '' - return `${words}${multiple}` -} - -const describeType = (type) => type.typeDescription || describeValue(type) - -// if it's a string, quote it. otherwise, just cast to string. -const describeValue = val => (typeof val === 'string' ? JSON.stringify(val) : String(val)) - -const wrapAll = s => { - let inCodeBlock = false - return s - .split('\n\n') - .map(block => { - if (inCodeBlock || block.startsWith('```')) { - inCodeBlock = !block.endsWith('```') - return block - } - - if (block.charAt(0) === '*') { - return ( - '* ' + - block - .slice(1) - .trim() - .split('\n* ') - .map(li => { - return wrap(li).replace(/\n/g, '\n ') - }) - .join('\n* ') - ) - } else { - return wrap(block) - } - }) - .join('\n\n') -} - -module.exports = Definition diff --git a/lib/utils/config/index.js b/lib/utils/config/index.js deleted file mode 100644 index 18837c9529ba7..0000000000000 --- a/lib/utils/config/index.js +++ /dev/null @@ -1,79 +0,0 @@ -const { definitions, derived } = require('./definitions.js') - -// use the defined flattening function, and copy over any scoped -// registries and registry-specific "nerfdart" configs verbatim -// -// TODO: make these getters so that we only have to make dirty -// the thing that changed, and then flatten the fields that -// could have changed when a config.set is called. -// -// TODO: move nerfdart auth stuff into a nested object that -// is only passed along to paths that end up calling npm-registry-fetch. -const flatten = (obj, flat = {}) => { - for (const [key, val] of Object.entries(obj)) { - const def = definitions[key] - if (def?.flatten) { - def.flatten(key, obj, flat) - } else if (/@.*:registry$/i.test(key) || /^\/\//.test(key)) { - flat[key] = val - } - } - - // XXX make this the bin/npm-cli.js file explicitly instead - // otherwise using npm programmatically is a bit of a pain. - flat.npmBin = require.main ? require.main.filename - : /* istanbul ignore next - not configurable property */ undefined - flat.nodeBin = process.env.NODE || process.execPath - - // XXX should this be sha512? is it even relevant? - flat.hashAlgorithm = 'sha1' - - return flat -} - -// aliases where they get expanded into a completely different thing -// these are NOT supported in the environment or npmrc files, only -// expanded on the CLI. -// TODO: when we switch off of nopt, use an arg parser that supports -// more reasonable aliasing and short opts right in the definitions set. -const shorthands = { - 'enjoy-by': ['--before'], - d: ['--loglevel', 'info'], - dd: ['--loglevel', 'verbose'], - ddd: ['--loglevel', 'silly'], - quiet: ['--loglevel', 'warn'], - q: ['--loglevel', 'warn'], - s: ['--loglevel', 'silent'], - silent: ['--loglevel', 'silent'], - verbose: ['--loglevel', 'verbose'], - desc: ['--description'], - help: ['--usage'], - local: ['--no-global'], - n: ['--no-yes'], - no: ['--no-yes'], - porcelain: ['--parseable'], - readonly: ['--read-only'], - reg: ['--registry'], - iwr: ['--include-workspace-root'], - ...Object.entries(definitions).reduce((acc, [key, { short = [] }]) => { - // can be either an array or string - for (const s of [].concat(short)) { - acc[s] = [`--${key}`] - } - return acc - }, {}), -} - -module.exports = { - get defaults () { - // NB: 'default' is a reserved word - return Object.entries(definitions).reduce((acc, [key, { default: d }]) => { - acc[key] = d - return acc - }, {}) - }, - definitions, - flatten, - shorthands, - derived, -} diff --git a/lib/utils/get-identity.js b/lib/utils/get-identity.js index d8f59da14247a..3dc615d834d8c 100644 --- a/lib/utils/get-identity.js +++ b/lib/utils/get-identity.js @@ -4,7 +4,7 @@ module.exports = async (npm, opts) => { const { registry } = opts // First, check if we have a user/pass-based auth - const creds = npm.config.getCredentialsByURI(registry) + const creds = npm.config.credentials.getByURI(registry) if (creds.username) { return creds.username } diff --git a/lib/utils/update-notifier.js b/lib/utils/update-notifier.js index 8625bb2b82900..34cf703ce52f6 100644 --- a/lib/utils/update-notifier.js +++ b/lib/utils/update-notifier.js @@ -5,7 +5,6 @@ const pacote = require('pacote') const ciInfo = require('ci-info') const semver = require('semver') -const chalk = require('chalk') const { stat, writeFile } = require('fs/promises') const { resolve } = require('path') @@ -62,8 +61,6 @@ const updateNotifier = async (npm, spec = 'latest') => { // if they're currently using a prerelease, nudge to the next prerelease // otherwise, nudge to latest. - const useColor = npm.logColor - const mani = await pacote.manifest(`npm@${spec}`, { // always prefer latest, even if doing --tag=whatever on the cmd defaultTag: 'latest', @@ -98,22 +95,15 @@ const updateNotifier = async (npm, spec = 'latest') => { : update.minor !== current.minor ? 'minor' : update.patch !== current.patch ? 'patch' : 'prerelease' - const typec = !useColor ? type - : type === 'major' ? chalk.red(type) - : type === 'minor' ? chalk.yellow(type) - : chalk.green(type) - const oldc = !useColor ? current : chalk.red(current) - const latestc = !useColor ? latest : chalk.green(latest) - const changelog = `https://github.com/npm/cli/releases/tag/v${latest}` - const changelogc = !useColor ? `<${changelog}>` : chalk.cyan(changelog) + const typeColor = type === 'major' ? 'red' : type === 'minor' ? 'yellow' : 'green' const cmd = `npm install -g npm@${latest}` - const cmdc = !useColor ? `\`${cmd}\`` : chalk.green(cmd) - const message = `\nNew ${typec} version of npm available! ` + - `${oldc} -> ${latestc}\n` + - `Changelog: ${changelogc}\n` + - `Run ${cmdc} to update!\n` + const changelog = `https://github.com/npm/cli/releases/tag/v${latest}` - return message + const { chalk } = npm + return `\nNew ${chalk[typeColor](type)} version of npm available! ` + + `${chalk.red(current)} -> ${chalk.green(latest)}\n` + + `Changelog: ${chalk.cyan(changelog)}\n` + + `Run ${chalk.green(cmd)} to update!\n` } // only update the notification timeout if we actually finished checking diff --git a/mock-globals/.eslintrc.js b/mock-globals/.eslintrc.js new file mode 100644 index 0000000000000..5db9f815536f1 --- /dev/null +++ b/mock-globals/.eslintrc.js @@ -0,0 +1,17 @@ +/* This file is automatically added by @npmcli/template-oss. Do not edit. */ + +'use strict' + +const { readdirSync: readdir } = require('fs') + +const localConfigs = readdir(__dirname) + .filter((file) => file.startsWith('.eslintrc.local.')) + .map((file) => `./${file}`) + +module.exports = { + root: true, + extends: [ + '@npmcli', + ...localConfigs, + ], +} diff --git a/mock-globals/.gitignore b/mock-globals/.gitignore new file mode 100644 index 0000000000000..79af2bfcaa4d8 --- /dev/null +++ b/mock-globals/.gitignore @@ -0,0 +1,21 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +# ignore everything in the root +/* + +# keep these +!**/.gitignore +!/.eslintrc.js +!/.eslintrc.local.* +!/.gitignore +!/bin/ +!/CHANGELOG* +!/docs/ +!/lib/ +!/LICENSE* +!/map.js +!/package.json +!/README* +!/scripts/ +!/tap-snapshots/ +!/test/ diff --git a/test/fixtures/mock-globals.js b/mock-globals/lib/index.js similarity index 100% rename from test/fixtures/mock-globals.js rename to mock-globals/lib/index.js diff --git a/mock-globals/package.json b/mock-globals/package.json new file mode 100644 index 0000000000000..705f7b86a036c --- /dev/null +++ b/mock-globals/package.json @@ -0,0 +1,50 @@ +{ + "name": "@npmcli/mock-globals", + "version": "1.0.0", + "description": "", + "main": "index.js", + "private": true, + "scripts": { + "test": "tap", + "lint": "eslint \"**/*.js\"", + "postlint": "template-oss-check", + "template-oss-apply": "template-oss-apply --force", + "lintfix": "node .. run lint -- --fix", + "snap": "tap", + "posttest": "node .. run lint" + }, + "repository": { + "type": "git", + "url": "https://github.com/npm/cli.git", + "directory": "mock-globals" + }, + "keywords": [], + "author": "GitHub Inc.", + "license": "ISC", + "bugs": { + "url": "https://github.com/npm/cli/issues" + }, + "homepage": "https://github.com/npm/cli#readme", + "files": [ + "bin/", + "lib/" + ], + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "templateOSS": { + "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", + "version": "4.11.0" + }, + "tap": { + "nyc-arg": [ + "--exclude", + "tap-snapshots/**" + ] + }, + "devDependencies": { + "@npmcli/eslint-config": "^4.0.1", + "@npmcli/template-oss": "4.11.0", + "tap": "^16.3.2" + } +} diff --git a/test/lib/fixtures/mock-globals.js b/mock-globals/test/index.js similarity index 99% rename from test/lib/fixtures/mock-globals.js rename to mock-globals/test/index.js index 55418dd8e199d..5480801196301 100644 --- a/test/lib/fixtures/mock-globals.js +++ b/mock-globals/test/index.js @@ -1,5 +1,5 @@ const t = require('tap') -const mockGlobals = require('../../fixtures/mock-globals') +const mockGlobals = require('..') /* eslint-disable no-console */ const originals = { diff --git a/node_modules/ci-info/LICENSE b/node_modules/ci-info/LICENSE index 7487192062871..44ca33aa611e7 100644 --- a/node_modules/ci-info/LICENSE +++ b/node_modules/ci-info/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016-2022 Thomas Watson Steen +Copyright (c) 2016-2023 Thomas Watson Steen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/node_modules/ci-info/index.js b/node_modules/ci-info/index.js index e91c518557897..47907264581eb 100644 --- a/node_modules/ci-info/index.js +++ b/node_modules/ci-info/index.js @@ -54,7 +54,8 @@ vendors.forEach(function (vendor) { }) exports.isCI = !!( - env.BUILD_ID || // Jenkins, Cloudbees + env.CI !== 'false' && // Bypass all checks if CI env is explicitly set to 'false' + (env.BUILD_ID || // Jenkins, Cloudbees env.BUILD_NUMBER || // Jenkins, TeamCity env.CI || // Travis CI, CircleCI, Cirrus CI, Gitlab CI, Appveyor, CodeShip, dsari env.CI_APP_ID || // Appflow @@ -64,7 +65,7 @@ exports.isCI = !!( env.CONTINUOUS_INTEGRATION || // Travis CI, Cirrus CI env.RUN_ID || // TaskCluster, dsari exports.name || - false + false) ) function checkEnv (obj) { diff --git a/node_modules/ci-info/package.json b/node_modules/ci-info/package.json index cadaa15026239..e05f375faf20e 100644 --- a/node_modules/ci-info/package.json +++ b/node_modules/ci-info/package.json @@ -1,6 +1,6 @@ { "name": "ci-info", - "version": "3.7.0", + "version": "3.7.1", "description": "Get details about the current Continuous Integration environment", "main": "index.js", "typings": "index.d.ts", @@ -22,6 +22,12 @@ "index.d.ts", "CHANGELOG.md" ], + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], "scripts": { "lint:fix": "standard --fix", "test": "standard && node test.js", diff --git a/package-lock.json b/package-lock.json index 905364f98d0b9..d7a9701ad27f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,7 +81,8 @@ "docs", "smoke-tests", "mock-registry", - "workspaces/*" + "workspaces/*", + "mock-globals" ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", @@ -181,6 +182,9 @@ "name": "@npmcli/docs", "version": "1.0.0", "license": "ISC", + "dependencies": { + "@npmcli/config": "^6.1.0" + }, "devDependencies": { "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/eslint-config": "^4.0.0", @@ -202,6 +206,19 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "mock-globals": { + "name": "@npmcli/mock-globals", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@npmcli/eslint-config": "^4.0.1", + "@npmcli/template-oss": "4.11.0", + "tap": "^16.3.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "mock-registry": { "name": "@npmcli/mock-registry", "version": "1.0.0", @@ -2189,6 +2206,10 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@npmcli/mock-globals": { + "resolved": "mock-globals", + "link": true + }, "node_modules/@npmcli/mock-registry": { "resolved": "mock-registry", "link": true @@ -3293,9 +3314,15 @@ } }, "node_modules/ci-info": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.0.tgz", - "integrity": "sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.1.tgz", + "integrity": "sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], "inBundle": true, "engines": { "node": ">=8" @@ -14365,7 +14392,9 @@ "version": "6.1.0", "license": "ISC", "dependencies": { + "@npmcli/arborist": "^6.1.5", "@npmcli/map-workspaces": "^3.0.0", + "ci-info": "^3.7.1", "ini": "^3.0.0", "nopt": "file:../../../nopt/nopt-7.0.0.tgz", "proc-log": "^3.0.0", diff --git a/package.json b/package.json index 982428da042c3..1191366ea08f4 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "docs", "smoke-tests", "mock-registry", - "workspaces/*" + "workspaces/*", + "mock-globals" ], "files": [ "bin/", @@ -244,9 +245,11 @@ "--exclude", "workspaces/**", "--exclude", + "mock-globals/**", + "--exclude", "tap-snapshots/**" ], - "test-ignore": "^(docs|smoke-tests|mock-registry|workspaces)/" + "test-ignore": "^(docs|smoke-tests|mock-registry|workspaces|mock-globals)/" }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", diff --git a/test/lib/utils/update-notifier.js b/test/lib/utils/update-notifier.js index 3a05f95aa7926..386541b84e8a7 100644 --- a/test/lib/utils/update-notifier.js +++ b/test/lib/utils/update-notifier.js @@ -18,11 +18,11 @@ const runUpdateNotifier = async (t, { WRITE_ERROR, PACOTE_ERROR, STAT_MTIME, - mocks = {}, - color = true, - command = null, - exec= [], - ...npmOptions + mocks: _mocks = {}, + command = 'view', + version = CURRENT_VERSION, + argv = [], + ...config } = {}) => { const mockFs = { ...require('fs/promises'), @@ -58,194 +58,152 @@ const runUpdateNotifier = async (t, { if (PACOTE_ERROR) { throw PACOTE_ERROR } - const version = spec === 'npm@latest' ? CURRENT_VERSION : /-/.test(spec) ? - CURRENT_BETA : - NEXT_VERSION - return { version } + const manifestV = spec === 'npm@latest' ? CURRENT_VERSION + : /-/.test(spec) ? CURRENT_BETA : NEXT_VERSION + return { version: manifestV } }, } + const mocks = { + pacote: mockPacote, + fs: mockFs, + '{ROOT}/package.json': { version }, + ..._mocks, + } + const mock = await mockNpm(t, { command, - mocks: { - pacote: mockPacote, - fs: mockFs, - ...mocks, - }, + mocks, + config, + argv, }) + const updateNotifier = tmock(t, '{LIB}/utils/update-notifier.js', mocks) - if (mock[command]) { - await mock[command].exec(exec) - } + await mock[command].cmd() - const updateNotifier = tmock(t, '{LIB}/utils/update-notifier.js', { - pacote: mockPacote, - fs: mockFs, - ...mocks, - }) + const result = await updateNotifier(mock.npm) + + return { + result, + MANIFEST_REQUEST, + } } +t.test('notifies by default', async t => { + const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t) + t.ok(result) + t.equal(MANIFEST_REQUEST.length, 1) +}) + t.test('situations in which we do not notify', t => { t.test('nothing to do if notifier disabled', async t => { - t.equal( - await runUpdateNotifier(t, { - config: { get: k => k !== 'update-notifier' }, - }), - null - ) + const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { + 'update-notifier': false, + }) + t.equal(result, null) t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests') }) t.test('do not suggest update if already updating', async t => { - t.equal( - await runUpdateNotifier(t, { - flatOptions: { ...flatOptions, global: true }, - command: 'install', - argv: ['npm'], - }), - null - ) + const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { + command: 'install', + argv: ['npm'], + global: true, + }) + t.equal(result, null) t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests') }) t.test('do not suggest update if already updating with spec', async t => { - t.equal( - await runUpdateNotifier(t, { - flatOptions: { ...flatOptions, global: true }, - command: 'install', - argv: ['npm@latest'], - }), - null - ) + const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { + command: 'install', + argv: ['npm@latest'], + global: true, + }) + t.equal(result, null) t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests') }) t.test('do not update if same as latest', async t => { - t.equal(await runUpdateNotifier(t), null) + const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t) + t.equal(result, null) t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version') }) t.test('check if stat errors (here for coverage)', async t => { - STAT_ERROR = new Error('blorg') - t.equal(await runUpdateNotifier(t), null) + const STAT_ERROR = new Error('blorg') + const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { STAT_ERROR }) + t.equal(result, null) t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version') }) t.test('ok if write errors (here for coverage)', async t => { - WRITE_ERROR = new Error('grolb') - t.equal(await runUpdateNotifier(t), null) + const WRITE_ERROR = new Error('grolb') + const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { WRITE_ERROR }) + t.equal(result, null) t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version') }) t.test('ignore pacote failures (here for coverage)', async t => { - PACOTE_ERROR = new Error('pah-KO-tchay') - t.equal(await runUpdateNotifier(t), null) + const PACOTE_ERROR = new Error('pah-KO-tchay') + const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { PACOTE_ERROR }) + t.equal(result, null) t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version') }) t.test('do not update if newer than latest, but same as next', async t => { - t.equal(await runUpdateNotifier(t, { version: NEXT_VERSION }), null) + const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { version: NEXT_VERSION }) + t.equal(result, null) const reqs = ['npm@latest', `npm@^${NEXT_VERSION}`] t.strictSame(MANIFEST_REQUEST, reqs, 'requested latest and next versions') }) t.test('do not update if on the latest beta', async t => { - t.equal(await runUpdateNotifier(t, { version: CURRENT_BETA }), null) + const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { version: CURRENT_BETA }) + t.equal(result, null) const reqs = [`npm@^${CURRENT_BETA}`] t.strictSame(MANIFEST_REQUEST, reqs, 'requested latest and next versions') }) t.test('do not update in CI', async t => { - t.teardown(() => { - ciMock = {} - }) - ciMock = { isCI: true, name: 'something' } - t.equal(await runUpdateNotifier(t), null) + const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { mocks: { + 'ci-info': { isCI: true, name: 'something' }, + } }) + t.equal(result, null) t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests') }) t.test('only check weekly for GA releases', async t => { // One week (plus five minutes to account for test environment fuzziness) - STAT_MTIME = Date.now() - 1000 * 60 * 60 * 24 * 7 + 1000 * 60 * 5 - t.equal(await runUpdateNotifier(t), null) + const STAT_MTIME = Date.now() - 1000 * 60 * 60 * 24 * 7 + 1000 * 60 * 5 + const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { STAT_MTIME }) + t.equal(result, null) t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests') }) t.test('only check daily for betas', async t => { // One day (plus five minutes to account for test environment fuzziness) - STAT_MTIME = Date.now() - 1000 * 60 * 60 * 24 + 1000 * 60 * 5 - t.equal(await runUpdateNotifier(t, { version: HAVE_BETA }), null) - t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests') + const STAT_MTIME = Date.now() - 1000 * 60 * 60 * 24 + 1000 * 60 * 5 + const res = await runUpdateNotifier(t, { STAT_MTIME, version: HAVE_BETA }) + t.equal(res.result, null) + t.strictSame(res.MANIFEST_REQUEST, [], 'no requests for manifests') }) t.end() }) -t.test('notification situations', t => { - t.test('new beta available', async t => { - const version = HAVE_BETA - t.matchSnapshot(await runUpdateNotifier(t, { version }), 'color') - t.matchSnapshot( - await runUpdateNotifier(t, { version, color: false }), - 'no color' - ) - t.strictSame(MANIFEST_REQUEST, [`npm@^${version}`, `npm@^${version}`]) - }) - - t.test('patch to next version', async t => { - const version = NEXT_PATCH - t.matchSnapshot(await runUpdateNotifier(t, { version }), 'color') - t.matchSnapshot( - await runUpdateNotifier(t, { version, color: false }), - 'no color' - ) - t.strictSame(MANIFEST_REQUEST, [ - 'npm@latest', - `npm@^${version}`, - 'npm@latest', - `npm@^${version}`, - ]) - }) - - t.test('minor to next version', async t => { - const version = NEXT_MINOR - t.matchSnapshot(await runUpdateNotifier(t, { version }), 'color') - t.matchSnapshot( - await runUpdateNotifier(t, { version, color: false }), - 'no color' - ) - t.strictSame(MANIFEST_REQUEST, [ - 'npm@latest', - `npm@^${version}`, - 'npm@latest', - `npm@^${version}`, - ]) - }) - - t.test('patch to current', async t => { - const version = CURRENT_PATCH - t.matchSnapshot(await runUpdateNotifier(t, { version }), 'color') - t.matchSnapshot( - await runUpdateNotifier(t, { version, color: false }), - 'no color' - ) - t.strictSame(MANIFEST_REQUEST, ['npm@latest', 'npm@latest']) - }) - - t.test('minor to current', async t => { - const version = CURRENT_MINOR - t.matchSnapshot(await runUpdateNotifier(t, { version }), 'color') - t.matchSnapshot( - await runUpdateNotifier(t, { version, color: false }), - 'no color' - ) - t.strictSame(MANIFEST_REQUEST, ['npm@latest', 'npm@latest']) - }) - - t.test('major to current', async t => { - const version = CURRENT_MAJOR - t.matchSnapshot(await runUpdateNotifier(t, { version }), 'color') - t.matchSnapshot( - await runUpdateNotifier(t, { version, color: false }), - 'no color' - ) - t.strictSame(MANIFEST_REQUEST, ['npm@latest', 'npm@latest']) - }) +t.test('notification situations', async t => { + const cases = { + [HAVE_BETA]: [`^{V}`, `^{V}`], + [NEXT_PATCH]: [`latest`, `^{V}`, 'latest', `^{V}`], + [NEXT_MINOR]: [`latest`, `^{V}`, 'latest', `^{V}`], + [CURRENT_PATCH]: ['latest', 'latest'], + [CURRENT_MINOR]: ['latest', 'latest'], + [CURRENT_MAJOR]: ['latest', 'latest'], + } - t.end() + for (const [version, reqs] of Object.entries(cases)) { + for (const color of [false, 'always']) { + await t.test(`${version} - color=${color}`, async t => { + const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { version, color }) + t.matchSnapshot(result) + t.strictSame(MANIFEST_REQUEST, reqs.map(r => `npm@${r.replace('{V}', version)}`)) + }) + } + } }) diff --git a/workspaces/config/lib/config-data.js b/workspaces/config/lib/config-data.js index 6ee4da8f79151..7b0e4f82779dc 100644 --- a/workspaces/config/lib/config-data.js +++ b/workspaces/config/lib/config-data.js @@ -1,70 +1,68 @@ const nopt = require('nopt').lib const log = require('proc-log') -const envReplace = require('./env-replace') -const nerfDart = require('./nerf-dart') -const { typeDefs, Types, getType } = require('./type-defs') const ini = require('ini') +const fs = require('fs/promises') +const { dirname } = require('path') +const nerfDart = require('./nerf-dart') +const envReplace = require('./env-replace') +const { typeDefs } = require('./type-defs') +const { definitions, shorthands, types } = require('./config') const SYMBOLS = { set: Symbol('set'), delete: Symbol('delete'), -} - -const typeDescription = type => { - if (Array.isArray(type)) { - return type.map(t => typeDescription(t)) - } - const def = getType(type) - return def ? def.description ?? def.typeDescription : type + clear: Symbol('clear'), } class ConfigData extends Map { - #type = null - - #data = null - #source = null - #valid = true + static mutateSymbols = SYMBOLS #parent = null - #deprecated = null - #shorthands = null - #types = null - #env = null + #where = null + #description = null + #type = null - static get mutateSymbols () { - return SYMBOLS - } + #envReplace = null - constructor (type, { parent, deprecated, shorthands, types, env }) { + #data = {} + #file = null + #loaded = null + #valid = true + + constructor (type, { parent, data, env }) { super() - this.#type = type this.#parent = parent - this.#deprecated = deprecated - this.#shorthands = shorthands - this.#types = types - this.#env = env + + const { where, description = where, ...opts } = typeof type === 'string' + ? { where: type } : type + + this.#where = where + this.#description = description + this.#type = opts + + this.#envReplace = (v) => envReplace(v, env) for (const key of Object.keys(SYMBOLS)) { this[key] = () => { - throw new Error(`attempted to call \`${key}\` directly on ConfigData:${this.#type.where}`) + throw new Error(`Cannot call \`${key}\` directly on ConfigData:${this.where}`) } } + + if (data) { + this.load(data) + } } get where () { - return this.#type.where + return this.#where } - get source () { - return this.#source + get file () { + return this.#file } get data () { - if (this.#data) { - return this.#data - } - this.#data = Object.fromEntries([...this.entries()]) return this.#data } @@ -72,80 +70,153 @@ class ConfigData extends Map { return ini.stringify(this.data).trim() } + #assertLoaded (val = true) { + if (!!this.#loaded === val) { + throw new Error(`Cannot load a config type more than once: ` + + `\`${this.where}\` previously loaded from \`${this.source}\``) + } + } + [SYMBOLS.set] (key, value) { + this.#valid = false + return this.#set(key, value) + } + + [SYMBOLS.delete] (key) { + this.#valid = false + return this.#delete(key) + } + + #set (key, value) { // XXX(npm9+) make this throw an error - if (!this.#type.allowDeprecated && this.#deprecated[key]) { - log.warn('config', key, this.#deprecated[key]) + const dep = definitions[key]?.deprecated + if (!this.#type.allowDeprecated && dep) { + log.warn('config', key, dep) } - // this is now dirty, the next call to this.valid will have to check it - this.#valid = false - // data will also be regnerated - this.#data = null + Object.defineProperty(this.#data, key, { + configurable: true, + enumerable: true, + get () { + return value + }, + }) return super.set(key, value) } - [SYMBOLS.delete] (key) { - this.#data = null + #delete (key) { + delete this.#data[key] return super.delete(key) } - clear () { - throw new Error(`attempted to call \`clear\` directly on ConfigData:${this.#type.where}`) + ignore (reason) { + this.#assertLoaded(false) + this.#loaded = `${this.description}, ignored: ${reason}` } - load (data, error, source = this.#type.defaultSource) { - this.#data = null + load (data, error, file) { + this.#assertLoaded(false) - if (this.source) { - throw new Error(`Double load ${this.where} ${this.source}`) + if (file) { + this.#file = file + this.#loaded = `${this.description}, file: ${file}` + } else { + this.#loaded = this.description } - if (error) { - if (error.code !== 'ENOENT') { - log.verbose('config', `error loading ${this.where} config`, error) - } - return + if (error?.code !== 'ENOENT') { + log.verbose('config', `error loading ${this.where} config`, error) } - const result = {} - let cleanData = null + if (error || !data) { + return + } + // an array comes from argv so we parse it in the standard nopt way if (Array.isArray(data)) { const { argv, ...parsedData } = nopt.nopt(data, { typeDefs, - types: this.#types, - shorthands: this.#shorthands, + shorthands, + types, invalidHandler: (...args) => this.#invalidHandler(...args), }) - result.argv = argv - cleanData = parsedData - } else if (data) { - if (typeof data === 'string') { - data = ini.parse(data) - } - cleanData = {} - for (const [k, v] of Object.entries(data)) { - cleanData[envReplace(k, this.#env)] = typeof v === 'string' ? envReplace(v, this.#env) : v - } - this.#clean(cleanData) + this.#setAll(parsedData) + return { argv } } - if (cleanData) { - // this.set reset #valid so we check the status before and reset - // it after setting all the properties because we just validated - // everything with nopt - const isValid = this.#valid - for (const [k, v] of Object.entries(cleanData)) { - this[SYMBOLS.set](k, v) + // if its a string then it came from a file and we need to parse + // it with ini first + if (typeof data === 'string') { + data = ini.parse(data) + } + + // then do any env specific replacements + const parsed = Object.entries(data).reduce((acc, [k, v]) => { + acc[this.#envReplace(k)] = typeof v === 'string' ? this.#envReplace(v) : v + return acc + }) + + // and finally only do a nopt clean since it is already parsed + this.#setAll(this.#clean(parsed)) + } + + #setAll (data) { + for (const [key, value] of Object.entries(data)) { + this.#set(key, value) + } + } + + #clean (d) { + nopt.clean(d, { + typeDefs, + types, + invalidHandler: (...args) => this.#invalidHandler(...args), + }) + return d + } + + #invalidHandler (k, val) { + this.#valid = false + const def = definitions[k] + const msg = def + ? `invalid item \`${k}\`, ${definitions[k].mustBe()} and got \`${val}\`` + : `unknown item \`${k}\`` + log.warn('config', msg) + } + + async save () { + this.#assertLoaded() + + if (!this.file) { + throw new Error(`Cannot save config since it was not loaded from a file: ` + + `\`${this.where}\` from \`${this.#description}\``) + } + + const { user } = this.#parent.constructor.Locations + if (this.where === user) { + // if email is nerfed, then we want to de-nerf it + const nerfed = nerfDart(this.get('registry')) + const email = this.get(`${nerfed}:email`, user) + if (email) { + this.delete(`${nerfed}:email`, user) + this.set('email', email, user) } - this.#valid = isValid - return result } - this.#source = `(${source}, ignored)` + const data = this.toString() + if (!data) { + // ignore the unlink error (eg, if file doesn't exist) + await fs.unlink(this.file).catch(() => {}) + return + } + + await fs.mkdir(dirname(this.file), { recursive: true }) + await fs.writeFile(this.file, data + '\n', 'utf8') + await fs.chmod(this.file, this.#type.mode || 0o666) } validate () { + this.#assertLoaded() + if (this.#valid) { return true } @@ -199,47 +270,6 @@ class ConfigData extends Map { return this.#valid } - - #clean (d) { - nopt.clean(d, { - typeDefs, - types: this.#types, - invalidHandler: (...args) => this.#invalidHandler(...args), - }) - } - - #invalidHandler (k, val, type) { - this.#valid = false - - if (Array.isArray(type)) { - if (type.includes(Types.url)) { - type = Types.url - } else /* istanbul ignore next */ if (type.includes(Types.path)) { - /* no actual configs matching this, but - * path types SHOULD be handled this way, like URLs, for the - * same reason */ - type = Types.path - } - } - - const typeDesc = [].concat(typeDescription(type)) - const oneOrMore = typeDesc.includes(Types.Array) - const mustBe = typeDesc.filter(m => m !== Types.Array) - - const oneOf = mustBe.length === 1 && oneOrMore ? 'one or more' - : mustBe.length > 1 && oneOrMore ? 'one or more of:' - : mustBe.length > 1 ? 'one of:' - : '' - const desc = mustBe.length === 1 ? mustBe[0] : mustBe.filter(m => m !== Types.Array) - .map(n => typeof n === 'string' ? n : JSON.stringify(n)) - .join(', ') - - log.warn('config', - `invalid item \`${k}\`.`, - `Must be ${oneOf}`.trim(), - `${desc}. Received value of \`${val}\`` - ) - } } module.exports = ConfigData diff --git a/workspaces/config/lib/config-locations.js b/workspaces/config/lib/config-locations.js new file mode 100644 index 0000000000000..1e1ad5b49cdaf --- /dev/null +++ b/workspaces/config/lib/config-locations.js @@ -0,0 +1,185 @@ +const ConfigData = require('./config-data') +const { derived, definitions, camelCase, keys: configKeys } = require('./config') + +// this is in order from least -> most precedence +const LocationsList = Object.entries({ + default: { description: `npm's default values`, allowDeprecated: true }, + builtin: { description: `npm's builtin npmrc file` }, + global: { description: 'global .npmrc file', validateAuth: true }, + user: { description: 'user .npmrc file', validateAuth: true, mode: 0o600 }, + project: { description: 'project .npmrc file', validateAuth: true }, + env: { description: 'environment variables' }, + cli: { description: 'command line options' }, +}) + +// an enum to export and use to make using `where` not rely on strings +const Locations = LocationsList.reduce((acc, [location]) => { + acc[location] = location + return acc +}, {}) + +class ConfigLocations extends Map { + static Locations = Locations + + #env = null + + #list = [] + #revList = [] + #indexes = {} + + #data = {} + #baseData = {} + + #base = new Map() + #derived = new Map() + + constructor ({ env }) { + super() + + this.#env = env + + for (const key of configKeys) { + this.#createBaseDescriptor(key) + } + + for (const key of Object.keys(derived)) { + this.#createDerivedDescriptor(key) + } + + for (const [where, conf] of LocationsList) { + this.add({ ...conf, where }) + } + + // symbols for mutating config data are shared here so that no method is exposed + // that can mutate a location's config data execpt for these + for (const key of Object.keys(ConfigData.mutateSymbols)) { + this[key] = () => { + throw new Error(`Cannot call ${key} on config locations`) + } + } + } + + get data () { + return this.#data + } + + get (where) { + if (!this.has(where)) { + throw new Error(`Cannot get invalid config location of \`${where}\``) + } + return super.get(where) + } + + add (location, configData) { + const data = new ConfigData(location, { parent: this, data: configData, env: this.#env }) + + this.#indexes[data.where] = this.#list.push(data.where) - 1 + this.#revList.unshift(data.where) + super.set(data.where, data) + + return data + } + + // defaults -> cli + * values (startWhere) { + const index = startWhere ? this.#indexes[startWhere] : 0 + const locations = index ? this.#list.slice(index) : this.#list + for (const where of locations) { + yield this.get(where) + } + } + + // cli -> defaults + * reverseValues (startWhere) { + const index = startWhere ? this.#revList.length - 1 - this.#indexes[startWhere] : 0 + const locations = index ? this.#revList.slice(index) : this.#revList + for (const where of locations) { + yield this.get(where) + } + } + + find (where, key) { + for (const config of this.reverseValues(where)) { + if (config.has(key)) { + return config.where + } + } + return null + } + + getData (where, key) { + if (where === null) { + for (const config of this.reverseValues()) { + if (config.has(key)) { + return config.get(key) + } + } + return + } + return this.get(where).get(key) + } + + hasData (where, key) { + if (where === null) { + for (const config of this.reverseValues()) { + if (config.has(key)) { + return true + } + } + return false + } + return this.get(where).has(key) + } + + setData (where, key, val) { + this.#mutateData(key) + return this.get(where)[ConfigData.mutateSymbols.set](key, val) + } + + deleteData (where, key) { + this.#mutateData(key) + return this.get(where)[ConfigData.mutateSymbols.delete](key) + } + + #mutateData (key) { + this.#base.delete(key) + for (const s of definitions[key].flatten.values()) { + this.#derived.delete(s) + } + } + + #createBaseDescriptor (k) { + const descriptor = { + configurable: true, + enumerable: true, + get () { + if (this.#base.has(k)) { + return this.#base.get(k) + } + const value = this.getData(null, k) + this.#base.set(k, value) + return value + }, + } + Object.defineProperty(this.#baseData, k, descriptor) + Object.defineProperty(this.#baseData, camelCase(k), descriptor) + } + + #createDerivedDescriptor (k) { + const derive = derived[k] + Object.defineProperty(this.#data, camelCase(k), { + configurable: true, + enumerable: true, + get () { + if (this.#derived.has(k)) { + return this.#derived.get(k) + } + const value = derive.set(this.#baseData) + this.#derived.set(k, value) + return value + }, + }) + } +} + +module.exports = ConfigLocations diff --git a/workspaces/config/lib/config-types.js b/workspaces/config/lib/config-types.js deleted file mode 100644 index ad9597a5aa052..0000000000000 --- a/workspaces/config/lib/config-types.js +++ /dev/null @@ -1,147 +0,0 @@ -const ConfigData = require('./config-data') - -// this is in order from least -> most precedence -const ConfTypesList = [ - { where: 'default', source: 'default values', allowDeprecated: true }, - { where: 'builtin' }, - { where: 'global', validateAuth: true }, - { where: 'user', validateAuth: true }, - { where: 'project', validateAuth: true }, - { where: 'env', source: 'environment' }, - { where: 'cli', source: 'command line options' }, -] - -// an enum to export and use to make using `where` not rely on strings -const ConfTypes = ConfTypesList.reduce((acc, type) => { - acc[type.where] = type.where - return acc -}, {}) - -class ConfigDataTypes extends Map { - #frozen = false - - #list = [] - #indexes = {} - #revList = [] - #revIndexes = {} - - #flat = null - #derived = {} - #effects = {} - #dirty = new Set() - #flatten = null - - static get ConfTypes () { - return ConfTypes - } - - constructor ({ flatten, derived, effects, ...opts }) { - super() - - this.#flatten = flatten - this.#derived = derived - this.#effects = effects - - for (const type of ConfTypesList) { - const data = new ConfigData(type, { ...opts, parent: this }) - this.set(type.where, data) - this.#indexes[type.where] = this.#list.push(type.where) - 1 - this.#revIndexes[type.where] = ConfTypesList.length - this.#revList.unshift(type.where) - } - - this.#frozen = true - - // symbols for mutating config data are shared here so that no method is exposed - // that can mutate a types config data execpt for these - for (const [key, symbol] of Object.entries(ConfigData.mutateSymbols)) { - this[`${key}Data`] = (...args) => this.#mutateData(symbol, ...args) - } - } - - get data () { - if (this.#flat) { - return this.#flat - } - process.emit('time', `config:flatten`) - this.#flat = {} - // walk from least priority to highest - for (const { data } of this.values()) { - //this.#flatten(data, this.#flat) - } - process.emit('timeEnd', `config:flatten`) - // return Object.freeze(this.#flat) - return this.#flat - } - - get (where) { - if (!this.has(where)) { - throw new Error(`Cannot get invalid config type of \`${where}\``) - } - return super.get(where) - } - - set (key, value) { - if (this.#frozen) { - throw new Error(`cannot change config types after init`) - } - return super.set(key, value) - } - - delete () { - throw new Error('cannot change config types after init') - } - - clear () { - throw new Error('cannot change config types after init') - } - - // defaults -> cli - * values (startWhere) { - const index = startWhere ? this.#indexes[startWhere] : 0 - const locations = index ? this.#list.slice(index) : this.#list - for (const where of locations) { - yield this.get(where) - } - } - - // cli -> defaults - * reverseValues (startWhere) { - const index = startWhere ? this.#revIndexes[startWhere] : 0 - const locations = index ? this.#revList.slice(index) : this.#revList - for (const where of locations) { - yield this.get(where) - } - } - - find (key) { - for (const config of this.reverseValues()) { - if (config.has(key)) { - return config.where - } - } - return null - } - - getData (where, key) { - if (where === null) { - for (const config of this.reverseValues()) { - if (config.has(key)) { - return config.get(key) - } - } - return - } - - return this.get(where).get(key) - } - - #mutateData(symbol, where, key) { - const effected = this.#effects(key) - // the flat options are invalidated, regenerate next time they're needed - this.#flat = null - return this.get(where)[symbol](...args) - } - } -} - -module.exports = ConfigDataTypes diff --git a/workspaces/config/lib/config/definition.js b/workspaces/config/lib/config/definition.js new file mode 100644 index 0000000000000..4b21fc8f73b7e --- /dev/null +++ b/workspaces/config/lib/config/definition.js @@ -0,0 +1,222 @@ +// class that describes a config key we know about +// this keeps us from defining a config key and not +// providing a default, description, etc. +// +// TODO: some kind of categorization system, so we can +// say "these are for registry access", "these are for +// version resolution" etc. + +const { Types, getType } = require('./type-defs') +const { wrap, unindent } = require('../../../lib/utils/wrap') + +const hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k) + +const required = ['type', 'description', 'default'] +const allowed = [ + ...required, + 'defaultDescription', + 'deprecated', + 'flatten', + 'hint', + 'short', + 'usage', + 'envExport', +] + +class Definition { + #key = null + #def = null + #flatten = new Set() + + constructor (key, def) { + this.#key = key + this.#def = def + + if (def.flatten === true) { + this.#flatten.add(key) + } else if (typeof def.flatten === 'string') { + this.#flatten.add(def.flatten) + } else if (Array.isArray(def.flatten)) { + for (const f of def.flatten) { + this.#flatten.add(f) + } + } + + for (const req of required) { + if (!hasOwn(this.#def, req)) { + throw new Error(`config \`${this.#key}\` lacks key \`${req}\``) + } + } + if (!this.#key) { + throw new Error(`config lacks key: ${this.#key}`) + } + for (const field of Object.keys(this.#def)) { + if (!allowed.includes(field)) { + throw new Error(`config defines unknown field ${field}: ${this.#key}`) + } + } + } + + get default () { + return this.#def.default + } + + get deprecated () { + return this.#def.deprecated?.trim()?.replace(/\n +/, '\n') + } + + get envExport () { + // if it's set falsey, don't export it, otherwise we do by default + return this.#def.envExport ?? true + } + + get flatten () { + return this.#flatten + } + + get short () { + return [].concat(this.#def.short) + } + + get isBoolean () { + return this.#typeDefs.some(t => t?.isBoolean) + } + + get #types () { + return [].concat(this.#def.type) + } + + get #typeMultiple () { + return this.#typeDefs.includes(Types.Array) + } + + get #typeDefs () { + return this.#types.map((t) => getType(t) ?? t) + } + + get #defaultDescription () { + return this.#def.defaultDescription ?? describeValue(this.#def.default) + } + + get #typeDescription () { + return this.#describeTypes() + } + + addFlatten (k) { + this.#flatten.add(k) + } + + // a textual description of this config, suitable for help output + describe () { + const sections = [ + ['Default', this.#defaultDescription], + ['Type', this.#typeDescription], + this.deprecated ? ['DEPRECATED', this.deprecated] : null, + this.#def.description, + !this.envExport && 'This value is not exported to the environment for child processes.', + ].map((s) => { + if (Array.isArray(s)) { + return `* ${s[0]}: ${unindent(s[1])}` + } + return s ? unindent(s) : null + }) + + return wrapAll(`#### \`${this.key}\`\n\n${sections.filter(Boolean).join('\n')}`) + } + + mustBe () { + const allowMultiple = this.#typeMultiple + const types = this.type.includes(Types.Url) ? [Types.Url] + // no actual configs matching this, but path types SHOULD be handled + // this way, like URLs, for the same reason + : /* istanbul ignore next */ this.type.includes(Types.Path) ? [Types.Path] + : this.type + + const mustBe = types.filter(t => t !== Types.Array).map((t) => { + const type = getType(t) + return type?.description ?? type?.typeDescription ?? type + }) + const singleValue = mustBe.length === 1 + + const oneOf = singleValue && allowMultiple ? 'one or more' + : !singleValue && allowMultiple ? 'one or more of:' + : !singleValue ? 'one of:' + : '' + + return `Must be ${oneOf} ${mustBe.map(describeValue).join(', ')}`.replace(/\s+/g, ' ') + } + + describeUsage () { + const usage = this.short.map(s => `-${s}`) + + if (this.isBoolean && this.default !== false) { + usage.push(`--no-${this.#key}`) + } + + usage.push(`--${this.#key}`) + + let description = [] + if (!this.isBoolean) { + // null type means optional and doesn't currently affect usage output since + // all non-optional params have defaults so we render everything as optional + const valueTypes = this.#typeDefs.filter(t => t !== null && t.type !== Types.Array) + + if (valueTypes.some(t => typeof t !== 'string' && typeof t !== 'number')) { + // Generic values, use hint + description = this.#def.hint ? [].concat(this.#def.hint) : this.typeDefs.map(t => t?.hint) + } else { + // Specific values, use specifics given + description = valueTypes + } + } + + const d = description.filter(Boolean).join('|') + const usageDesc = `${usage.join('|')} ${d ? `<${d}>` : ''}`.trim() + + return this.$typeMultiple ? `${usageDesc} [${usageDesc} ...]` : usageDesc + } + + #describeTypes () { + const descriptions = this.#typeDefs + .filter(t => t?.type !== Types.Array) + .flatMap(t => t?.typeDescription ?? t) + .map(describeValue) + + // [a] => "a" + // [a, b] => "a or b" + // [a, b, c] => "a, b, or c" + // [a, Array] => "a (can be set multiple times)" + // [a, Array, b] => "a or b (can be set multiple times)" + const last = descriptions.length > 1 ? [descriptions.pop()] : [] + const oxford = descriptions.length > 1 ? ', or ' : ' or ' + const words = [descriptions.join(', ')].concat(last).join(oxford) + const multiple = this.#typeMultiple ? ' (can be set multiple times)' : '' + return `${words}${multiple}` + } +} + +// if it's a string, quote it. otherwise, just cast to string. +const describeValue = val => (typeof val === 'string' ? JSON.stringify(val) : String(val)) + +const wrapAll = s => { + let inCodeBlock = false + return s.split('\n\n').map(block => { + if (inCodeBlock || block.startsWith('```')) { + inCodeBlock = !block.endsWith('```') + return block + } + + if (block.startsWith('*')) { + return '* ' + block + .slice(1) + .trim() + .split('\n* ') + .map(li => wrap(li).replace(/\n/g, '\n ')) + .join('\n* ') + } + + return wrap(block) + }).join('\n\n') +} + +module.exports = Definition diff --git a/lib/utils/config/definitions.js b/workspaces/config/lib/config/index.js similarity index 76% rename from lib/utils/config/definitions.js rename to workspaces/config/lib/config/index.js index 3a62c1c1eb964..e5704a05ae3e7 100644 --- a/lib/utils/config/definitions.js +++ b/workspaces/config/lib/config/index.js @@ -1,33 +1,34 @@ -const Definition = require('./definition.js') -const { version: npmVersion } = require('../../../package.json') const ciInfo = require('ci-info') -const querystring = require('querystring') -const { isWindows } = require('../is-windows.js') -const { join } = require('path') const { tmpdir } = require('os') +const { join } = require('path') const fs = require('fs') - -const { Types } = require('@npmcli/config') - -const editor = process.env.EDITOR || process.env.VISUAL || - (isWindows ? `${process.env.SYSTEMROOT}\\notepad.exe` : 'vi') - -const shell = isWindows - ? process.env.ComSpec || 'cmd' - : process.env.SHELL || 'sh' - -const unicode = /UTF-?8$/i.test( - process.env.LC_ALL || - process.env.LC_CTYPE || - process.env.LANG -) - +const Arborist = require('@npmcli/arborist') +const { Types } = require('../type-defs') +const Definition = require('./definition') +const { version } = require('../../../../package.json') + +const { + EDITOR, + VISUAL, + SYSTEMROOT, + ComSpec = 'cmd', + SHELL = 'sh', + LC_ALL, + LC_CTYPE, + LANG, + LOCALAPPDATA, + NODE_ENV, + NO_COLOR, +} = process.env +const isWindows = process.platform === 'win32' + +const Editor = EDITOR || VISUAL || (isWindows ? `${SYSTEMROOT}\\notepad.exe` : 'vi') +const Shell = isWindows ? ComSpec : SHELL +const Unicode = /UTF-?8$/i.test(LC_ALL || LC_CTYPE || LANG) // use LOCALAPPDATA on Windows, if set https://github.com/npm/cli/pull/899 -const cacheRoot = (isWindows && process.env.LOCALAPPDATA) || '~' -const cacheExtra = isWindows ? 'npm-cache' : '.npm' -const cache = `${cacheRoot}/${cacheExtra}` +const CacheRoot = (isWindows && LOCALAPPDATA) || '~' +const Cache = `${CacheRoot}/${isWindows ? 'npm-cache' : '.npm'}` -// used by cafile flattening to flatOptions.ca const maybeReadFile = file => { try { return fs.readFileSync(file, 'utf8') @@ -39,87 +40,113 @@ const maybeReadFile = file => { } } -const buildOmitList = ({ include = [], omit = [], only, production, also, dev, optional }) => { - if (/^prod(uction)?$/.test(only) || production) { - omit.push('dev') - } else if (production === false) { - include.push('dev') - } - - if (/^dev/.test(also) || dev) { - include.push('dev') - } +const camelCase = (k) => k.replace(/-(ssl|[a-z])/g, (...a) => a[1].toUpperCase()) +module.exports = { + definitions: {}, + defaults: {}, + keys: [], + shortKeys: [], + derivedKeys: [], + types: {}, + derived: {}, + // aliases where they get expanded into a completely different thing + // these are NOT supported in the environment or npmrc files, only + // expanded on the CLI. + // TODO: when we switch off of nopt, use an arg parser that supports + // more reasonable aliasing and short opts right in the definitions set. + shorthands: { + 'enjoy-by': ['--before'], + d: ['--loglevel', 'info'], + dd: ['--loglevel', 'verbose'], + ddd: ['--loglevel', 'silly'], + quiet: ['--loglevel', 'warn'], + q: ['--loglevel', 'warn'], + s: ['--loglevel', 'silent'], + silent: ['--loglevel', 'silent'], + verbose: ['--loglevel', 'verbose'], + desc: ['--description'], + help: ['--usage'], + local: ['--no-global'], + n: ['--no-yes'], + no: ['--no-yes'], + porcelain: ['--parseable'], + readonly: ['--read-only'], + reg: ['--registry'], + iwr: ['--include-workspace-root'], + }, + camelCase, +} - if (optional === false) { - omit.push('optional') - } else if (optional === true) { - include.push('optional') +// // TODO: move nerfdart auth stuff into a nested object that +// // is only passed along to paths that end up calling npm-registry-fetch. +// const flatten = (obj, flat = {}) => { +// for (const [key, val] of Object.entries(obj)) { +// const def = definitions[key] +// if (def?.flatten) { +// def.flatten(key, obj, flat) +// } else if (/@.*:registry$/i.test(key) || /^\/\//.test(key)) { +// flat[key] = val +// } +// } + +// return flat +// } + +class Derived { + #set = null + + get set () { + return this.#set } - omit = [...new Set(omit)].filter(type => !include.includes(type)) - include = [...new Set(include)] + constructor (key, { set, value, nested, sources = [] } = {}) { + const camelKey = camelCase(key) - if (omit.includes('dev')) { - process.env.NODE_ENV = 'production' - } - - return omit -} + if (value) { + this.#set = () => value + } else if (set) { + this.#set = nested + ? (d) => set(d)[camelKey] + : set + } else { + this.#set = (d) => d[camelKey] + } -const definitions = {} -const derived = {} -module.exports = { definitions, derived } + for (const s of new Set([key, ...sources]).values()) { + module.exports.definitions[s].addFlatten(key) + } -const define = (key, def) => { - /* istanbul ignore if - this should never happen, prevents mistakes below */ - if (definitions[key]) { - throw new Error(`defining key more than once: ${key}`) + module.exports.derivedKeys.push(key) } - definitions[key] = new Definition(key, def) } -// basic flattening function, just copy it over camelCase -const flatten = (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] +const define = (key, data) => { + const def = new Definition(key, data) + for (const s of def.short) { + module.exports.shorthands[s] = [`--${key}`] + module.exports.shortKeys.push(s) + } + module.exports.defaults[key] = def.default + module.exports.types[key] = def.type + module.exports.keys.push(key) + module.exports.definitions[key] = def + for (const f of def.flatten.values()) { + module.exports.derived[f] = new Derived(f) + } } -const derive = (key, def) => { - /* istanbul ignore if - this should never happen, prevents mistakes below */ - if (derived[key]) { - throw new Error(`defining key more than once: ${key}`) +const derive = (keys, set, sources) => { + const nested = Array.isArray(keys) + for (const key of [].concat(keys)) { + const d = new Derived(key, { nested, sources: sources.concat(keys), set }) + module.exports.derived[key] = d } - derived[key] = def } -derive('omit', (...args) => { - console.log(args) - throw new Error('x') -}) - -// TODO: -// Instead of having each definition provide a flatten method, -// provide the (?list of?) flat option field(s?) that it impacts. -// When that config is set, we mark the relevant flatOption fields -// dirty. Then, a getter for that field defines how we actually -// set it. -// -// So, `save-dev`, `save-optional`, `save-prod`, et al would indicate -// that they affect the `saveType` flat option. Then the config.flat -// object has a `get saveType () { ... }` that looks at the "real" -// config settings from files etc and returns the appropriate value. -// -// Getters will also (maybe?) give us a hook to audit flat option -// usage, so we can document and group these more appropriately. -// -// This will be a problem with cases where we currently do: -// const opts = { ...npm.flatOptions, foo: 'bar' }, but we can maybe -// instead do `npm.config.set('foo', 'bar')` prior to passing the -// config object down where it needs to go. -// -// This way, when we go hunting for "where does saveType come from anyway!?" -// while fixing some Arborist bug, we won't have to hunt through too -// many places. +const flat = (key, value) => { + const d = new Derived(key, { value }) + module.exports.derived[key] = d +} // Define all config keys we know about @@ -135,7 +162,7 @@ define('_auth', { is safer to use a registry-provided authentication bearer token stored in the ~/.npmrc file by running \`npm login\`. `, - flatten, + flatten: true, }) define('access', { @@ -155,7 +182,7 @@ define('access', { publish will change the access for an existing package the same way that \`npm access set status\` would. `, - flatten, + flatten: true, }) define('all', { @@ -167,7 +194,7 @@ define('all', { all outdated or installed packages, rather than only those directly depended upon by the current project. `, - flatten, + flatten: true, }) define('allow-same-version', { @@ -177,7 +204,7 @@ define('allow-same-version', { Prevents throwing an error when \`npm version\` is used to set the new version to the same value as the current version. `, - flatten, + flatten: true, }) define('also', { @@ -188,10 +215,6 @@ define('also', { \`--include=dev\`. `, deprecated: 'Please use --include=dev instead.', - derived: ['omit'], - flatten (key, obj, flatOptions) { - definitions.omit.flatten('omit', obj, flatOptions) - }, }) define('audit', { @@ -203,7 +226,7 @@ define('audit', { documentation for [\`npm audit\`](/commands/npm-audit) for details on what is submitted. `, - flatten, + flatten: true, }) define('audit-level', { @@ -213,7 +236,7 @@ define('audit-level', { The minimum level of vulnerability for \`npm audit\` to exit with a non-zero exit code. `, - flatten, + flatten: true, }) define('auth-type', { @@ -222,7 +245,7 @@ define('auth-type', { description: ` What authentication strategy to use with \`login\`. `, - flatten, + flatten: true, }) define('before', { @@ -239,7 +262,7 @@ define('before', { to that tag will be used. For example, \`foo@latest\` might install \`foo@1.2\` even though \`latest\` is \`2.0\`. `, - flatten, + flatten: true, }) define('bin-links', { @@ -253,7 +276,7 @@ define('bin-links', { fact that some file systems don't support symlinks, even on ostensibly Unix systems. `, - flatten, + flatten: true, }) define('browser', { @@ -261,16 +284,16 @@ define('browser', { defaultDescription: ` OS X: \`"open"\`, Windows: \`"start"\`, Others: \`"xdg-open"\` `, - type: [Types.BooleanOrString, Types.Boolean], + type: Types.BooleanOrString, description: ` The browser that is called by npm commands to open websites. - Set to \`false\` to suppress browser behavior and instead print Types.urls to + Set to \`false\` to suppress browser behavior and instead print urls to terminal. Set to \`true\` to use default system URL opener. `, - flatten, + flatten: true, }) define('ca', { @@ -298,22 +321,18 @@ define('ca', { See also the \`strict-ssl\` config. `, - flatten, + flatten: true, }) define('cache', { - default: cache, + default: Cache, defaultDescription: ` Windows: \`%LocalAppData%\\npm-cache\`, Posix: \`~/.npm\` `, - type: Types.path, + type: Types.Path, description: ` The location of npm's cache directory. `, - flatten (key, obj, flatOptions) { - flatOptions.cache = join(obj.cache, '_cacache') - flatOptions.npxCache = join(obj.cache, '_npx') - }, }) define('cache-max', { @@ -325,11 +344,6 @@ define('cache-max', { deprecated: ` This option has been deprecated in favor of \`--prefer-online\` `, - flatten (key, obj, flatOptions) { - if (obj[key] <= 0) { - flatOptions.preferOnline = true - } - }, }) define('cache-min', { @@ -341,37 +355,16 @@ define('cache-min', { deprecated: ` This option has been deprecated in favor of \`--prefer-offline\`. `, - flatten (key, obj, flatOptions) { - if (obj[key] >= 9999) { - flatOptions.preferOffline = true - } - }, }) define('cafile', { default: null, - type: [null, Types.path], + type: [null, Types.Path], description: ` - A Types.path to a file containing one or multiple Certificate Authority signing + A Types.Path to a file containing one or multiple Certificate Authority signing certificates. Similar to the \`ca\` setting, but allows for multiple CA's, as well as for the CA information to be stored in a file on disk. `, - flatten (key, obj, flatOptions) { - // always set to null in defaults - if (!obj.cafile) { - return - } - - const raw = maybeReadFile(obj.cafile) - if (!raw) { - return - } - - const delim = '-----END CERTIFICATE-----' - flatOptions.ca = raw.replace(/\r\n/g, '\n').split(delim) - .filter(section => section.trim()) - .map(section => section.trimLeft() + delim) - }, }) define('call', { @@ -386,7 +379,7 @@ define('call', { npm exec --package yo --package generator-node --call "yo node" \`\`\` `, - flatten, + flatten: true, }) define('cert', { @@ -401,8 +394,8 @@ define('cert', { cert="-----BEGIN CERTIFICATE-----\\nXXXX\\nXXXX\\n-----END CERTIFICATE-----" \`\`\` - It is _not_ the Types.path to a certificate file, though you can set a registry-scoped - "certfile" Types.path like "//other-registry.tld/:certfile=/path/to/cert.pem". + It is _not_ the Types.Path to a certificate file, though you can set a registry-scoped + "certfile" Types.Path like "//other-registry.tld/:certfile=/path/to/cert.pem". `, deprecated: ` \`key\` and \`cert\` are no longer used for most registry operations. @@ -411,7 +404,7 @@ define('cert', { //other-registry.tld/:keyfile=/path/to/key.pem //other-registry.tld/:certfile=/path/to/cert.crt `, - flatten, + flatten: true, }) define('ci-name', { @@ -426,7 +419,7 @@ define('ci-name', { will detect the current CI environment using the [\`ci-info\`](http://npm.im/ci-info) module. `, - flatten, + flatten: true, }) define('cidr', { @@ -436,15 +429,11 @@ define('cidr', { This is a list of CIDR address to be used when configuring limited access tokens with the \`npm token create\` command. `, - flatten, + flatten: true, }) -// This should never be directly used, the flattened value is the derived value -// and is sent to other modules, and is also exposed as `npm.color` for use -// inside npm itself. define('color', { - default: !process.env.NO_COLOR || process.env.NO_COLOR === '0', - usage: '--color|--no-color|--color always', + default: !NO_COLOR || NO_COLOR === '0', defaultDescription: ` true unless the NO_COLOR environ is set to something other than '0' `, @@ -453,14 +442,6 @@ define('color', { If false, never shows colors. If \`"always"\` then always shows colors. If true, then only prints color codes for tty file descriptors. `, - flatten (key, obj, flatOptions) { - flatOptions.color = !obj.color ? false - : obj.color === 'always' ? true - : !!process.stdout.isTTY - flatOptions.logColor = !obj.color ? false - : obj.color === 'always' ? true - : !!process.stderr.isTTY - }, }) define('commit-hooks', { @@ -469,7 +450,7 @@ define('commit-hooks', { description: ` Run git commit hooks when using the \`npm version\` command. `, - flatten, + flatten: true, }) define('depth', { @@ -485,20 +466,15 @@ define('depth', { root project. If \`--all\` is set, then npm will show all dependencies by default. `, - flatten, + flatten: true, }) define('description', { default: true, type: Types.Boolean, - usage: '--no-description', description: ` Show the description in \`npm search\` `, - flatten (key, obj, flatOptions) { - flatOptions.search = flatOptions.search || { limit: 20 } - flatOptions.search[key] = obj[key] - }, }) define('dev', { @@ -508,19 +484,15 @@ define('dev', { Alias for \`--include=dev\`. `, deprecated: 'Please use --include=dev instead.', - flatten (key, obj, flatOptions) { - definitions.omit.flatten('omit', obj, flatOptions) - }, }) define('diff', { default: [], - hint: '', - type: [Types.String, Types.Array], + type: [Types.Spec, Types.Array], description: ` Define arguments to compare in \`npm diff\`. `, - flatten, + flatten: true, }) define('diff-ignore-all-space', { @@ -529,7 +501,7 @@ define('diff-ignore-all-space', { description: ` Ignore whitespace when comparing lines in \`npm diff\`. `, - flatten, + flatten: true, }) define('diff-name-only', { @@ -538,7 +510,7 @@ define('diff-name-only', { description: ` Prints only filenames when using \`npm diff\`. `, - flatten, + flatten: true, }) define('diff-no-prefix', { @@ -550,27 +522,25 @@ define('diff-no-prefix', { Note: this causes \`npm diff\` to ignore the \`--diff-src-prefix\` and \`--diff-dst-prefix\` configs. `, - flatten, + flatten: true, }) define('diff-dst-prefix', { default: 'b/', - hint: '', - type: Types.String, + type: Types.Path, description: ` Destination prefix to be used in \`npm diff\` output. `, - flatten, + flatten: true, }) define('diff-src-prefix', { default: 'a/', - hint: '', - type: Types.String, + type: Types.Path, description: ` Source prefix to be used in \`npm diff\` output. `, - flatten, + flatten: true, }) define('diff-text', { @@ -579,7 +549,7 @@ define('diff-text', { description: ` Treat all files as text in \`npm diff\`. `, - flatten, + flatten: true, }) define('diff-unified', { @@ -588,7 +558,7 @@ define('diff-unified', { description: ` The number of lines of context to print in \`npm diff\`. `, - flatten, + flatten: true, }) define('dry-run', { @@ -604,11 +574,11 @@ define('dry-run', { Note: This is NOT honored by other network related commands, eg \`dist-tags\`, \`owner\`, etc. `, - flatten, + flatten: true, }) define('editor', { - default: editor, + default: Editor, defaultDescription: ` The EDITOR or VISUAL environment variables, or '%SYSTEMROOT%\\notepad.exe' on Windows, or 'vi' on Unix systems @@ -617,7 +587,7 @@ define('editor', { description: ` The command to run for \`npm edit\` and \`npm config edit\`. `, - flatten, + flatten: true, }) define('engine-strict', { @@ -630,7 +600,7 @@ define('engine-strict', { This can be overridden by setting the \`--force\` flag. `, - flatten, + flatten: true, }) define('fetch-retries', { @@ -643,10 +613,7 @@ define('fetch-retries', { npm will retry idempotent read requests to the registry in the case of network failures or 5xx HTTP errors. `, - flatten (key, obj, flatOptions) { - flatOptions.retry = flatOptions.retry || {} - flatOptions.retry.retries = obj[key] - }, + flatten: 'retry.retries', }) define('fetch-retry-factor', { @@ -656,10 +623,7 @@ define('fetch-retry-factor', { The "factor" config for the \`retry\` module to use when fetching packages. `, - flatten (key, obj, flatOptions) { - flatOptions.retry = flatOptions.retry || {} - flatOptions.retry.factor = obj[key] - }, + flatten: 'retry.factor', }) define('fetch-retry-maxtimeout', { @@ -670,10 +634,7 @@ define('fetch-retry-maxtimeout', { The "maxTimeout" config for the \`retry\` module to use when fetching packages. `, - flatten (key, obj, flatOptions) { - flatOptions.retry = flatOptions.retry || {} - flatOptions.retry.maxTimeout = obj[key] - }, + flatten: 'retry.max-timeout', }) define('fetch-retry-mintimeout', { @@ -684,10 +645,7 @@ define('fetch-retry-mintimeout', { The "minTimeout" config for the \`retry\` module to use when fetching packages. `, - flatten (key, obj, flatOptions) { - flatOptions.retry = flatOptions.retry || {} - flatOptions.retry.minTimeout = obj[key] - }, + flatten: 'retry.min-timeout', }) define('fetch-timeout', { @@ -697,9 +655,7 @@ define('fetch-timeout', { description: ` The maximum amount of time to wait for HTTP requests to complete. `, - flatten (key, obj, flatOptions) { - flatOptions.timeout = obj[key] - }, + flatten: 'timeout', }) define('force', { @@ -729,7 +685,7 @@ define('force', { If you don't have a clear idea of what you want to do, it is strongly recommended that you do not use this option! `, - flatten, + flatten: true, }) define('foreground-scripts', { @@ -744,7 +700,7 @@ define('foreground-scripts', { Note that this will generally make installs run slower, and be much noisier, but can be useful for debugging. `, - flatten, + flatten: true, }) define('format-package-lock', { @@ -754,7 +710,7 @@ define('format-package-lock', { Format \`package-lock.json\` or \`npm-shrinkwrap.json\` as a human readable file. `, - flatten, + flatten: true, }) define('fund', { @@ -765,7 +721,7 @@ define('fund', { acknowledging the number of dependencies looking for funding. See [\`npm fund\`](/commands/npm-fund) for details. `, - flatten, + flatten: true, }) define('git', { @@ -773,10 +729,10 @@ define('git', { type: Types.String, description: ` The command to use for git commands. If git is installed on the - computer, but is not in the \`PATH\`, then set this to the full Types.path to + computer, but is not in the \`PATH\`, then set this to the full path to the git binary. `, - flatten, + flatten: true, }) define('git-tag-version', { @@ -786,7 +742,7 @@ define('git-tag-version', { Tag the commit when using the \`npm version\` command. Setting this to false results in no commit being made at all. `, - flatten, + flatten: true, }) define('global', { @@ -804,17 +760,10 @@ define('global', { * bin files are linked to \`{prefix}/bin\` * man pages are linked to \`{prefix}/share/man\` `, - flatten: (key, obj, flatOptions) => { - flatten(key, obj, flatOptions) - if (flatOptions.global) { - flatOptions.location = 'global' - } - }, }) -// the globalconfig has its default defined outside of this module define('globalconfig', { - type: Types.path, + type: Types.Path, default: '', defaultDescription: ` The global --prefix setting plus 'etc/npmrc'. For example, @@ -823,8 +772,7 @@ define('globalconfig', { description: ` The config file to read for global config options. `, - - flatten, + flatten: true, }) define('global-style', { @@ -838,12 +786,6 @@ define('global-style', { deprecated: ` This option has been deprecated in favor of \`--install-strategy=shallow\` `, - flatten (key, obj, flatOptions) { - if (obj[key]) { - obj['install-strategy'] = 'shallow' - flatOptions.installStrategy = 'shallow' - } - }, }) define('heading', { @@ -852,19 +794,19 @@ define('heading', { description: ` The string that starts all the debugging log output. `, - flatten, + flatten: true, }) define('https-proxy', { default: null, - type: [null, Types.url], + type: [null, Types.URL], description: ` A proxy to use for outgoing https requests. If the \`HTTPS_PROXY\` or \`https_proxy\` or \`HTTP_PROXY\` or \`http_proxy\` environment variables are set, proxy settings will be honored by the underlying \`make-fetch-happen\` library. `, - flatten, + flatten: true, }) define('if-present', { @@ -879,7 +821,7 @@ define('if-present', { This is useful, for example, when running scripts that may only apply for some builds in an otherwise generic CI setup. `, - flatten, + flatten: true, }) define('ignore-scripts', { @@ -893,7 +835,7 @@ define('ignore-scripts', { run-script\` will still run their intended script if \`ignore-scripts\` is set, but they will *not* run any pre- or post-scripts. `, - flatten, + flatten: true, }) define('include', { @@ -908,10 +850,6 @@ define('include', { regardless of the order in which omit/include are specified on the command-line. `, - flatten (key, obj, flatOptions) { - // just call the omit flattener, it reads from obj.include - definitions.omit.flatten('omit', obj, flatOptions) - }, }) define('include-staged', { @@ -923,7 +861,7 @@ define('include-staged', { This is experimental, and not implemented by the npm public registry. `, - flatten, + flatten: true, }) define('include-workspace-root', { @@ -937,7 +875,7 @@ define('include-workspace-root', { or all workspaces via the \`workspaces\` flag, will cause npm to operate only on the specified workspaces, and not on the root project. `, - flatten, + flatten: true, }) define('init-author-email', { @@ -959,7 +897,7 @@ define('init-author-name', { define('init-author-url', { default: '', - type: ['', Types.url], + type: ['', Types.URL], description: ` The value \`npm init\` should use by default for the package author's homepage. `, @@ -975,7 +913,7 @@ define('init-license', { define('init-module', { default: '~/.npm-init.js', - type: Types.path, + type: Types.Path, description: ` A module that will be loaded by the \`npm init\` command. See the documentation for the @@ -986,7 +924,7 @@ define('init-module', { define('init-version', { default: '1.0.0', - type: Types.semver, + type: Types.Semver, description: ` The value that \`npm init\` should use by default for the package version number, if not already set in package.json. @@ -1018,7 +956,7 @@ define('init.author.name', { define('init.author.url', { default: '', - type: ['', Types.url], + type: ['', Types.URL], deprecated: ` Use \`--init-author-url\` instead. `, @@ -1040,7 +978,7 @@ define('init.license', { define('init.module', { default: '~/.npm-init.js', - type: Types.path, + type: Types.Path, deprecated: ` Use \`--init-module\` instead. `, @@ -1051,7 +989,7 @@ define('init.module', { define('init.version', { default: '1.0.0', - type: Types.semver, + type: Types.Semver, deprecated: ` Use \`--init-version\` instead. `, @@ -1068,7 +1006,7 @@ define('install-links', { regular dependencies instead of creating a symlink. This option has no effect on workspaces. `, - flatten, + flatten: true, }) define('install-strategy', { @@ -1083,7 +1021,7 @@ define('install-strategy', { linked: (coming soon) install in node_modules/.store, link in place, unhoisted. `, - flatten, + flatten: true, }) define('json', { @@ -1097,7 +1035,7 @@ define('json', { Not supported by all npm commands. `, - flatten, + flatten: true, }) define('key', { @@ -1111,8 +1049,8 @@ define('key', { key="-----BEGIN PRIVATE KEY-----\\nXXXX\\nXXXX\\n-----END PRIVATE KEY-----" \`\`\` - It is _not_ the Types.path to a key file, though you can set a registry-scoped - "keyfile" Types.path like "//other-registry.tld/:keyfile=/path/to/key.pem". + It is _not_ the path to a key file, though you can set a registry-scoped + "keyfile" path like "//other-registry.tld/:keyfile=/path/to/key.pem". `, deprecated: ` \`key\` and \`cert\` are no longer used for most registry operations. @@ -1121,7 +1059,7 @@ define('key', { //other-registry.tld/:keyfile=/path/to/key.pem //other-registry.tld/:certfile=/path/to/cert.crt `, - flatten, + flatten: true, }) define('legacy-bundling', { @@ -1137,12 +1075,6 @@ define('legacy-bundling', { deprecated: ` This option has been deprecated in favor of \`--install-strategy=nested\` `, - flatten (key, obj, flatOptions) { - if (obj[key]) { - obj['install-strategy'] = 'nested' - flatOptions.installStrategy = 'nested' - } - }, }) define('legacy-peer-deps', { @@ -1163,7 +1095,7 @@ define('legacy-peer-deps', { Use of \`legacy-peer-deps\` is not recommended, as it will not enforce the \`peerDependencies\` contract that meta-dependencies may rely on. `, - flatten, + flatten: true, }) define('link', { @@ -1182,7 +1114,7 @@ define('local-address', { The IP address of the local interface to use when making connections to the npm registry. Must be IPv4 in versions of Node prior to 0.12. `, - flatten, + flatten: true, }) define('location', { @@ -1208,23 +1140,15 @@ define('location', { * bin files are linked to \`{prefix}/bin\` * man pages are linked to \`{prefix}/share/man\` `, - flatten: (key, obj, flatOptions) => { - flatten(key, obj, flatOptions) - if (flatOptions.global) { - flatOptions.location = 'global' - } - if (obj.location === 'global') { - flatOptions.global = true - } - }, }) define('lockfile-version', { default: null, - type: [null, 1, 2, 3, '1', '2', '3'], + type: [null, 1, 2, 3], defaultDescription: ` Version 3 if no lockfile, auto-converting v1 lockfiles to v3, otherwise - maintain current lockfile version.`, + maintain current lockfile version. + `, description: ` Set the lockfile format version to be used in package-lock.json and npm-shrinkwrap-json files. Possible options are: @@ -1242,9 +1166,7 @@ define('lockfile-version', { on disk than lockfile version 2, but not interoperable with older npm versions. Ideal if all users are on npm version 7 and higher. `, - flatten: (key, obj, flatOptions) => { - flatOptions.lockfileVersion = obj[key] && parseInt(obj[key], 10) - }, + flatten: true, }) define('loglevel', { @@ -1261,24 +1183,21 @@ define('loglevel', { ], description: ` What level of logs to report. All logs are written to a debug log, - with the Types.path to that file printed if the execution of a command fails. + with the path to that file printed if the execution of a command fails. Any logs of a higher level than the setting are shown. The default is "notice". See also the \`foreground-scripts\` config. `, - flatten (key, obj, flatOptions) { - flatOptions.silent = obj[key] === 'silent' - }, }) define('logs-dir', { default: null, - type: [null, Types.path], + type: [null, Types.Path], defaultDescription: ` A directory named \`_logs\` inside the cache -`, + `, description: ` The location of npm's log directory. See [\`npm logging\`](/using-npm/logging) for more information. @@ -1311,9 +1230,7 @@ define('maxsockets', { The maximum number of connections to use per origin (protocol/host/port combination). `, - flatten (key, obj, flatOptions) { - flatOptions.maxSockets = obj[key] - }, + flatten: 'max-sockets', }) define('message', { @@ -1325,7 +1242,7 @@ define('message', { Any "%s" in the message will be replaced with the version number. `, - flatten, + flatten: true, }) define('node-options', { @@ -1343,19 +1260,13 @@ define('noproxy', { defaultDescription: ` The value of the NO_PROXY environment variable `, - type: [Types.String, Types.Array], + type: [Types.String, Types.CSV, Types.Array], description: ` Domain extensions that should bypass any proxies. Also accepts a comma-delimited string. `, - flatten (key, obj, flatOptions) { - if (Array.isArray(obj[key])) { - flatOptions.noProxy = obj[key].join(',') - } else { - flatOptions.noProxy = obj[key] - } - }, + flatten: 'no-proxy', }) define('offline', { @@ -1365,11 +1276,11 @@ define('offline', { Force offline mode: no network requests will be done during install. To allow the CLI to fill in missing cache data, see \`--prefer-offline\`. `, - flatten, + flatten: true, }) define('omit', { - default: process.env.NODE_ENV === 'production' ? ['dev'] : [], + default: NODE_ENV === 'production' ? ['dev'] : [], defaultDescription: ` 'dev' if the \`NODE_ENV\` environment variable is set to 'production', otherwise empty. @@ -1389,9 +1300,6 @@ define('omit', { environment variable will be set to \`'production'\` for all lifecycle scripts. `, - flatten (key, obj, flatOptions) { - flatOptions.omit = buildOmitList(obj) - }, }) define('omit-lockfile-registry-resolved', { @@ -1403,7 +1311,7 @@ define('omit-lockfile-registry-resolved', { endpoints with the configured registry, likely resulting in a longer install time. `, - flatten, + flatten: true, }) define('only', { @@ -1416,9 +1324,6 @@ define('only', { When set to \`prod\` or \`production\`, this is an alias for \`--omit=dev\`. `, - flatten (key, obj, flatOptions) { - definitions.omit.flatten('omit', obj, flatOptions) - }, }) define('optional', { @@ -1433,9 +1338,6 @@ define('optional', { description: ` Alias for --include=optional or --omit=optional `, - flatten (key, obj, flatOptions) { - definitions.omit.flatten('omit', obj, flatOptions) - }, }) define('otp', { @@ -1448,17 +1350,16 @@ define('otp', { If not set, and a registry response fails with a challenge for a one-time password, npm will prompt on the command line for one. `, - flatten, + flatten: true, }) define('package', { default: [], - hint: '', - type: [Types.String, Types.Array], + type: [Types.Spec, Types.Array], description: ` The package or packages to install for [\`npm exec\`](/commands/npm-exec) `, - flatten, + flatten: true, }) define('package-lock', { @@ -1471,12 +1372,6 @@ define('package-lock', { This configuration does not affect \`npm ci\`. `, - flatten: (key, obj, flatOptions) => { - flatten(key, obj, flatOptions) - if (flatOptions.packageLockOnly) { - flatOptions.packageLock = true - } - }, }) define('package-lock-only', { @@ -1492,12 +1387,6 @@ define('package-lock-only', { For \`list\` this means the output will be based on the tree described by the \`package-lock.json\`, rather than the contents of \`node_modules\`. `, - flatten: (key, obj, flatOptions) => { - flatten(key, obj, flatOptions) - if (flatOptions.packageLockOnly) { - flatOptions.packageLock = true - } - }, }) define('pack-destination', { @@ -1506,7 +1395,7 @@ define('pack-destination', { description: ` Directory in which \`npm pack\` will save tarballs. `, - flatten, + flatten: true, }) define('parseable', { @@ -1517,7 +1406,7 @@ define('parseable', { Output parseable results from commands that write to standard output. For \`npm search\`, this will be tab-separated table format. `, - flatten, + flatten: true, }) define('prefer-offline', { @@ -1528,7 +1417,7 @@ define('prefer-offline', { data will be requested from the server. To force full offline mode, use \`--offline\`. `, - flatten, + flatten: true, }) define('prefer-online', { @@ -1538,12 +1427,12 @@ define('prefer-online', { If true, staleness checks for cached data will be forced, making the CLI look for updates immediately even for fresh package data. `, - flatten, + flatten: true, }) // `prefix` has its default defined outside of this module define('prefix', { - type: Types.path, + type: Types.Path, short: 'C', default: '', defaultDescription: ` @@ -1563,9 +1452,9 @@ define('preid', { type: Types.String, description: ` The "prerelease identifier" to use as a prefix for the "prerelease" part - of a Types.semver. Like the \`rc\` in \`1.2.0-rc.8\`. + of a semver. Like the \`rc\` in \`1.2.0-rc.8\`. `, - flatten, + flatten: true, }) define('production', { @@ -1573,9 +1462,6 @@ define('production', { type: [null, Types.Boolean], deprecated: 'Use `--omit=dev` instead.', description: 'Alias for `--omit=dev`', - flatten (key, obj, flatOptions) { - definitions.omit.flatten('omit', obj, flatOptions) - }, }) define('progress', { @@ -1590,21 +1476,18 @@ define('progress', { Set to \`false\` to suppress the progress bar. `, - flatten (key, obj, flatOptions) { - flatOptions.progress = !obj.progress ? false - : !!process.stderr.isTTY && process.env.TERM !== 'dumb' - }, + flatten: true, }) define('proxy', { default: null, - type: [null, false, Types.url], // allow proxy to be disabled explicitly + type: [null, false, Types.URL], // allow proxy to be disabled explicitly description: ` A proxy to use for outgoing http requests. If the \`HTTP_PROXY\` or \`http_proxy\` environment variables are set, proxy settings will be honored by the underlying \`request\` library. `, - flatten, + flatten: true, }) define('read-only', { @@ -1614,7 +1497,7 @@ define('read-only', { This is used to mark a token as unable to publish when configuring limited access tokens with the \`npm token create\` command. `, - flatten, + flatten: true, }) define('rebuild-bundle', { @@ -1623,21 +1506,20 @@ define('rebuild-bundle', { description: ` Rebuild bundled dependencies after installation. `, - flatten, + flatten: true, }) define('registry', { default: 'https://registry.npmjs.org/', - type: Types.url, + type: Types.URL, description: ` The base URL of the npm registry. `, - flatten, + flatten: true, }) define('replace-registry-host', { default: 'npmjs', - hint: ' | hostname', type: ['npmjs', 'never', 'always', Types.String], description: ` Defines behavior for replacing the registry host in a lockfile with the @@ -1650,14 +1532,15 @@ define('replace-registry-host', { You may also specify a bare hostname (e.g., "registry.npmjs.org"). `, - flatten, + flatten: true, }) define('save', { default: true, - defaultDescription: `\`true\` unless when using \`npm update\` where it - defaults to \`false\``, - usage: '-S|--save|--no-save|--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle', + defaultDescription: ` + \`true\` unless when using \`npm update\` where it defaults to \`false\` + `, + usage: '--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle', type: Types.Boolean, short: 'S', description: ` @@ -1668,7 +1551,7 @@ define('save', { Will also prevent writing to \`package-lock.json\` if set to \`false\`. `, - flatten, + flatten: true, }) define('save-bundle', { @@ -1682,14 +1565,7 @@ define('save-bundle', { Ignored if \`--save-peer\` is set, since peerDependencies cannot be bundled. `, - flatten (key, obj, flatOptions) { - // XXX update arborist to just ignore it if resulting saveType is peer - // otherwise this won't have the expected effect: - // - // npm config set save-peer true - // npm i foo --save-bundle --save-prod <-- should bundle - flatOptions.saveBundle = obj['save-bundle'] && !obj['save-peer'] - }, + flatten: true, }) // XXX: We should really deprecate all these `--save-blah` switches @@ -1704,16 +1580,6 @@ define('save-dev', { description: ` Save installed packages to a package.json file as \`devDependencies\`. `, - flatten (key, obj, flatOptions) { - if (!obj[key]) { - if (flatOptions.saveType === 'dev') { - delete flatOptions.saveType - } - return - } - - flatOptions.saveType = 'dev' - }, }) define('save-exact', { @@ -1722,12 +1588,8 @@ define('save-exact', { short: 'E', description: ` Dependencies saved to package.json will be configured with an exact - version rather than using npm's default Types.semver range operator. + version rather than using npm's default semver range operator. `, - flatten (key, obj, flatOptions) { - // just call the save-prefix flattener, it reads from obj['save-exact'] - definitions['save-prefix'].flatten('save-prefix', obj, flatOptions) - }, }) define('save-optional', { @@ -1738,26 +1600,6 @@ define('save-optional', { Save installed packages to a package.json file as \`optionalDependencies\`. `, - flatten (key, obj, flatOptions) { - if (!obj[key]) { - if (flatOptions.saveType === 'optional') { - delete flatOptions.saveType - } else if (flatOptions.saveType === 'peerOptional') { - flatOptions.saveType = 'peer' - } - return - } - - if (flatOptions.saveType === 'peerOptional') { - return - } - - if (flatOptions.saveType === 'peer') { - flatOptions.saveType = 'peerOptional' - } else { - flatOptions.saveType = 'optional' - } - }, }) define('save-peer', { @@ -1766,26 +1608,6 @@ define('save-peer', { description: ` Save installed packages to a package.json file as \`peerDependencies\` `, - flatten (key, obj, flatOptions) { - if (!obj[key]) { - if (flatOptions.saveType === 'peer') { - delete flatOptions.saveType - } else if (flatOptions.saveType === 'peerOptional') { - flatOptions.saveType = 'optional' - } - return - } - - if (flatOptions.saveType === 'peerOptional') { - return - } - - if (flatOptions.saveType === 'optional') { - flatOptions.saveType = 'peerOptional' - } else { - flatOptions.saveType = 'peer' - } - }, }) define('save-prefix', { @@ -1800,10 +1622,6 @@ define('save-prefix', { \`npm config set save-prefix='~'\` it would be set to \`~1.2.3\` which only allows patch upgrades. `, - flatten (key, obj, flatOptions) { - flatOptions.savePrefix = obj['save-exact'] ? '' : obj['save-prefix'] - obj['save-prefix'] = flatOptions.savePrefix - }, }) define('save-prod', { @@ -1819,16 +1637,6 @@ define('save-prod', { This is the default behavior if \`--save\` is true, and neither \`--save-dev\` or \`--save-optional\` are true. `, - flatten (key, obj, flatOptions) { - if (!obj[key]) { - if (flatOptions.saveType === 'prod') { - delete flatOptions.saveType - } - return - } - - flatOptions.saveType = 'prod' - }, }) define('scope', { @@ -1836,8 +1644,7 @@ define('scope', { defaultDescription: ` the scope of the current project, if any, or "" `, - type: Types.String, - hint: '<@scope>', + type: Types.Scope, description: ` Associate an operation with a scope for a scoped registry. @@ -1863,13 +1670,8 @@ define('scope', { npm init --scope=@foo --yes \`\`\` `, - flatten (key, obj, flatOptions) { - const value = obj[key] - const scope = value && !/^@/.test(value) ? `@${value}` : value - flatOptions.scope = scope - // projectScope is kept for compatibility with npm-registry-fetch - flatOptions.projectScope = scope - }, + // projectScope is kept for compatibility with npm-registry-fetch + flatten: [true, 'project-scope'], }) define('script-shell', { @@ -1882,9 +1684,7 @@ define('script-shell', { The shell to use for scripts run with the \`npm exec\`, \`npm run\` and \`npm init \` commands. `, - flatten (key, obj, flatOptions) { - flatOptions.scriptShell = obj[key] || undefined - }, + flatten: true, }) define('searchexclude', { @@ -1893,35 +1693,26 @@ define('searchexclude', { description: ` Space-separated options that limit the results from search. `, - flatten (key, obj, flatOptions) { - flatOptions.search = flatOptions.search || { limit: 20 } - flatOptions.search.exclude = obj[key].toLowerCase() - }, + flatten: 'search.exclude', }) define('searchlimit', { default: 20, type: Types.Number, description: ` - Types.Number of items to limit search results to. Will not apply at all to + Number of items to limit search results to. Will not apply at all to legacy searches. `, - flatten (key, obj, flatOptions) { - flatOptions.search = flatOptions.search || {} - flatOptions.search.limit = obj[key] - }, + flatten: 'search.limit', }) define('searchopts', { default: '', - type: Types.String, + type: Types.Querystring, description: ` Space-separated options that are always passed to search. `, - flatten (key, obj, flatOptions) { - flatOptions.search = flatOptions.search || { limit: 20 } - flatOptions.search.opts = querystring.parse(obj[key]) - }, + flatten: 'search.opts', }) define('searchstaleness', { @@ -1931,14 +1722,11 @@ define('searchstaleness', { The age of the cache, in seconds, before another registry request is made if using legacy search endpoint. `, - flatten (key, obj, flatOptions) { - flatOptions.search = flatOptions.search || { limit: 20 } - flatOptions.search.staleness = obj[key] - }, + flatten: 'search.staleness', }) define('shell', { - default: shell, + default: Shell, defaultDescription: ` SHELL environment variable, or "bash" on Posix, or "cmd.exe" on Windows `, @@ -1946,7 +1734,7 @@ define('shell', { description: ` The shell to run for the \`npm explore\` command. `, - flatten, + flatten: true, }) define('shrinkwrap', { @@ -1958,10 +1746,7 @@ define('shrinkwrap', { description: ` Alias for --package-lock `, - flatten (key, obj, flatOptions) { - obj['package-lock'] = obj.shrinkwrap - definitions['package-lock'].flatten('package-lock', obj, flatOptions) - }, + flatten: 'package-lock', }) define('sign-git-commit', { @@ -1974,7 +1759,7 @@ define('sign-git-commit', { Note that git requires you to have set up GPG keys in your git configs for this to work properly. `, - flatten, + flatten: true, }) define('sign-git-tag', { @@ -1987,7 +1772,7 @@ define('sign-git-tag', { Note that git requires you to have set up GPG keys in your git configs for this to work properly. `, - flatten, + flatten: true, }) define('strict-peer-deps', { @@ -2008,7 +1793,7 @@ define('strict-peer-deps', { conflict and the packages involved. If \`--strict-peer-deps\` is set, then this warning is treated as a failure. `, - flatten, + flatten: true, }) define('strict-ssl', { @@ -2020,9 +1805,7 @@ define('strict-ssl', { See also the \`ca\` config. `, - flatten (key, obj, flatOptions) { - flatOptions.strictSSL = obj[key] - }, + flatten: true, }) define('tag', { @@ -2038,9 +1821,7 @@ define('tag', { When used by the \`npm diff\` command, this is the tag used to fetch the tarball that will be compared with the local files by default. `, - flatten (key, obj, flatOptions) { - flatOptions.defaultTag = obj[key] - }, + flatten: 'default-tag', }) define('tag-version-prefix', { @@ -2055,7 +1836,7 @@ define('tag-version-prefix', { like \`v1.0.0\`, _only use this property if it is absolutely necessary_. In particular, use care when overriding this setting for public packages. `, - flatten, + flatten: true, }) define('timing', { @@ -2079,7 +1860,7 @@ define('tmp', { The value returned by the Node.js \`os.tmpdir()\` method `, - type: Types.path, + type: Types.Path, deprecated: ` This setting is no longer used. npm stores temporary files in a special location in the cache, and they are managed by @@ -2110,11 +1891,11 @@ define('umask', { meaning that folders and executables are created with a mode of 0o755 and other files are created with a mode of 0o644. `, - flatten, + flatten: true, }) define('unicode', { - default: unicode, + default: Unicode, defaultDescription: ` false on windows, true on mac/unix systems with a unicode locale, as defined by the \`LC_ALL\`, \`LC_CTYPE\`, or \`LANG\` environment variables. @@ -2124,7 +1905,7 @@ define('unicode', { When set to true, npm uses unicode characters in the tree output. When false, it uses ascii characters instead of unicode glyphs. `, - flatten, + flatten: true, }) define('update-notifier', { @@ -2166,36 +1947,12 @@ define('user-agent', { * \`{ci}\` - The value of the \`ci-name\` config, if set, prefixed with \`ci/\`, or an empty string if \`ci-name\` is empty. `, - flatten (key, obj, flatOptions) { - const value = obj[key] - const ciName = obj['ci-name'] - let inWorkspaces = false - if (obj.workspaces || obj.workspace && obj.workspace.length) { - inWorkspaces = true - } - flatOptions.userAgent = - value.replace(/\{node-version\}/gi, process.version) - .replace(/\{npm-version\}/gi, npmVersion) - .replace(/\{platform\}/gi, process.platform) - .replace(/\{arch\}/gi, process.arch) - .replace(/\{workspaces\}/gi, inWorkspaces) - .replace(/\{ci\}/gi, ciName ? `ci/${ciName}` : '') - .trim() - - // We can't clobber the original or else subsequent flattening will fail - // (i.e. when we change the underlying config values) - // obj[key] = flatOptions.userAgent - - // user-agent is a unique kind of config item that gets set from a template - // and ends up translated. Because of this, the normal "should we set this - // to process.env also doesn't work - process.env.npm_config_user_agent = flatOptions.userAgent - }, + flatten: true, }) define('userconfig', { default: '~/.npmrc', - type: Types.path, + type: Types.Path, description: ` The location of user-level configuration settings. @@ -2243,8 +2000,7 @@ define('viewer', { define('which', { default: null, - hint: '', - type: [null, Types.Number], + type: [null, Types.PositiveInteger], description: ` If there are multiple funding sources, which 1-indexed source URL to open. `, @@ -2252,8 +2008,8 @@ define('which', { define('workspace', { default: [], - type: [Types.String, Types.Array], - hint: '', + type: [Types.String, Types.Path, Types.Array], + hint: 'workspace-name|workspace-path', short: 'w', envExport: false, description: ` @@ -2272,9 +2028,6 @@ define('workspace', { a workspace which does not yet exist, to create the folder and set it up as a brand new workspace within the project. `, - flatten: (key, obj, flatOptions) => { - definitions['user-agent'].flatten('user-agent', obj, flatOptions) - }, }) define('workspaces', { @@ -2296,17 +2049,6 @@ define('workspaces', { on the root project, _unless_ one or more workspaces are specified in the \`workspace\` config. `, - flatten: (key, obj, flatOptions) => { - definitions['user-agent'].flatten('user-agent', obj, flatOptions) - - // TODO: this is a derived value, and should be reworked when we have a - // pattern for derived value - - // workspacesEnabled is true whether workspaces is null or true - // commands contextually work with workspaces or not regardless of - // configuration, so we need an option specifically to disable workspaces - flatOptions.workspacesEnabled = obj[key] !== false - }, }) define('workspaces-update', { @@ -2316,7 +2058,7 @@ define('workspaces-update', { If set to true, the npm cli will run an update after operations that may possibly change the workspaces installed to the \`node_modules\` folder. `, - flatten, + flatten: true, }) define('yes', { @@ -2328,3 +2070,180 @@ define('yes', { the command line. `, }) + +// These are default values that cannot be overridden at any +// other level so they are defined here instead of definitions +// since we do not want to document them but they should still +// be applied to flat options + +flat('npm-command', '') +flat('npm-version', version) + +// the Arborist constructor is used almost everywhere we call pacote, it's +// easiest to attach it to flatOptions so it goes everywhere without having +// to touch every call +flat('Arborist', Arborist) + +// XXX should this be sha512? is it even relevant? +flat('hash-algorithm', 'sha1') + +// env vars have already been set in @npmcli/config based on +// other logic so it is safe to use it here +derive('npm-bin', () => process.env.npm_execpath) +derive('node-bin', () => process.env.NODE) + +derive(['omit', 'include'], ({ omit, include, dev, production, optional, also, only }) => { + const derived = { omit: [...omit], include: [...include] } + + if (/^prod(uction)?$/.test(only) || production) { + derived.omit.push('dev') + } else if (production === false) { + derived.include.push('dev') + } + + if (/^dev/.test(also)) { + derived.include.push('dev') + } + + if (dev) { + derived.include.push('dev') + } + + if (optional === false) { + derived.omit.push('optional') + } else if (optional === true) { + derived.include.push('optional') + } + + derived.omit = [...new Set(derived.omit)].filter(type => !derived.include.includes(type)) + derived.include = [...new Set(derived.include)] + + return derived +}, ['dev', 'production', 'optional', 'also', 'only']) + +derive(['global', 'location'], ({ global, location }) => { + const isGlobal = global || location === 'global' + return isGlobal ? { global: true, location: 'global' } : { global, location } +}) + +derive(['cache', 'npx-cache', 'logs-dir'], ({ cache, logsDir }) => { + return { + cache: join(cache, '_cacache'), + npxCache: join(cache, '_npx'), + logsDir: logsDir || join(cache, '_logs'), + } +}) + +derive('prefer-online', ({ cacheMax, preferOnline }) => { + return cacheMax <= 0 ? true : preferOnline +}, ['cache-max']) + +derive('prefer-offline', ({ cacheMin, preferOffline }) => { + return cacheMin >= 9999 ? true : preferOffline +}, ['cache-min']) + +derive('ca', ({ cafile }) => { + const raw = cafile ? maybeReadFile(cafile) : null + if (!raw) { + return + } + const delim = '-----END CERTIFICATE-----' + return raw.replace(/\r\n/g, '\n') + .split(delim) + .filter(s => s.trim()) + .map(s => s.trimLeft() + delim) +}, ['cafile']) + +derive('color', ({ color }) => { + return !color ? false : color === 'always' ? true : !!process.stdout.isTTY +}) + +derive('log-color', ({ color }) => { + return !color ? false : color === 'always' ? true : !!process.stderr.isTTY +}, ['color']) + +derive('search.limit', ({ searchlimit }) => { + return searchlimit +}, ['searchlimit']) + +derive('search.description', ({ description }) => { + return description +}, ['description']) + +derive('search.exclude', ({ searchexclude }) => { + return searchexclude.toLowerCase() +}, ['searchexclude']) + +derive('search.opts', ({ searchopts }) => { + return searchopts +}, ['searchopts']) + +derive('progress', ({ progress }) => { + return !progress ? false : !!process.stderr.isTTY && process.env.TERM !== 'dumb' +}) + +derive('save-bundle', ({ saveBundle, savePeer }) => { + // XXX update arborist to just ignore it if resulting saveType is peer + // otherwise this won't have the expected effect: + // + // npm config set save-peer true + // npm i foo --save-bundle --save-prod <-- should bundle + return saveBundle && !savePeer +}, ['save-peer']) + +derive('install-strategy', ({ globalStyle, legacyBundling, installStrategy }) => { + return globalStyle ? 'shallow' : legacyBundling ? 'nested' : installStrategy +}, ['global-style', 'legacy-bundling']) + +derive('save-prefix', ({ savePrefix, saveExact }) => { + return saveExact ? '' : savePrefix +}, ['save-exact']) + +derive('save-type', ({ saveDev, saveOptional, savePeer, saveProd }) => { + if (savePeer && saveOptional) { + return 'peerOptional' + } + if (savePeer) { + return 'peer' + } + if (saveOptional) { + return 'optional' + } + if (saveDev) { + return 'dev' + } + if (saveProd) { + return 'prod' + } +}, ['save-dev', 'save-optional', 'save-peer', 'save-prod']) + +derive('project-scope', ({ scope }) => { + return scope +}, ['scope']) + +derive('user-agent', ({ userAgent, ciName, workspaces, workspace }) => { + const ws = !!(workspaces || workspace?.length) + return userAgent.replace(/\{node-version\}/gi, process.version) + .replace(/\{npm-version\}/gi, version) + .replace(/\{platform\}/gi, process.platform) + .replace(/\{arch\}/gi, process.arch) + .replace(/\{workspaces\}/gi, ws) + .replace(/\{ci\}/gi, ciName ? `ci/${ciName}` : '') + .trim() +}, ['ci-name', 'workspaces', 'workspace']) + +derive('silent', ({ loglevel }) => { + return loglevel === 'silent' +}, ['loglevel']) + +derive(['workspaces-enabled'], ({ workspaces }) => { + return workspaces !== false +}, ['workspaces']) + +derive(['package-lock', 'package-lock-only'], ({ packageLock, packageLockOnly }) => { + const lock = !!(packageLock || packageLockOnly) + return { + packageLock: lock, + packageLockOnly: lock, + } +}) diff --git a/workspaces/config/lib/credentials.js b/workspaces/config/lib/credentials.js index ed4361705cc12..c5cd14b06c9d5 100644 --- a/workspaces/config/lib/credentials.js +++ b/workspaces/config/lib/credentials.js @@ -1,7 +1,21 @@ const nerfDart = require('./nerf-dart.js') const { ConfTypes } = require('./config-data') +// These are the configs that we can nerf-dart. Not all of them currently even +// *have* config definitions so we have to explicitly validate them here +const nerfDarts = [ + '_auth', + '_authToken', + 'username', + '_password', + 'email', + 'certfile', + 'keyfile', +] + class Credentials { + static nerfDarts = nerfDarts + #config = null constructor (config) { @@ -33,13 +47,9 @@ class Credentials { this.#set('email', email) } } - this.#delete(`${nerfed}:_authToken`) - this.#delete(`${nerfed}:_auth`) - this.#delete(`${nerfed}:_password`) - this.#delete(`${nerfed}:username`) - this.#delete(`${nerfed}:email`) - this.#delete(`${nerfed}:certfile`) - this.#delete(`${nerfed}:keyfile`) + for (const k of nerfDarts) { + this.#delete(`${nerfed}:${k}`) + } } setByURI (uri, { token, username, password, email, certfile, keyfile }) { diff --git a/workspaces/config/lib/index.js b/workspaces/config/lib/index.js index eb194c3d33a7b..ca7b558592c08 100644 --- a/workspaces/config/lib/index.js +++ b/workspaces/config/lib/index.js @@ -8,11 +8,12 @@ const { resolve, dirname, join } = require('path') const { homedir } = require('os') const fs = require('fs/promises') const TypeDefs = require('./type-defs.js') -const nerfDart = require('./nerf-dart.js') -const { setEnv, setProcess, setNpmEnv, sameConfigValue, ...SetEnv } = require('./set-envs.js') +const SetEnv = require('./set-envs.js') const { ErrInvalidAuth } = require('./errors') -const ConfigTypes = require('./config-types') const Credentials = require('./credentials.js') +const ConfigTypes = require('./config-locations') +const { definitions, defaults } = require('./config') +const { isNerfed } = require('./nerf-dart.js') const ConfTypes = ConfigTypes.ConfTypes const fileExists = (...p) => fs.stat(resolve(...p)) @@ -40,73 +41,56 @@ const settableGetter = (get, ...args) => Object.defineProperty(...args, { }) class Config { - static get TypeDefs () { - return TypeDefs - } - - static get Types () { - return TypeDefs.Types - } - - static get ConfTypes () { - return ConfTypes - } - - static get EnvKeys () { - return [...SetEnv.ALLOWED_ENV_KEYS.values()] - } - - static get ProcessKeys () { - return [...SetEnv.ALLOWED_PROCESS_KEYS.values()] - } + static TypeDefs = TypeDefs + static Types = TypeDefs.Types + static ConfigTypes = ConfigTypes.ConfigTypes + static EnvKeys = [...SetEnv.ALLOWED_ENV_KEYS.values()] + static ProcessKeys = [...SetEnv.ALLOWED_ENV_KEYS.values()] + static nerfDarts = Credentials.nerfDarts + + // state + #configData = null // required options in constructor - #definitions = null - #npmPath = null - #derived = null + #npmRoot = null + #argv = null + #cwdRoot = null // options just to override in tests, mostly #process = null - #argv = null #env = null - #execPath = null #platform = null + #execPath = null #cwd = null - #cwdRoot = null // set when we load configs #globalPrefix = null #localPrefix = null #localPackage = null - #loaded = false #home = null - #parsedArgv = null // from nopt - // built in constructor from definitions - #defaults = {} - // data configs for each config type - #configData = null + #parsedArgv = null + + // functions + #setEnv = null + #setNpmEnv = null #credentials = null constructor ({ - definitions, - shorthands, - flatten, - npmPath, - derived, + npmRoot, + argv, + cwdRoot, // pass in process to set everything, but also allow // overriding specific parts of process that are used + // these are only used for testing process: _process = process, env = _process.env, - argv = _process.argv, platform = _process.platform, execPath = _process.execPath, cwd = _process.cwd(), - cwdRoot = null, }) { - this.#definitions = definitions - this.#npmPath = npmPath + this.#npmRoot = npmRoot this.#process = _process this.#env = env @@ -120,36 +104,11 @@ class Config { TypeDefs.typeDefs.path.HOME = this.#home TypeDefs.typeDefs.path.PLATFORM = this.#platform - // turn the definitions into nopt's weirdo syntax - const types = {} - const deprecated = {} - const effects = {} - for (const [key, def] of Object.entries(this.#definitions)) { - this.#defaults[key] = def.default - types[key] = def.type - if (def.deprecated) { - deprecated[key] = def.deprecated.trim().replace(/\n +/, '\n') - } - if (def.derived) { - effects[key] = def.derived - } - } - Object.freeze(this.#definitions) - Object.freeze(this.#defaults) - Object.freeze(types) - Object.freeze(deprecated) - - this.#configData = new ConfigTypes({ - flatten, - deprecated, - shorthands, - effects, - derived, - types, - env: this.#env, - }) + this.#configData = new ConfigTypes({ env: this.#env }) this.#credentials = new Credentials(this) + this.#setEnv = (...args) => SetEnv.setEnv(this.#env, ...args) + this.#setNpmEnv = (...args) => SetEnv.npm.setEnv(this.#env, ...args) } // ============================================= @@ -214,13 +173,22 @@ class Config { // Get/Set/Find/Delete, etc. // // ============================================= - find (key) { + find (key, where) { + this.#assertLoaded() + return this.#find(key, where) + } + + #find (key, where = null) { + return this.#configData.find(where, key) + } + + has (key, where) { this.#assertLoaded() - return this.#find(key) + return this.#has(key, where) } - #find (key) { - return this.#configData.find(key) + #has (key, where = null) { + return this.#configData.has(where, key) } get (key, where) { @@ -263,10 +231,12 @@ class Config { // Config Type Loaders // // ============================================= - async load () { - if (this.#loaded) { - throw new Error(`attempting to call config.load() multiple times`) + async load (where, data) { + if (where) { + this.#assertLoaded() + return this.#configData.add(where, data) } + this.#assertLoaded(false) return this.#time('load', () => this.#load()) } @@ -307,7 +277,7 @@ class Config { if (node?.toUpperCase() !== this.#execPath.toUpperCase()) { log.verbose('node symlink', node) this.#execPath = node - setProcess(this.#process, 'execPath', node) + SetEnv.setProcess(this.#process, 'execPath', node) } }) @@ -326,7 +296,7 @@ class Config { } } - this.#loadObject({ ...this.#defaults, prefix: this.#globalPrefix }, ConfTypes.default) + this.#loadObject(ConfTypes.default, { ...defaults, prefix: this.#globalPrefix }) const { data } = this.#configData.get(ConfTypes.default) @@ -342,7 +312,7 @@ class Config { } async #loadBuiltin () { - await this.#loadFile(resolve(this.#npmPath, 'npmrc'), ConfTypes.builtin) + await this.#loadFile(resolve(this.#npmRoot, 'npmrc'), ConfTypes.builtin) } async #loadGlobal () { @@ -370,7 +340,7 @@ class Config { const config = this.#configData.get(ConfTypes.project) if (this.global) { - config.load(null, null, 'global mode enabled') + config.ignore('global mode enabled') return } @@ -381,44 +351,45 @@ class Config { // which causes some calamaties. So, we only load project config if // it doesn't match what the userconfig will be. if (projectFile === this.#get('userconfig')) { - config.load(null, null, 'same as "user" config') + config.ignore('same as "user" config') return } await this.#loadFile(projectFile, ConfTypes.project) } #loadEnv () { - const data = Object.entries(this.#env).reduce((acc, [envKey, envVal]) => { - if (!/^npm_config_/i.test(envKey) || envVal === '') { + const data = Object.entries(this.#env).reduce((acc, [key, val]) => { + if (SetEnv.npm.testKey(key) || !val) { return acc } - let key = envKey.slice('npm_config_'.length) - if (!key.startsWith('//')) { + const configKey = key.slice(SetEnv.npm.prefix.length) + if (isNerfed(configKey)) { // don't normalize nerf-darted keys - // don't replace _ at the start of the key - key = key.replace(/(?!^)_/g, '-').toLowerCase() + acc[configKey] = val + return acc } - acc[key] = envVal + // don't replace _ at the start of the key + acc[configKey.replace(/(?!^)_/g, '-').toLowerCase()] = val return acc }, {}) - this.#loadObject(data, ConfTypes.env) + this.#loadObject(ConfTypes.env, data) } #loadCli () { - const res = this.#loadObject(this.#argv.slice(2), ConfTypes.cli) - this.#parsedArgv = res?.argv ?? null + const { argv } = this.#loadObject(ConfTypes.cli, this.#argv.slice(2)) + this.#parsedArgv = argv } async #loadFile (file, where) { // only catch the error from readFile, not from the loadObject call await this.#time(`load:file:${file}`, () => fs.readFile(file, 'utf8').then( - data => this.#loadObject(data, where, file), - er => this.#loadObject(null, where, file, er) + data => this.#loadObject(where, data, file), + er => this.#loadObject(where, null, file, er) )) } - #loadObject (data, where, source, error) { - return this.#configData.get(where).load(data, error, source) + #loadObject (where, data, file, error) { + return this.#configData.get(where).load(data, error, file) } async #loadLocalPrefix () { @@ -497,8 +468,8 @@ class Config { // This ensures that all npm config values that are not the defaults are // shared appropriately with child processes, without false positives. #setEnvs () { - setNpmEnv(this.#env, 'global-prefix', this.#globalPrefix) - setNpmEnv(this.#env, 'local-prefix', this.#localPrefix) + this.#setNpmEnv('global-prefix', this.#globalPrefix) + this.#setNpmEnv('local-prefix', this.#localPrefix) // if the key is deprecated, skip it always. // if the key is the default value, @@ -508,84 +479,64 @@ class Config { // if the key is NOT the default value, // if the env is setting it, then leave it (already set) // otherwise, set the env - const configValues = this.#configData.reverseValues() - const cliConf = configValues.next().value - const envConf = configValues.next().value + const cliConf = this.#configData.get(ConfTypes.cli) + const envConf = this.#configData.get(ConfTypes.env) for (const [key, value] in cliConf.entries()) { - const { deprecated, envExport = true } = this.#definitions[key] || {} - if (deprecated || envExport === false) { + const def = definitions[key] + if (def?.deprecated || !def?.envExport) { continue } - if (sameConfigValue(this.#defaults[key], value)) { + if (SetEnv.sameValue(defaults[key], value)) { // config is the default, if the env thought different, then we // have to set it BACK to the default in the environment. - if (!sameConfigValue(envConf.get(key), value)) { - setNpmEnv(this.#env, key, value) + if (!SetEnv.sameValue(envConf.get(key), value)) { + this.#setNpmEnv(key, value) } } else { // config is not the default. if the env wasn't the one to set // it that way, then we have to put it in the env if (!(envConf.has(key) && !cliConf.has(key))) { - setNpmEnv(this.#env, key, value) + this.#setNpmEnv(key, value) } } } + // these depend on derived values so they use the flat data + this.#setNpmEnv('user-agent', this.flat.userAgent) + this.#setEnv('COLOR', this.flat.color ? '1' : '0') + this.#setEnv('NODE_ENV', this.flat.omit.includes('dev') ? 'production' : null) + // XXX make this the bin/npm-cli.js file explicitly instead + // otherwise using npm programmatically is a bit of a pain. + this.#setEnv('npm_execpath', this.flat.npmBin ?? null) + // also set some other common nice envs that we want to rely on - setEnv(this.#env, 'INIT_CWD', this.#cwd) - setEnv(this.#env, 'HOME', this.#home) - setEnv(this.#env, 'NODE', this.#execPath) - setEnv(this.#env, 'npm_node_execpath', this.#execPath) - setEnv(this.#env, 'npm_execpath', require.main?.filename ?? null) - setEnv(this.#env, 'EDITOR', cliConf.has('editor') ? cliConf.get('editor') : null) + this.#setEnv('INIT_CWD', this.#cwd) + this.#setEnv('HOME', this.#home) + this.#setEnv('NODE', this.#execPath) + this.#setEnv('npm_node_execpath', this.#execPath) + this.#setEnv('EDITOR', cliConf.has('editor') ? cliConf.get('editor') : null) // note: this doesn't afect the *current* node process, of course, since // it's already started, but it does affect the options passed to scripts. if (cliConf.has('node-options')) { - setEnv(this.#env, 'NODE_OPTIONS', cliConf.get('node-options')) + this.#setEnv('NODE_OPTIONS', cliConf.get('node-options')) } } // ============================================= // - // Save + // Save / Validation / Repair // // ============================================= async save (where) { this.#assertLoaded() - - if (where === ConfTypes.user) { - // if email is nerfed, then we want to de-nerf it - const nerfed = nerfDart(this.get('registry')) - const email = this.get(`${nerfed}:email`, where) - if (email) { - this.delete(`${nerfed}:email`, where) - this.set('email', email, where) - } - } - const conf = this.#configData.get(where) - const data = conf.toString() - - if (!data) { - // ignore the unlink error (eg, if file doesn't exist) - await fs.unlink(conf.source).catch(() => {}) - return - } - const dir = dirname(conf.source) - await fs.mkdir(dir, { recursive: true }) - await fs.writeFile(conf.source, data + '\n', 'utf8') - const mode = where === ConfTypes.user ? 0o600 : 0o666 - await fs.chmod(conf.source, mode) + await conf.save() + return conf } - // ============================================= - // - // Validation / Repair - // - // ============================================= validate () { this.#assertLoaded() @@ -649,9 +600,9 @@ class Config { // Utils/Misc // // ============================================= - #assertLoaded () { - if (!this.loaded) { - throw new Error('call config.load() before reading values') + #assertLoaded (val = true) { + if (this.loaded !== val) { + throw new Error(`config ${val ? 'must' : 'must not'} be loaded to perform this action`) } } diff --git a/workspaces/config/lib/nerf-dart.js b/workspaces/config/lib/nerf-dart.js index d6ae4aa2aa7e2..96e3c50579ba7 100644 --- a/workspaces/config/lib/nerf-dart.js +++ b/workspaces/config/lib/nerf-dart.js @@ -16,3 +16,5 @@ module.exports = (url) => { const res = `//${rel.host}${rel.pathname}` return res } + +module.exports.isNerfed = (s) => s.startsWith('//') diff --git a/workspaces/config/lib/set-envs.js b/workspaces/config/lib/set-envs.js index 396ad43c1fa06..337b14850576f 100644 --- a/workspaces/config/lib/set-envs.js +++ b/workspaces/config/lib/set-envs.js @@ -1,5 +1,6 @@ const NPM_PREFIX = `npm_` const CONFIG_PREFIX = `${NPM_PREFIX}config_` +const rePrefix = new RegExp(`^${CONFIG_PREFIX}`, 'i') // This is an allow list of env variables that this config // module can set. Note that this only applies to environs @@ -36,10 +37,6 @@ const envVal = val => Array.isArray(val) ? val.map(v => envVal(v)).join('\n\n') : typeof val === 'object' ? null : String(val) -const sameConfigValue = (def, val) => - !Array.isArray(val) || !Array.isArray(def) ? def === val - : sameArrayValue(def, val) - const sameArrayValue = (def, val) => { if (def.length !== val.length) { return false @@ -56,6 +53,10 @@ const sameArrayValue = (def, val) => { return true } +const sameValue = (def, val) => + !Array.isArray(val) || !Array.isArray(def) ? def === val + : sameArrayValue(def, val) + const setNpmEnv = (env, rawKey, rawVal) => { const val = envVal(rawVal) const key = envKey(rawKey, val) @@ -66,7 +67,7 @@ const setNpmEnv = (env, rawKey, rawVal) => { const setEnv = (env, key, rawVal) => { if (!key.startsWith(NPM_PREFIX) && !ALLOWED_ENV_KEYS.has(key)) { - throw new Error(`attempted to set non-allowed environ: ${key}`) + throw new Error(`not allowed to to set environ: \`${key}\``) } const val = envVal(rawVal) if (key && val !== null) { @@ -79,6 +80,10 @@ module.exports = { ALLOWED_ENV_KEYS, setProcess, setEnv, - setNpmEnv, - sameConfigValue, + sameValue, + npm: { + setEnv: setNpmEnv, + testKey: (k) => rePrefix.test(k), + prefix: CONFIG_PREFIX, + }, } diff --git a/workspaces/config/lib/type-defs.js b/workspaces/config/lib/type-defs.js index 9a194ac74756a..e17f482dba413 100644 --- a/workspaces/config/lib/type-defs.js +++ b/workspaces/config/lib/type-defs.js @@ -1,5 +1,6 @@ const nopt = require('nopt').lib const semver = require('semver') +const querystring = require('querystring') const { resolve } = require('path') const { networkInterfaces } = require('os') @@ -44,6 +45,29 @@ const validatePath = (data, k, val) => { } } +function validatePositiveNumber (data, k, val) { + if (isNaN(val)) { + return false + } + val = +val + if (val < 1) { + return false + } + data[k] = val +} + +const validateQs = (data, k, val) => { + data[k] = querystring.parse(val.replace(/\s+/g, '&')) +} + +const validateCsv = (data, k, val) => { + data[k] = val.split(',') +} + +const validateScope = (data, k, val) => { + data[k] = !/^@/.test(val) ? `@${val}` : val +} + const IpAddress = (() => { try { return [ @@ -61,11 +85,6 @@ const isStrictBool = (val) => { } } -// These properties are only used for displaying appropriate config usage -const isString = { isString: true } -const isBoolean = { isBoolean: true } -const isNumber = { isNumber: true } - // `description` gets shown during a runtime validation warning // `typeDescription` gets displayed in the docs for the `Type:` const typeDefs = { @@ -73,47 +92,77 @@ const typeDefs = { ...nopt.typeDefs.String, type: Symbol('String'), typeDescription: 'String', - ...isString, + description: 'a string', }, Boolean: { ...nopt.typeDefs.Boolean, type: Symbol('Boolean'), typeDescription: 'Boolean', - description: 'boolean value (true or false)', - ...isBoolean, + description: 'a boolean value (true or false)', + isBoolean: true, }, + // todo: when type is all numbers, allow string verion of those numbers too Number: { ...nopt.typeDefs.Number, type: Symbol('Number'), typeDescription: 'Number', - description: 'numeric value', - ...isNumber, + description: 'a numeric value', + hint: 'number', + }, + PositiveInteger: { + type: Symbol('PositiveInteger'), + validate: validatePositiveNumber, + typeDescription: 'Positive integer', + description: 'an integer greater than or equal to 1', + hint: '1|2|3|n', }, Date: { ...nopt.typeDefs.Date, type: Symbol('Date'), typeDescription: 'Date', - description: 'valid Date string', + description: 'a valid Date string', }, - url: { + URL: { ...nopt.typeDefs.url, type: Symbol('URL'), typeDescription: 'URL', - description: 'full url with "http://"', + description: 'a full url with "http://"', }, - path: { + Querystring: { + type: Symbol('Querystring'), + validate: validateQs, + typeDescription: 'Querystring', + description: 'a space-delimited querystring', + hint: 'key=val key2=val2', + }, + CSV: { + type: Symbol('CSV'), + validate: validateCsv, + typeDescription: 'Comma-delimited string', + description: 'a comma-delimited string', + }, + Scope: { + type: Symbol('Scope'), + validate: validateScope, + typeDescription: 'Scope', + description: 'an npm scope with or without the leading @', + hint: '@scope', + }, + Spec: { + type: Symbol('Spec'), + validate: nopt.typeDefs.String.validate, + typeDescription: 'Spec', + description: 'an npm package spec', + hint: 'package-spec', + }, + Path: { ...nopt.typeDefs.path, type: Symbol('Path'), validate: validatePath, typeDescription: 'Path', - description: 'valid filesystem path', - }, - Stream: { - ...nopt.typeDefs.Stream, - type: Symbol('Stream'), - typeDescription: 'Stream', + description: 'a valid filesystem path', }, - semver: { + Semver: { type: Symbol('Semver'), validate: (data, k, val) => { const valid = semver.valid(val) @@ -123,19 +172,22 @@ const typeDefs = { data[k] = valid }, typeDescription: 'SemVer string', - description: 'full valid SemVer string', + description: 'a full valid SemVer string', }, Umask: { type: Symbol('Umask'), validate: validateUmask, typeDescription: 'Octal numeric string in range 0000..0777 (0..511)', - description: 'octal number in range 0o000..0o777 (0..511)', + description: 'an octal number in range 0o000..0o777 (0..511)', }, IpAddress: { // this one cant be a symbol because it is an actual array of local // ip addresses for the current runtime type: IpAddress, typeDescription: 'IP Address', + // this explicitly has no description since it is an array of values + // that will be shown to the user when it is invalid + description: null, }, BooleanOrString: { type: Symbol('BooleanOrString'), @@ -146,9 +198,8 @@ const typeDefs = { return nopt.typeDefs.String.validate(data, k, val) }, typeDescription: ['Boolean', 'String'], - description: 'boolean value (true or false) or a string', - ...isBoolean, - ...isString, + description: ['a boolean value (true or false)', 'a string'], + isBoolean: true, }, BooleanOrNumber: { type: Symbol('BooleanOrNumber'), @@ -159,10 +210,11 @@ const typeDefs = { return nopt.typeDefs.Number.validate(data, k, val) }, typeDescription: ['Boolean', 'Number'], - description: 'boolean value (true or false) or a string', - ...isBoolean, - ...isNumber, + description: ['a boolean value (true or false)', 'a a numeric valid'], + isBoolean: true, }, + // the array type has no validation or description its presence in a + // definition signals that more than one value of the other types are allowed Array: { type: Symbol('Array'), }, @@ -170,20 +222,18 @@ const typeDefs = { const byType = new Map() const Types = {} +const TypesList = [] const getType = (k) => byType.get(k) for (const [key, value] of Object.entries(typeDefs)) { - // allow looking up a full type def by string key or type value byType.set(value.type, value) - byType.set(key, value) Types[key] = value.type + TypesList.push(value.type) } module.exports = { typeDefs, Types, + TypesList, getType, - isString: (ts) => [].concat(ts).some(t => getType(t)?.isString), - isBoolean: (ts) => [].concat(ts).some(t => getType(t)?.isBoolean), - isNumber: (ts) => [].concat(ts).some(t => getType(t)?.isNumber), } diff --git a/workspaces/config/package.json b/workspaces/config/package.json index a06483db7e327..701836a1a89ba 100644 --- a/workspaces/config/package.json +++ b/workspaces/config/package.json @@ -37,7 +37,9 @@ "tap": "^16.3.2" }, "dependencies": { + "@npmcli/arborist": "^6.1.5", "@npmcli/map-workspaces": "^3.0.0", + "ci-info": "^3.7.1", "ini": "^3.0.0", "nopt": "file:../../../nopt/nopt-7.0.0.tgz", "proc-log": "^3.0.0", diff --git a/test/lib/utils/config/definition.js b/workspaces/config/test/config/definition.js similarity index 100% rename from test/lib/utils/config/definition.js rename to workspaces/config/test/config/definition.js diff --git a/test/lib/utils/config/definitions.js b/workspaces/config/test/config/definitions.js similarity index 100% rename from test/lib/utils/config/definitions.js rename to workspaces/config/test/config/definitions.js diff --git a/test/lib/utils/config/index.js b/workspaces/config/test/config/index.js similarity index 95% rename from test/lib/utils/config/index.js rename to workspaces/config/test/config/index.js index cb323e4b716cd..e38beb4f013ce 100644 --- a/test/lib/utils/config/index.js +++ b/workspaces/config/test/config/index.js @@ -1,7 +1,7 @@ const t = require('tap') const config = require('../../../../lib/utils/config/index.js') const { definitions } = require('../../../../lib/utils/config/definitions.js') -const mockGlobals = require('../../../fixtures/mock-globals.js') +const mockGlobals = require('../../../../test/fixtures/mock-globals.js') t.test('defaults', t => { // just spot check a few of these to show that we got defaults assembled diff --git a/workspaces/config/test/fixtures/defaults.js b/workspaces/config/test/fixtures/defaults.js deleted file mode 100644 index 322ceb018bf31..0000000000000 --- a/workspaces/config/test/fixtures/defaults.js +++ /dev/null @@ -1,143 +0,0 @@ -module.exports = { - methane: 'CH4', - access: null, - all: false, - 'allow-same-version': false, - 'always-auth': false, - also: null, - audit: true, - 'audit-level': null, - 'auth-type': 'legacy', - - before: null, - 'bin-links': true, - browser: null, - - ca: null, - cafile: null, - - cache: '~/.npm', - - 'cache-lock-stale': 60000, - 'cache-lock-retries': 10, - 'cache-lock-wait': 10000, - - 'cache-max': Infinity, - 'cache-min': 10, - - cert: null, - - cidr: null, - - color: true, - call: '', - depth: 0, - description: true, - dev: false, - 'dry-run': false, - editor: 'vim', - 'engine-strict': false, - force: false, - 'format-package-lock': true, - - fund: true, - - 'fetch-retries': 2, - 'fetch-retry-factor': 10, - 'fetch-retry-mintimeout': 10000, - 'fetch-retry-maxtimeout': 60000, - - git: 'git', - 'git-tag-version': true, - 'commit-hooks': true, - - global: false, - 'global-style': false, - heading: 'npm', - 'if-present': false, - include: [], - 'include-staged': false, - 'ignore-prepublish': false, - 'ignore-scripts': false, - 'init-module': '~/.npm-init.js', - 'init-author-name': '', - 'init-author-email': '', - 'init-author-url': '', - 'init-version': '1.0.0', - 'init-license': 'ISC', - json: false, - key: null, - 'legacy-bundling': false, - 'legacy-peer-deps': false, - link: false, - 'local-address': undefined, - loglevel: 'notice', - 'logs-max': 10, - long: false, - maxsockets: 50, - message: '%s', - 'metrics-registry': null, - 'node-options': null, - 'node-version': process.version, - offline: false, - omit: [], - only: null, - optional: true, - otp: null, - package: [], - 'package-lock': true, - 'package-lock-only': false, - parseable: false, - 'prefer-offline': false, - 'prefer-online': false, - preid: '', - production: true, - progress: true, - proxy: null, - 'https-proxy': null, - noproxy: null, - 'user-agent': 'npm/{npm-version} ' + - 'node/{node-version} ' + - '{platform} ' + - '{arch} ' + - '{ci}', - 'read-only': false, - 'rebuild-bundle': true, - registry: 'https://registry.npmjs.org/', - rollback: true, - save: true, - 'save-bundle': false, - 'save-dev': false, - 'save-exact': false, - 'save-optional': false, - 'save-prefix': '^', - 'save-prod': false, - scope: '', - 'script-shell': null, - 'scripts-prepend-node-path': 'warn-only', - searchopts: '', - searchexclude: null, - searchlimit: 20, - searchstaleness: 15 * 60, - 'send-metrics': false, - shell: '/bin/sh', - shrinkwrap: true, - 'sign-git-commit': false, - 'sign-git-tag': false, - 'sso-poll-frequency': 500, - 'sso-type': 'oauth', - 'strict-ssl': true, - tag: 'latest', - 'tag-version-prefix': 'v', - timing: false, - unicode: /UTF-?8$/i.test( - process.env.LC_ALL || process.env.LC_CTYPE || process.env.LANG - ), - 'update-notifier': true, - usage: false, - userconfig: '~/.npmrc', - umask: 0o22, - version: false, - versions: false, - viewer: 'man', -} diff --git a/workspaces/config/test/fixtures/definitions.js b/workspaces/config/test/fixtures/definitions.js deleted file mode 100644 index ce0aff6f3c457..0000000000000 --- a/workspaces/config/test/fixtures/definitions.js +++ /dev/null @@ -1,2609 +0,0 @@ -const url = require('url') -const path = require('path') -const { join } = path -const querystring = require('querystring') -const semver = require('semver') -const Umask = require('../../lib/type-defs.js').Umask.type - -// dumped out of npm/cli/lib/utils/config/definitions.js - -// used by cafile flattening to flatOptions.ca -const fs = require('fs') -const maybeReadFile = file => { - if (file.includes('WEIRD-ERROR')) { - throw Object.assign(new Error('weird error'), { code: 'EWEIRD' }) - } - - try { - return fs.readFileSync(file, 'utf8') - } catch (er) { - if (er.code !== 'ENOENT') { - throw er - } - return null - } -} - -const definitions = module.exports = { - methane: { - envExport: false, - type: String, - typeDescription: 'Greenhouse Gas', - default: 'CH4', - description: ` - This is bad for the environment, for our children, do not put it there. - `, - }, - 'multiple-numbers': { - key: 'multiple-numbers', - default: [], - type: [ - Array, - Number, - ], - descriptions: 'one or more numbers', - }, - _auth: { - key: '_auth', - default: null, - type: [ - null, - String, - ], - description: ` - A basic-auth string to use when authenticating against the npm registry. - - Warning: This should generally not be set via a command-line option. It - is safer to use a registry-provided authentication bearer token stored in - the ~/.npmrc file by running \`npm login\`. - `, - defaultDescription: 'null', - typeDescription: 'null or String', - }, - access: { - key: 'access', - default: null, - defaultDescription: ` - 'restricted' for scoped packages, 'public' for unscoped packages - `, - type: [ - null, - 'restricted', - 'public', - ], - description: ` - When publishing scoped packages, the access level defaults to - \`restricted\`. If you want your scoped package to be publicly viewable - (and installable) set \`--access=public\`. The only valid values for - \`access\` are \`public\` and \`restricted\`. Unscoped packages _always_ - have an access level of \`public\`. - - Note: Using the \`--access\` flag on the \`npm publish\` command will only - set the package access level on the initial publish of the package. Any - subsequent \`npm publish\` commands using the \`--access\` flag will not - have an effect to the access level. To make changes to the access level - after the initial publish use \`npm access\`. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - typeDescription: 'null, "restricted", or "public"', - }, - all: { - key: 'all', - default: false, - type: Boolean, - short: 'a', - description: ` - When running \`npm outdated\` and \`npm ls\`, setting \`--all\` will show - all outdated or installed packages, rather than only those directly - depended upon by the current project. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'allow-same-version': { - key: 'allow-same-version', - default: false, - type: Boolean, - description: ` - Prevents throwing an error when \`npm version\` is used to set the new - version to the same value as the current version. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - also: { - key: 'also', - default: null, - type: [ - null, - 'dev', - 'development', - ], - description: ` - When set to \`dev\` or \`development\`, this is an alias for - \`--include=dev\`. - `, - deprecated: 'Please use --include=dev instead.', - flatten (key, obj, flatOptions) { - if (!/^dev(elopment)?$/.test(obj.also)) { - return - } - - // add to include, and call the omit flattener - obj.include = obj.include || [] - obj.include.push('dev') - definitions.omit.flatten('omit', obj, flatOptions) - }, - defaultDescription: 'null', - typeDescription: 'null, "dev", or "development"', - }, - audit: { - key: 'audit', - default: true, - type: Boolean, - description: ` - When "true" submit audit reports alongside the current npm command to the - default registry and all registries configured for scopes. See the - documentation for [\`npm audit\`](/commands/npm-audit) for details on what - is submitted. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'true', - typeDescription: 'Boolean', - }, - 'audit-level': { - key: 'audit-level', - default: null, - type: [ - 'low', - 'moderate', - 'high', - 'critical', - 'none', - null, - ], - description: ` - The minimum level of vulnerability for \`npm audit\` to exit with - a non-zero exit code. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'null', - typeDescription: '"low", "moderate", "high", "critical", "none", or null', - }, - 'auth-type': { - key: 'auth-type', - default: 'legacy', - type: [ - 'legacy', - 'sso', - 'saml', - 'oauth', - ], - deprecated: ` - This method of SSO/SAML/OAuth is deprecated and will be removed in - a future version of npm in favor of web-based login. - `, - description: ` - What authentication strategy to use with \`adduser\`/\`login\`. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: '"legacy"', - typeDescription: '"legacy", "sso", "saml", or "oauth"', - }, - before: { - key: 'before', - default: null, - type: [ - null, - Date, - ], - description: ` - If passed to \`npm install\`, will rebuild the npm tree such that only - versions that were available **on or before** the \`--before\` time get - installed. If there's no versions available for the current set of - direct dependencies, the command will error. - - If the requested version is a \`dist-tag\` and the given tag does not - pass the \`--before\` filter, the most recent version less than or equal - to that tag will be used. For example, \`foo@latest\` might install - \`foo@1.2\` even though \`latest\` is \`2.0\`. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'null', - typeDescription: 'null or Date', - }, - 'bin-links': { - key: 'bin-links', - default: true, - type: Boolean, - description: ` - Tells npm to create symlinks (or \`.cmd\` shims on Windows) for package - executables. - - Set to false to have it not do this. This can be used to work around the - fact that some file systems don't support symlinks, even on ostensibly - Unix systems. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'true', - typeDescription: 'Boolean', - }, - browser: { - key: 'browser', - default: null, - defaultDescription: ` - OS X: \`"open"\`, Windows: \`"start"\`, Others: \`"xdg-open"\` - `, - type: [ - null, - Boolean, - String, - ], - description: ` - The browser that is called by npm commands to open websites. - - Set to \`false\` to suppress browser behavior and instead print urls to - terminal. - - Set to \`true\` to use default system URL opener. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - typeDescription: 'null, Boolean, or String', - }, - ca: { - key: 'ca', - default: null, - type: [ - null, - String, - Array, - ], - description: ` - The Certificate Authority signing certificate that is trusted for SSL - connections to the registry. Values should be in PEM format (Windows - calls it "Base-64 encoded X.509 (.CER)") with newlines replaced by the - string "\\n". For example: - - \`\`\`ini - ca="-----BEGIN CERTIFICATE-----\\nXXXX\\nXXXX\\n-----END CERTIFICATE-----" - \`\`\` - - Set to \`null\` to only allow "known" registrars, or to a specific CA - cert to trust only that specific signing authority. - - Multiple CAs can be trusted by specifying an array of certificates: - - \`\`\`ini - ca[]="..." - ca[]="..." - \`\`\` - - See also the \`strict-ssl\` config. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'null', - typeDescription: 'null or String (can be set multiple times)', - }, - cache: { - key: 'cache', - default: '~/.npm', - defaultDescription: ` - Windows: \`%LocalAppData%\\npm-cache\`, Posix: \`~/.npm\` - `, - type: path, - description: ` - The location of npm's cache directory. See [\`npm - cache\`](/commands/npm-cache) - `, - flatten (key, obj, flatOptions) { - flatOptions.cache = join(obj.cache, '_cacache') - }, - typeDescription: 'Path', - }, - 'cache-max': { - key: 'cache-max', - default: null, - type: Number, - description: ` - \`--cache-max=0\` is an alias for \`--prefer-online\` - `, - deprecated: ` - This option has been deprecated in favor of \`--prefer-online\` - `, - flatten (key, obj, flatOptions) { - if (obj[key] <= 0) { - flatOptions.preferOnline = true - } - }, - defaultDescription: 'Infinity', - typeDescription: 'Number', - }, - 'cache-min': { - key: 'cache-min', - default: 0, - type: Number, - description: ` - \`--cache-min=9999 (or bigger)\` is an alias for \`--prefer-offline\`. - `, - deprecated: ` - This option has been deprecated in favor of \`--prefer-offline\`. - `, - flatten (key, obj, flatOptions) { - if (obj[key] >= 9999) { - flatOptions.preferOffline = true - } - }, - defaultDescription: '0', - typeDescription: 'Number', - }, - cafile: { - key: 'cafile', - default: null, - type: path, - description: ` - A path to a file containing one or multiple Certificate Authority signing - certificates. Similar to the \`ca\` setting, but allows for multiple - CA's, as well as for the CA information to be stored in a file on disk. - `, - flatten (key, obj, flatOptions) { - // always set to null in defaults - if (!obj.cafile) { - return - } - - const raw = maybeReadFile(obj.cafile) - if (!raw) { - return - } - - const delim = '-----END CERTIFICATE-----' - flatOptions.ca = raw.replace(/\r\n/g, '\n').split(delim) - .filter(section => section.trim()) - .map(section => section.trimLeft() + delim) - }, - defaultDescription: 'null', - typeDescription: 'Path', - }, - call: { - key: 'call', - default: '', - type: String, - short: 'c', - description: ` - Optional companion option for \`npm exec\`, \`npx\` that allows for - specifying a custom command to be run along with the installed packages. - - \`\`\`bash - npm exec --package yo --package generator-node --call "yo node" - \`\`\` - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: '""', - typeDescription: 'String', - }, - cert: { - key: 'cert', - default: null, - type: [ - null, - String, - ], - description: ` - A client certificate to pass when accessing the registry. Values should - be in PEM format (Windows calls it "Base-64 encoded X.509 (.CER)") with - newlines replaced by the string "\\n". For example: - - \`\`\`ini - cert="-----BEGIN CERTIFICATE-----\\nXXXX\\nXXXX\\n-----END CERTIFICATE-----" - \`\`\` - - It is _not_ the path to a certificate file (and there is no "certfile" - option). - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'null', - typeDescription: 'null or String', - }, - 'ci-name': { - key: 'ci-name', - default: null, - defaultDescription: ` - The name of the current CI system, or \`null\` when not on a known CI - platform. - `, - type: [ - null, - String, - ], - description: ` - The name of a continuous integration system. If not set explicitly, npm - will detect the current CI environment using the - [\`@npmcli/ci-detect\`](http://npm.im/@npmcli/ci-detect) module. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - typeDescription: 'null or String', - }, - cidr: { - key: 'cidr', - default: null, - type: [ - null, - String, - Array, - ], - description: ` - This is a list of CIDR address to be used when configuring limited access - tokens with the \`npm token create\` command. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'null', - typeDescription: 'null or String (can be set multiple times)', - }, - color: { - key: 'color', - default: true, - defaultDescription: ` - true unless the NO_COLOR environ is set to something other than '0' - `, - type: [ - 'always', - Boolean, - ], - description: ` - If false, never shows colors. If \`"always"\` then always shows colors. - If true, then only prints color codes for tty file descriptors. - `, - flatten (key, obj, flatOptions) { - flatOptions.color = !obj.color ? false - : obj.color === 'always' ? true - : process.stdout.isTTY - }, - typeDescription: '"always" or Boolean', - }, - 'commit-hooks': { - key: 'commit-hooks', - default: true, - type: Boolean, - description: ` - Run git commit hooks when using the \`npm version\` command. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'true', - typeDescription: 'Boolean', - }, - depth: { - key: 'depth', - default: null, - defaultDescription: '\n `Infinity` if `--all` is set, otherwise `1`\n ', - type: [ - null, - Number, - ], - description: ` - The depth to go when recursing packages for \`npm ls\`. - - If not set, \`npm ls\` will show only the immediate dependencies of the - root project. If \`--all\` is set, then npm will show all dependencies - by default. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - typeDescription: 'null or Number', - }, - description: { - key: 'description', - default: true, - type: Boolean, - description: ` - Show the description in \`npm search\` - `, - flatten (key, obj, flatOptions) { - flatOptions.search = flatOptions.search || { limit: 20 } - flatOptions.search[key] = obj[key] - }, - defaultDescription: 'true', - typeDescription: 'Boolean', - }, - diff: { - key: 'diff', - default: [], - type: [ - String, - Array, - ], - description: ` - Define arguments to compare in \`npm diff\`. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: '', - typeDescription: 'String (can be set multiple times)', - }, - 'diff-ignore-all-space': { - key: 'diff-ignore-all-space', - default: false, - type: Boolean, - description: ` - Ignore whitespace when comparing lines in \`npm diff\`. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'diff-name-only': { - key: 'diff-name-only', - default: false, - type: Boolean, - description: ` - Prints only filenames when using \`npm diff\`. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'diff-no-prefix': { - key: 'diff-no-prefix', - default: false, - type: Boolean, - description: ` - Do not show any source or destination prefix in \`npm diff\` output. - - Note: this causes \`npm diff\` to ignore the \`--diff-src-prefix\` and - \`--diff-dst-prefix\` configs. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'diff-dst-prefix': { - key: 'diff-dst-prefix', - default: 'b/', - type: String, - description: ` - Destination prefix to be used in \`npm diff\` output. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: '"b/"', - typeDescription: 'String', - }, - 'diff-src-prefix': { - key: 'diff-src-prefix', - default: 'a/', - type: String, - description: ` - Source prefix to be used in \`npm diff\` output. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: '"a/"', - typeDescription: 'String', - }, - 'diff-text': { - key: 'diff-text', - default: false, - type: Boolean, - description: ` - Treat all files as text in \`npm diff\`. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'diff-unified': { - key: 'diff-unified', - default: 3, - type: Number, - description: ` - The number of lines of context to print in \`npm diff\`. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: '3', - typeDescription: 'Number', - }, - 'dry-run': { - key: 'dry-run', - default: false, - type: Boolean, - description: ` - Indicates that you don't want npm to make any changes and that it should - only report what it would have done. This can be passed into any of the - commands that modify your local installation, eg, \`install\`, - \`update\`, \`dedupe\`, \`uninstall\`, as well as \`pack\` and - \`publish\`. - - Note: This is NOT honored by other network related commands, eg - \`dist-tags\`, \`owner\`, etc. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - editor: { - key: 'editor', - default: 'vim', - defaultDescription: ` - The EDITOR or VISUAL environment variables, or 'notepad.exe' on Windows, - or 'vim' on Unix systems - `, - type: String, - description: ` - The command to run for \`npm edit\` and \`npm config edit\`. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - typeDescription: 'String', - }, - 'engine-strict': { - key: 'engine-strict', - default: false, - type: Boolean, - description: ` - If set to true, then npm will stubbornly refuse to install (or even - consider installing) any package that claims to not be compatible with - the current Node.js version. - - This can be overridden by setting the \`--force\` flag. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'fetch-retries': { - key: 'fetch-retries', - default: 2, - type: Number, - description: ` - The "retries" config for the \`retry\` module to use when fetching - packages from the registry. - - npm will retry idempotent read requests to the registry in the case - of network failures or 5xx HTTP errors. - `, - flatten (key, obj, flatOptions) { - flatOptions.retry = flatOptions.retry || {} - flatOptions.retry.retries = obj[key] - }, - defaultDescription: '2', - typeDescription: 'Number', - }, - 'fetch-retry-factor': { - key: 'fetch-retry-factor', - default: 10, - type: Number, - description: ` - The "factor" config for the \`retry\` module to use when fetching - packages. - `, - flatten (key, obj, flatOptions) { - flatOptions.retry = flatOptions.retry || {} - flatOptions.retry.factor = obj[key] - }, - defaultDescription: '10', - typeDescription: 'Number', - }, - 'fetch-retry-maxtimeout': { - key: 'fetch-retry-maxtimeout', - default: 60000, - defaultDescription: '60000 (1 minute)', - type: Number, - description: ` - The "maxTimeout" config for the \`retry\` module to use when fetching - packages. - `, - flatten (key, obj, flatOptions) { - flatOptions.retry = flatOptions.retry || {} - flatOptions.retry.maxTimeout = obj[key] - }, - typeDescription: 'Number', - }, - 'fetch-retry-mintimeout': { - key: 'fetch-retry-mintimeout', - default: 10000, - defaultDescription: '10000 (10 seconds)', - type: Number, - description: ` - The "minTimeout" config for the \`retry\` module to use when fetching - packages. - `, - flatten (key, obj, flatOptions) { - flatOptions.retry = flatOptions.retry || {} - flatOptions.retry.minTimeout = obj[key] - }, - typeDescription: 'Number', - }, - 'fetch-timeout': { - key: 'fetch-timeout', - default: 300000, - defaultDescription: '300000 (5 minutes)', - type: Number, - description: ` - The maximum amount of time to wait for HTTP requests to complete. - `, - flatten (key, obj, flatOptions) { - flatOptions.timeout = obj[key] - }, - typeDescription: 'Number', - }, - force: { - key: 'force', - default: false, - type: Boolean, - short: 'f', - description: ` - Removes various protections against unfortunate side effects, common - mistakes, unnecessary performance degradation, and malicious input. - - * Allow clobbering non-npm files in global installs. - * Allow the \`npm version\` command to work on an unclean git repository. - * Allow deleting the cache folder with \`npm cache clean\`. - * Allow installing packages that have an \`engines\` declaration - requiring a different version of npm. - * Allow installing packages that have an \`engines\` declaration - requiring a different version of \`node\`, even if \`--engine-strict\` - is enabled. - * Allow \`npm audit fix\` to install modules outside your stated - dependency range (including SemVer-major changes). - * Allow unpublishing all versions of a published package. - * Allow conflicting peerDependencies to be installed in the root project. - * Implicitly set \`--yes\` during \`npm init\`. - * Allow clobbering existing values in \`npm pkg\` - - If you don't have a clear idea of what you want to do, it is strongly - recommended that you do not use this option! - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'foreground-scripts': { - key: 'foreground-scripts', - default: false, - type: Boolean, - description: ` - Run all build scripts (ie, \`preinstall\`, \`install\`, and - \`postinstall\`) scripts for installed packages in the foreground - process, sharing standard input, output, and error with the main npm - process. - - Note that this will generally make installs run slower, and be much - noisier, but can be useful for debugging. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'format-package-lock': { - key: 'format-package-lock', - default: true, - type: Boolean, - description: ` - Format \`package-lock.json\` or \`npm-shrinkwrap.json\` as a human - readable file. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'true', - typeDescription: 'Boolean', - }, - fund: { - key: 'fund', - default: true, - type: Boolean, - description: ` - When "true" displays the message at the end of each \`npm install\` - acknowledging the number of dependencies looking for funding. - See [\`npm fund\`](/commands/npm-fund) for details. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'true', - typeDescription: 'Boolean', - }, - git: { - key: 'git', - default: 'git', - type: String, - description: ` - The command to use for git commands. If git is installed on the - computer, but is not in the \`PATH\`, then set this to the full path to - the git binary. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: '"git"', - typeDescription: 'String', - }, - 'git-tag-version': { - key: 'git-tag-version', - default: true, - type: Boolean, - description: ` - Tag the commit when using the \`npm version\` command. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'true', - typeDescription: 'Boolean', - }, - global: { - key: 'global', - default: false, - type: Boolean, - short: 'g', - description: ` - Operates in "global" mode, so that packages are installed into the - \`prefix\` folder instead of the current working directory. See - [folders](/configuring-npm/folders) for more on the differences in - behavior. - - * packages are installed into the \`{prefix}/lib/node_modules\` folder, - instead of the current working directory. - * bin files are linked to \`{prefix}/bin\` - * man pages are linked to \`{prefix}/share/man\` - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'global-style': { - key: 'global-style', - default: false, - type: Boolean, - description: ` - Causes npm to install the package into your local \`node_modules\` folder - with the same layout it uses with the global \`node_modules\` folder. - Only your direct dependencies will show in \`node_modules\` and - everything they depend on will be flattened in their \`node_modules\` - folders. This obviously will eliminate some deduping. If used with - \`legacy-bundling\`, \`legacy-bundling\` will be preferred. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - globalconfig: { - key: 'globalconfig', - type: path, - default: '', - defaultDescription: ` - The global --prefix setting plus 'etc/npmrc'. For example, - '/usr/local/etc/npmrc' - `, - description: ` - The config file to read for global config options. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - typeDescription: 'Path', - }, - heading: { - key: 'heading', - default: 'npm', - type: String, - description: ` - The string that starts all the debugging log output. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: '"npm"', - typeDescription: 'String', - }, - 'https-proxy': { - key: 'https-proxy', - default: null, - type: [ - null, - url, - ], - description: ` - A proxy to use for outgoing https requests. If the \`HTTPS_PROXY\` or - \`https_proxy\` or \`HTTP_PROXY\` or \`http_proxy\` environment variables - are set, proxy settings will be honored by the underlying - \`make-fetch-happen\` library. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'null', - typeDescription: 'null or URL', - }, - 'if-present': { - key: 'if-present', - default: false, - type: Boolean, - description: ` - If true, npm will not exit with an error code when \`run-script\` is - invoked for a script that isn't defined in the \`scripts\` section of - \`package.json\`. This option can be used when it's desirable to - optionally run a script when it's present and fail if the script fails. - This is useful, for example, when running scripts that may only apply for - some builds in an otherwise generic CI setup. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'ignore-scripts': { - key: 'ignore-scripts', - default: false, - type: Boolean, - description: ` - If true, npm does not run scripts specified in package.json files. - - Note that commands explicitly intended to run a particular script, such - as \`npm start\`, \`npm stop\`, \`npm restart\`, \`npm test\`, and \`npm - run-script\` will still run their intended script if \`ignore-scripts\` is - set, but they will *not* run any pre- or post-scripts. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - include: { - key: 'include', - default: [], - type: [ - Array, - 'prod', - 'dev', - 'optional', - 'peer', - ], - description: ` - Option that allows for defining which types of dependencies to install. - - This is the inverse of \`--omit=\`. - - Dependency types specified in \`--include\` will not be omitted, - regardless of the order in which omit/include are specified on the - command-line. - `, - flatten (key, obj, flatOptions) { - // just call the omit flattener, it reads from obj.include - definitions.omit.flatten('omit', obj, flatOptions) - }, - defaultDescription: '', - typeDescription: '"prod", "dev", "optional", or "peer" (can be set multiple times)', - }, - 'include-staged': { - key: 'include-staged', - default: false, - type: Boolean, - description: ` - Allow installing "staged" published packages, as defined by [npm RFC PR - #92](https://github.com/npm/rfcs/pull/92). - - This is experimental, and not implemented by the npm public registry. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'init-author-email': { - key: 'init-author-email', - default: '', - type: String, - description: ` - The value \`npm init\` should use by default for the package author's - email. - `, - defaultDescription: '""', - typeDescription: 'String', - }, - 'init-author-name': { - key: 'init-author-name', - default: '', - type: String, - description: ` - The value \`npm init\` should use by default for the package author's name. - `, - defaultDescription: '""', - typeDescription: 'String', - }, - 'init-author-url': { - key: 'init-author-url', - default: '', - type: [ - '', - url, - ], - description: ` - The value \`npm init\` should use by default for the package author's homepage. - `, - defaultDescription: '""', - typeDescription: '"" or URL', - }, - 'init-license': { - key: 'init-license', - default: 'ISC', - type: String, - description: ` - The value \`npm init\` should use by default for the package license. - `, - defaultDescription: '"ISC"', - typeDescription: 'String', - }, - 'init-module': { - key: 'init-module', - default: '~/.npm-init.js', - type: path, - description: ` - A module that will be loaded by the \`npm init\` command. See the - documentation for the - [init-package-json](https://github.com/npm/init-package-json) module for - more information, or [npm init](/commands/npm-init). - `, - defaultDescription: '"~/.npm-init.js"', - typeDescription: 'Path', - }, - 'init-version': { - key: 'init-version', - default: '1.0.0', - type: semver, - description: ` - The value that \`npm init\` should use by default for the package - version number, if not already set in package.json. - `, - defaultDescription: '"1.0.0"', - typeDescription: 'SemVer string', - }, - 'init.author.email': { - key: 'init.author.email', - default: '', - type: String, - deprecated: ` - Use \`--init-author-email\` instead.`, - description: ` - Alias for \`--init-author-email\` - `, - defaultDescription: '""', - typeDescription: 'String', - }, - 'init.author.name': { - key: 'init.author.name', - default: '', - type: String, - deprecated: ` - Use \`--init-author-name\` instead. - `, - description: ` - Alias for \`--init-author-name\` - `, - defaultDescription: '""', - typeDescription: 'String', - }, - 'init.author.url': { - key: 'init.author.url', - default: '', - type: [ - '', - url, - ], - deprecated: ` - Use \`--init-author-url\` instead. - `, - description: ` - Alias for \`--init-author-url\` - `, - defaultDescription: '""', - typeDescription: '"" or URL', - }, - 'init.license': { - key: 'init.license', - default: 'ISC', - type: String, - deprecated: ` - Use \`--init-license\` instead. - `, - description: ` - Alias for \`--init-license\` - `, - defaultDescription: '"ISC"', - typeDescription: 'String', - }, - 'init.module': { - key: 'init.module', - default: '~/.npm-init.js', - type: path, - deprecated: ` - Use \`--init-module\` instead. - `, - description: ` - Alias for \`--init-module\` - `, - defaultDescription: '"~/.npm-init.js"', - typeDescription: 'Path', - }, - 'init.version': { - key: 'init.version', - default: '1.0.0', - type: semver, - deprecated: ` - Use \`--init-version\` instead. - `, - description: ` - Alias for \`--init-version\` - `, - defaultDescription: '"1.0.0"', - typeDescription: 'SemVer string', - }, - json: { - key: 'json', - default: false, - type: Boolean, - description: ` - Whether or not to output JSON data, rather than the normal output. - - * In \`npm pkg set\` it enables parsing set values with JSON.parse() - before saving them to your \`package.json\`. - - Not supported by all npm commands. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - key: { - key: 'key', - default: null, - type: [ - null, - String, - ], - description: ` - A client key to pass when accessing the registry. Values should be in - PEM format with newlines replaced by the string "\\n". For example: - - \`\`\`ini - key="-----BEGIN PRIVATE KEY-----\\nXXXX\\nXXXX\\n-----END PRIVATE KEY-----" - \`\`\` - - It is _not_ the path to a key file (and there is no "keyfile" option). - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'null', - typeDescription: 'null or String', - }, - 'legacy-bundling': { - key: 'legacy-bundling', - default: false, - type: Boolean, - description: ` - Causes npm to install the package such that versions of npm prior to 1.4, - such as the one included with node 0.8, can install the package. This - eliminates all automatic deduping. If used with \`global-style\` this - option will be preferred. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'legacy-peer-deps': { - key: 'legacy-peer-deps', - default: false, - type: Boolean, - description: ` - Causes npm to completely ignore \`peerDependencies\` when building a - package tree, as in npm versions 3 through 6. - - If a package cannot be installed because of overly strict - \`peerDependencies\` that collide, it provides a way to move forward - resolving the situation. - - This differs from \`--omit=peer\`, in that \`--omit=peer\` will avoid - unpacking \`peerDependencies\` on disk, but will still design a tree such - that \`peerDependencies\` _could_ be unpacked in a correct place. - - Use of \`legacy-peer-deps\` is not recommended, as it will not enforce - the \`peerDependencies\` contract that meta-dependencies may rely on. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - link: { - key: 'link', - default: false, - type: Boolean, - description: ` - Used with \`npm ls\`, limiting output to only those packages that are - linked. - `, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'local-address': { - key: 'local-address', - default: null, - type: [ - null, - '127.0.0.1', - '::1', - 'fe80::1', - 'fe80::aede:48ff:fe00:1122', - 'fe80::18fe:6168:6908:4239', - '2600:1700:87d0:b28f:481:1fd0:2067:5a90', - '2600:1700:87d0:b28f:11be:d3f3:278c:ade9', - 'fd2e:635c:9594:10:109e:699c:6fdc:41b9', - 'fd2e:635c:9594:10:69ce:d360:4ab9:1632', - '192.168.103.122', - 'fe80::715:4a5e:3af5:99e5', - 'fe80::d32a:27b1:2ac:1155', - 'fe80::bbb2:6e76:3877:9f2f', - 'fe80::8e1f:15b0:b70:2d70', - ], - typeDescription: 'IP Address', - description: ` - The IP address of the local interface to use when making connections to - the npm registry. Must be IPv4 in versions of Node prior to 0.12. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'null', - }, - location: { - key: 'location', - default: 'user', - type: ['global', 'user', 'project'], - description: ` - When passed to \`npm config\` this refers to which config file to use. - `, - defaultDescription: ` - "user" unless \`--global\` is passed, which will also set this value to "global" - `, - typeDescription: '"global", "user", or "project"', - }, - loglevel: { - key: 'loglevel', - default: 'notice', - type: [ - 'silent', - 'error', - 'warn', - 'notice', - 'http', - 'timing', - 'info', - 'verbose', - 'silly', - ], - description: ` - What level of logs to report. All logs are written to a debug log, - with the path to that file printed if the execution of a command fails. - - Any logs of a higher level than the setting are shown. The default is - "notice". - - See also the \`foreground-scripts\` config. - `, - defaultDescription: '"notice"', - typeDescription: '"silent", "error", "warn", "notice", "http", "timing", "info", "verbose",' + - ' or "silly"', - }, - 'logs-max': { - key: 'logs-max', - default: 10, - type: Number, - description: ` - The maximum number of log files to store. - `, - defaultDescription: '10', - typeDescription: 'Number', - }, - long: { - key: 'long', - default: false, - type: Boolean, - short: 'l', - description: ` - Show extended information in \`ls\`, \`search\`, and \`help-search\`. - `, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - maxsockets: { - key: 'maxsockets', - default: null, - type: Number, - description: ` - The maximum number of connections to use per origin (protocol/host/port - combination). - `, - flatten (key, obj, flatOptions) { - flatOptions.maxSockets = obj[key] - }, - defaultDescription: 'Infinity', - typeDescription: 'Number', - }, - message: { - key: 'message', - default: '%s', - type: String, - short: 'm', - description: ` - Commit message which is used by \`npm version\` when creating version commit. - - Any "%s" in the message will be replaced with the version number. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: '"%s"', - typeDescription: 'String', - }, - 'node-options': { - key: 'node-options', - default: null, - type: [ - null, - String, - ], - description: ` - Options to pass through to Node.js via the \`NODE_OPTIONS\` environment - variable. This does not impact how npm itself is executed but it does - impact how lifecycle scripts are called. - `, - defaultDescription: 'null', - typeDescription: 'null or String', - }, - 'node-version': { - key: 'node-version', - default: 'v15.3.0', - defaultDescription: 'Node.js `process.version` value', - type: semver, - description: ` - The node version to use when checking a package's \`engines\` setting. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - typeDescription: 'SemVer string', - }, - noproxy: { - key: 'noproxy', - default: '', - defaultDescription: ` - The value of the NO_PROXY environment variable - `, - type: [ - String, - Array, - ], - description: ` - Domain extensions that should bypass any proxies. - - Also accepts a comma-delimited string. - `, - flatten (key, obj, flatOptions) { - flatOptions.noProxy = obj[key].join(',') - }, - typeDescription: 'String (can be set multiple times)', - }, - 'npm-version': { - key: 'npm-version', - default: '7.6.3', - defaultDescription: 'Output of `npm --version`', - type: semver, - description: ` - The npm version to use when checking a package's \`engines\` setting. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - typeDescription: 'SemVer string', - }, - offline: { - key: 'offline', - default: false, - type: Boolean, - description: ` - Force offline mode: no network requests will be done during install. To allow - the CLI to fill in missing cache data, see \`--prefer-offline\`. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - omit: { - key: 'omit', - default: [], - defaultDescription: ` - 'dev' if the \`NODE_ENV\` environment variable is set to 'production', - otherwise empty. - `, - type: [ - Array, - 'dev', - 'optional', - 'peer', - ], - description: ` - Dependency types to omit from the installation tree on disk. - - Note that these dependencies _are_ still resolved and added to the - \`package-lock.json\` or \`npm-shrinkwrap.json\` file. They are just - not physically installed on disk. - - If a package type appears in both the \`--include\` and \`--omit\` - lists, then it will be included. - - If the resulting omit list includes \`'dev'\`, then the \`NODE_ENV\` - environment variable will be set to \`'production'\` for all lifecycle - scripts. - `, - flatten (key, obj, flatOptions) { - const include = obj.include || [] - const omit = flatOptions.omit || [] - flatOptions.omit = omit.concat(obj[key]) - .filter(type => type && !include.includes(type)) - }, - typeDescription: '"dev", "optional", or "peer" (can be set multiple times)', - }, - only: { - key: 'only', - default: null, - type: [ - null, - 'prod', - 'production', - ], - deprecated: ` - Use \`--omit=dev\` to omit dev dependencies from the install. - `, - description: ` - When set to \`prod\` or \`production\`, this is an alias for - \`--omit=dev\`. - `, - flatten (key, obj, flatOptions) { - const value = obj[key] - if (!/^prod(uction)?$/.test(value)) { - return - } - - obj.omit = obj.omit || [] - obj.omit.push('dev') - definitions.omit.flatten('omit', obj, flatOptions) - }, - defaultDescription: 'null', - typeDescription: 'null, "prod", or "production"', - }, - optional: { - key: 'optional', - default: null, - type: [ - null, - Boolean, - ], - deprecated: ` - Use \`--omit=optional\` to exclude optional dependencies, or - \`--include=optional\` to include them. - - Default value does install optional deps unless otherwise omitted. - `, - description: ` - Alias for --include=optional or --omit=optional - `, - flatten (key, obj, flatOptions) { - const value = obj[key] - if (value === null) { - return - } else if (value === true) { - obj.include = obj.include || [] - obj.include.push('optional') - } else { - obj.omit = obj.omit || [] - obj.omit.push('optional') - } - definitions.omit.flatten('omit', obj, flatOptions) - }, - defaultDescription: 'null', - typeDescription: 'null or Boolean', - }, - otp: { - key: 'otp', - default: null, - type: [ - null, - String, - ], - description: ` - This is a one-time password from a two-factor authenticator. It's needed - when publishing or changing package permissions with \`npm access\`. - - If not set, and a registry response fails with a challenge for a one-time - password, npm will prompt on the command line for one. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'null', - typeDescription: 'null or String', - }, - package: { - key: 'package', - default: [], - type: [ - String, - Array, - ], - description: ` - The package to install for [\`npm exec\`](/commands/npm-exec) - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: '', - typeDescription: 'String (can be set multiple times)', - }, - 'package-lock': { - key: 'package-lock', - default: true, - type: Boolean, - description: ` - If set to false, then ignore \`package-lock.json\` files when installing. - This will also prevent _writing_ \`package-lock.json\` if \`save\` is - true. - - When package package-locks are disabled, automatic pruning of extraneous - modules will also be disabled. To remove extraneous modules with - package-locks disabled use \`npm prune\`. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'true', - typeDescription: 'Boolean', - }, - 'package-lock-only': { - key: 'package-lock-only', - default: false, - type: Boolean, - description: ` - If set to true, the current operation will only use the \`package-lock.json\`, - ignoring \`node_modules\`. - - For \`update\` this means only the \`package-lock.json\` will be updated, - instead of checking \`node_modules\` and downloading dependencies. - - For \`list\` this means the output will be based on the tree described by the - \`package-lock.json\`, rather than the contents of \`node_modules\`. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - parseable: { - key: 'parseable', - default: false, - type: Boolean, - short: 'p', - description: ` - Output parseable results from commands that write to standard output. For - \`npm search\`, this will be tab-separated table format. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'prefer-offline': { - key: 'prefer-offline', - default: false, - type: Boolean, - description: ` - If true, staleness checks for cached data will be bypassed, but missing - data will be requested from the server. To force full offline mode, use - \`--offline\`. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'prefer-online': { - key: 'prefer-online', - default: false, - type: Boolean, - description: ` - If true, staleness checks for cached data will be forced, making the CLI - look for updates immediately even for fresh package data. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - prefix: { - key: 'prefix', - type: path, - short: 'C', - default: '', - defaultDescription: ` - In global mode, the folder where the node executable is installed. In - local mode, the nearest parent folder containing either a package.json - file or a node_modules folder. - `, - description: ` - The location to install global items. If set on the command line, then - it forces non-global commands to run in the specified folder. - `, - typeDescription: 'Path', - }, - preid: { - key: 'preid', - default: '', - type: String, - description: ` - The "prerelease identifier" to use as a prefix for the "prerelease" part - of a semver. Like the \`rc\` in \`1.2.0-rc.8\`. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: '""', - typeDescription: 'String', - }, - production: { - key: 'production', - default: false, - type: Boolean, - deprecated: 'Use `--omit=dev` instead.', - description: 'Alias for `--omit=dev`', - flatten (key, obj, flatOptions) { - const value = obj[key] - if (!value) { - return - } - - obj.omit = obj.omit || [] - obj.omit.push('dev') - definitions.omit.flatten('omit', obj, flatOptions) - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - progress: { - key: 'progress', - default: true, - defaultDescription: '\n `true` unless running in a known CI system\n ', - type: Boolean, - description: ` - When set to \`true\`, npm will display a progress bar during time - intensive operations, if \`process.stderr\` is a TTY. - - Set to \`false\` to suppress the progress bar. - `, - typeDescription: 'Boolean', - }, - proxy: { - key: 'proxy', - default: null, - type: [ - null, - false, - url, - ], - description: ` - A proxy to use for outgoing http requests. If the \`HTTP_PROXY\` or - \`http_proxy\` environment variables are set, proxy settings will be - honored by the underlying \`request\` library. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'null', - typeDescription: 'null, false, or URL', - }, - 'read-only': { - key: 'read-only', - default: false, - type: Boolean, - description: ` - This is used to mark a token as unable to publish when configuring - limited access tokens with the \`npm token create\` command. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'rebuild-bundle': { - key: 'rebuild-bundle', - default: true, - type: Boolean, - description: ` - Rebuild bundled dependencies after installation. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'true', - typeDescription: 'Boolean', - }, - registry: { - key: 'registry', - default: 'https://registry.npmjs.org/', - type: [null, url], - description: ` - The base URL of the npm registry. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: '"https://registry.npmjs.org/"', - typeDescription: 'URL', - }, - save: { - key: 'save', - default: true, - type: Boolean, - short: 'S', - description: ` - Save installed packages to a \`package.json\` file as dependencies. - - When used with the \`npm rm\` command, removes the dependency from - \`package.json\`. - - Will also prevent writing to \`package-lock.json\` if set to \`false\`. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'true', - typeDescription: 'Boolean', - }, - 'save-bundle': { - key: 'save-bundle', - default: false, - type: Boolean, - short: 'B', - description: ` - If a package would be saved at install time by the use of \`--save\`, - \`--save-dev\`, or \`--save-optional\`, then also put it in the - \`bundleDependencies\` list. - - Ignore if \`--save-peer\` is set, since peerDependencies cannot be bundled. - `, - flatten (key, obj, flatOptions) { - // XXX update arborist to just ignore it if resulting saveType is peer - // otherwise this won't have the expected effect: - // - // npm config set save-peer true - // npm i foo --save-bundle --save-prod <-- should bundle - flatOptions.saveBundle = obj['save-bundle'] && !obj['save-peer'] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'save-dev': { - key: 'save-dev', - default: false, - type: Boolean, - short: 'D', - description: ` - Save installed packages to a package.json file as \`devDependencies\`. - `, - flatten (key, obj, flatOptions) { - if (!obj[key]) { - if (flatOptions.saveType === 'dev') { - delete flatOptions.saveType - } - return - } - - flatOptions.saveType = 'dev' - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'save-exact': { - key: 'save-exact', - default: false, - type: Boolean, - short: 'E', - description: ` - Dependencies saved to package.json will be configured with an exact - version rather than using npm's default semver range operator. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'save-optional': { - key: 'save-optional', - default: false, - type: Boolean, - short: 'O', - description: ` - Save installed packages to a package.json file as - \`optionalDependencies\`. - `, - flatten (key, obj, flatOptions) { - if (!obj[key]) { - if (flatOptions.saveType === 'optional') { - delete flatOptions.saveType - } else if (flatOptions.saveType === 'peerOptional') { - flatOptions.saveType = 'peer' - } - return - } - - if (flatOptions.saveType === 'peerOptional') { - return - } - - if (flatOptions.saveType === 'peer') { - flatOptions.saveType = 'peerOptional' - } else { - flatOptions.saveType = 'optional' - } - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'save-peer': { - key: 'save-peer', - default: false, - type: Boolean, - description: ` - Save installed packages to a package.json file as \`peerDependencies\` - `, - flatten (key, obj, flatOptions) { - if (!obj[key]) { - if (flatOptions.saveType === 'peer') { - delete flatOptions.saveType - } else if (flatOptions.saveType === 'peerOptional') { - flatOptions.saveType = 'optional' - } - return - } - - if (flatOptions.saveType === 'peerOptional') { - return - } - - if (flatOptions.saveType === 'optional') { - flatOptions.saveType = 'peerOptional' - } else { - flatOptions.saveType = 'peer' - } - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'save-prefix': { - key: 'save-prefix', - default: '^', - type: String, - description: ` - Configure how versions of packages installed to a package.json file via - \`--save\` or \`--save-dev\` get prefixed. - - For example if a package has version \`1.2.3\`, by default its version is - set to \`^1.2.3\` which allows minor upgrades for that package, but after - \`npm config set save-prefix='~'\` it would be set to \`~1.2.3\` which - only allows patch upgrades. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: '"^"', - typeDescription: 'String', - }, - 'save-prod': { - key: 'save-prod', - default: false, - type: Boolean, - short: 'P', - description: ` - Save installed packages into \`dependencies\` specifically. This is - useful if a package already exists in \`devDependencies\` or - \`optionalDependencies\`, but you want to move it to be a non-optional - production dependency. - - This is the default behavior if \`--save\` is true, and neither - \`--save-dev\` or \`--save-optional\` are true. - `, - flatten (key, obj, flatOptions) { - if (!obj[key]) { - if (flatOptions.saveType === 'prod') { - delete flatOptions.saveType - } - return - } - - flatOptions.saveType = 'prod' - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - scope: { - key: 'scope', - default: '', - defaultDescription: ` - the scope of the current project, if any, or "" - `, - type: String, - description: ` - Associate an operation with a scope for a scoped registry. - - Useful when logging in to or out of a private registry: - - \`\`\` - # log in, linking the scope to the custom registry - npm login --scope=@mycorp --registry=https://registry.mycorp.com - - # log out, removing the link and the auth token - npm logout --scope=@mycorp - \`\`\` - - This will cause \`@mycorp\` to be mapped to the registry for future - installation of packages specified according to the pattern - \`@mycorp/package\`. - - This will also cause \`npm init\` to create a scoped package. - - \`\`\` - # accept all defaults, and create a package named "@foo/whatever", - # instead of just named "whatever" - npm init --scope=@foo --yes - \`\`\` - `, - flatten (key, obj, flatOptions) { - const value = obj[key] - flatOptions.projectScope = value && !/^@/.test(value) ? `@${value}` : value - }, - typeDescription: 'String', - }, - 'script-shell': { - key: 'script-shell', - default: null, - defaultDescription: ` - '/bin/sh' on POSIX systems, 'cmd.exe' on Windows - `, - type: [ - null, - String, - ], - description: ` - The shell to use for scripts run with the \`npm exec\`, - \`npm run\` and \`npm init \` commands. - `, - flatten (key, obj, flatOptions) { - flatOptions.scriptShell = obj[key] || undefined - }, - typeDescription: 'null or String', - }, - searchexclude: { - key: 'searchexclude', - default: '', - type: String, - description: ` - Space-separated options that limit the results from search. - `, - flatten (key, obj, flatOptions) { - flatOptions.search = flatOptions.search || { limit: 20 } - flatOptions.search.exclude = obj[key] - }, - defaultDescription: '""', - typeDescription: 'String', - }, - searchlimit: { - key: 'searchlimit', - default: 20, - type: Number, - description: ` - Number of items to limit search results to. Will not apply at all to - legacy searches. - `, - flatten (key, obj, flatOptions) { - flatOptions.search = flatOptions.search || {} - flatOptions.search.limit = obj[key] - }, - defaultDescription: '20', - typeDescription: 'Number', - }, - searchopts: { - key: 'searchopts', - default: '', - type: String, - description: ` - Space-separated options that are always passed to search. - `, - flatten (key, obj, flatOptions) { - flatOptions.search = flatOptions.search || { limit: 20 } - flatOptions.search.opts = querystring.parse(obj[key]) - }, - defaultDescription: '""', - typeDescription: 'String', - }, - searchstaleness: { - key: 'searchstaleness', - default: 900, - type: Number, - description: ` - The age of the cache, in seconds, before another registry request is made - if using legacy search endpoint. - `, - flatten (key, obj, flatOptions) { - flatOptions.search = flatOptions.search || { limit: 20 } - flatOptions.search.staleness = obj[key] - }, - defaultDescription: '900', - typeDescription: 'Number', - }, - shell: { - key: 'shell', - default: '/usr/local/bin/bash', - defaultDescription: ` - SHELL environment variable, or "bash" on Posix, or "cmd.exe" on Windows - `, - type: String, - description: ` - The shell to run for the \`npm explore\` command. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - typeDescription: 'String', - }, - shrinkwrap: { - key: 'shrinkwrap', - default: true, - type: Boolean, - deprecated: ` - Use the --package-lock setting instead. - `, - description: ` - Alias for --package-lock - `, - flatten (key, obj, flatOptions) { - obj['package-lock'] = obj.shrinkwrap - definitions['package-lock'].flatten('package-lock', obj, flatOptions) - }, - defaultDescription: 'true', - typeDescription: 'Boolean', - }, - 'sign-git-commit': { - key: 'sign-git-commit', - default: false, - type: Boolean, - description: ` - If set to true, then the \`npm version\` command will commit the new - package version using \`-S\` to add a signature. - - Note that git requires you to have set up GPG keys in your git configs - for this to work properly. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'sign-git-tag': { - key: 'sign-git-tag', - default: false, - type: Boolean, - description: ` - If set to true, then the \`npm version\` command will tag the version - using \`-s\` to add a signature. - - Note that git requires you to have set up GPG keys in your git configs - for this to work properly. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'sso-poll-frequency': { - key: 'sso-poll-frequency', - default: 500, - type: Number, - deprecated: ` - The --auth-type method of SSO/SAML/OAuth will be removed in a future - version of npm in favor of web-based login. - `, - description: ` - When used with SSO-enabled \`auth-type\`s, configures how regularly the - registry should be polled while the user is completing authentication. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: '500', - typeDescription: 'Number', - }, - 'sso-type': { - key: 'sso-type', - default: 'oauth', - type: [ - null, - 'oauth', - 'saml', - ], - deprecated: ` - The --auth-type method of SSO/SAML/OAuth will be removed in a future - version of npm in favor of web-based login. - `, - description: ` - If \`--auth-type=sso\`, the type of SSO type to use. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: '"oauth"', - typeDescription: 'null, "oauth", or "saml"', - }, - 'strict-peer-deps': { - key: 'strict-peer-deps', - default: false, - type: Boolean, - description: ` - If set to \`true\`, and \`--legacy-peer-deps\` is not set, then _any_ - conflicting \`peerDependencies\` will be treated as an install failure, - even if npm could reasonably guess the appropriate resolution based on - non-peer dependency relationships. - - By default, conflicting \`peerDependencies\` deep in the dependency graph - will be resolved using the nearest non-peer dependency specification, - even if doing so will result in some packages receiving a peer dependency - outside the range set in their package's \`peerDependencies\` object. - - When such and override is performed, a warning is printed, explaining the - conflict and the packages involved. If \`--strict-peer-deps\` is set, - then this warning is treated as a failure. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'strict-ssl': { - key: 'strict-ssl', - default: true, - type: Boolean, - description: ` - Whether or not to do SSL key validation when making requests to the - registry via https. - - See also the \`ca\` config. - `, - flatten (key, obj, flatOptions) { - flatOptions.strictSSL = obj[key] - }, - defaultDescription: 'true', - typeDescription: 'Boolean', - }, - tag: { - key: 'tag', - default: 'latest', - type: String, - description: ` - If you ask npm to install a package and don't tell it a specific version, - then it will install the specified tag. - - Also the tag that is added to the package@version specified by the \`npm - tag\` command, if no explicit tag is given. - - When used by the \`npm diff\` command, this is the tag used to fetch the - tarball that will be compared with the local files by default. - `, - flatten (key, obj, flatOptions) { - flatOptions.defaultTag = obj[key] - }, - defaultDescription: '"latest"', - typeDescription: 'String', - }, - 'tag-version-prefix': { - key: 'tag-version-prefix', - default: 'v', - type: String, - description: ` - If set, alters the prefix used when tagging a new version when performing - a version increment using \`npm-version\`. To remove the prefix - altogether, set it to the empty string: \`""\`. - - Because other tools may rely on the convention that npm version tags look - like \`v1.0.0\`, _only use this property if it is absolutely necessary_. - In particular, use care when overriding this setting for public packages. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: '"v"', - typeDescription: 'String', - }, - timing: { - key: 'timing', - default: false, - type: Boolean, - description: ` - If true, writes an \`npm-debug\` log to \`_logs\` and timing information - to \`_timing.json\`, both in your cache, even if the command completes - successfully. \`_timing.json\` is a newline delimited list of JSON - objects. - - You can quickly view it with this [json](https://npm.im/json) command - line: \`npm exec -- json -g < ~/.npm/_timing.json\`. - `, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - tmp: { - key: 'tmp', - default: '/var/folders/zc/5n20yjzn7mn7cz_qckj3b3440000gn/T', - defaultDescription: ` - The value returned by the Node.js \`os.tmpdir()\` method - - `, - type: path, - deprecated: ` - This setting is no longer used. npm stores temporary files in a special - location in the cache, and they are managed by - [\`cacache\`](http://npm.im/cacache). - `, - description: ` - Historically, the location where temporary files were stored. No longer - relevant. - `, - typeDescription: 'Path', - }, - umask: { - key: 'umask', - default: 0, - type: Umask, - description: ` - The "umask" value to use when setting the file creation mode on files and - folders. - - Folders and executables are given a mode which is \`0o777\` masked - against this value. Other files are given a mode which is \`0o666\` - masked against this value. - - Note that the underlying system will _also_ apply its own umask value to - files and folders that are created, and npm does not circumvent this, but - rather adds the \`--umask\` config to it. - - Thus, the effective default umask value on most POSIX systems is 0o22, - meaning that folders and executables are created with a mode of 0o755 and - other files are created with a mode of 0o644. - `, - flatten: (key, obj, flatOptions) => { - const camel = key.replace(/-([a-z])/g, (_0, _1) => _1.toUpperCase()) - flatOptions[camel] = obj[key] - }, - defaultDescription: '0', - typeDescription: 'Octal numeric string in range 0000..0777 (0..511)', - }, - unicode: { - key: 'unicode', - default: true, - defaultDescription: ` - false on windows, true on mac/unix systems with a unicode locale, as - defined by the \`LC_ALL\`, \`LC_CTYPE\`, or \`LANG\` environment variables. - `, - type: Boolean, - description: ` - When set to true, npm uses unicode characters in the tree output. When - false, it uses ascii characters instead of unicode glyphs. - `, - typeDescription: 'Boolean', - }, - 'update-notifier': { - key: 'update-notifier', - default: true, - type: Boolean, - description: ` - Set to false to suppress the update notification when using an older - version of npm than the latest. - `, - defaultDescription: 'true', - typeDescription: 'Boolean', - }, - usage: { - key: 'usage', - default: false, - type: Boolean, - short: [ - '?', - 'H', - 'h', - ], - description: ` - Show short usage output about the command specified. - `, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - 'user-agent': { - key: 'user-agent', - default: 'npm/{npm-version} node/{node-version} {platform} {arch} {ci}', - type: String, - description: ` - Sets the User-Agent request header. The following fields are replaced - with their actual counterparts: - - * \`{npm-version}\` - The npm version in use - * \`{node-version}\` - The Node.js version in use - * \`{platform}\` - The value of \`process.platform\` - * \`{arch}\` - The value of \`process.arch\` - * \`{workspaces}\` - Set to \`true\` if the \`workspaces\` or \`workspace\` - options are set. - * \`{ci}\` - The value of the \`ci-name\` config, if set, prefixed with - \`ci/\`, or an empty string if \`ci-name\` is empty. - `, - flatten (key, obj, flatOptions) { - const value = obj[key] - const ciName = obj['ci-name'] - flatOptions.userAgent = - value.replace(/\{node-version\}/gi, obj['node-version']) - .replace(/\{npm-version\}/gi, obj['npm-version']) - .replace(/\{platform\}/gi, process.platform) - .replace(/\{arch\}/gi, process.arch) - .replace(/\{ci\}/gi, ciName ? `ci/${ciName}` : '') - .trim() - }, - defaultDescription: '"npm/{npm-version} node/{node-version} {platform} {arch} {ci}"', - typeDescription: 'String', - }, - userconfig: { - key: 'userconfig', - default: '~/.npmrc', - type: path, - description: ` - The location of user-level configuration settings. - - This may be overridden by the \`npm_config_userconfig\` environment - variable or the \`--userconfig\` command line option, but may _not_ - be overridden by settings in the \`globalconfig\` file. - `, - defaultDescription: '"~/.npmrc"', - typeDescription: 'Path', - }, - version: { - key: 'version', - default: false, - type: Boolean, - short: 'v', - description: ` - If true, output the npm version and exit successfully. - - Only relevant when specified explicitly on the command line. - `, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - versions: { - key: 'versions', - default: false, - type: Boolean, - description: ` - If true, output the npm version as well as node's \`process.versions\` - map and the version in the current working directory's \`package.json\` - file if one exists, and exit successfully. - - Only relevant when specified explicitly on the command line. - `, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, - viewer: { - key: 'viewer', - default: 'man', - defaultDescription: '\n "man" on Posix, "browser" on Windows\n ', - type: String, - description: ` - The program to use to view help content. - - Set to \`"browser"\` to view html help content in the default web browser. - `, - typeDescription: 'String', - }, - workspace: { - key: 'workspace', - default: [], - type: [String, Array], - short: 'w', - envExport: false, - description: ` - Enable running a command in the context of the configured workspaces of the - current project while filtering by running only the workspaces defined by - this configuration option. - - Valid values for the \`workspace\` config are either: - - * Workspace names - * Path to a workspace directory - * Path to a parent workspace directory (will result in selecting all - workspaces within that folder) - - When set for the \`npm init\` command, this may be set to the folder of - a workspace which does not yet exist, to create the folder and set it - up as a brand new workspace within the project. - `, - defaultDescription: '', - typeDescription: 'String (can be set multiple times)', - flatten: (key, obj, flatOptions) => { - definitions['user-agent'].flatten('user-agent', obj, flatOptions) - }, - }, - yes: { - key: 'yes', - default: false, - type: Boolean, - short: 'y', - description: ` - Automatically answer "yes" to any prompts that npm might print on - the command line. - `, - defaultDescription: 'false', - typeDescription: 'Boolean', - }, -} diff --git a/workspaces/config/test/fixtures/flatten.js b/workspaces/config/test/fixtures/flatten.js deleted file mode 100644 index 588d05bf0d77d..0000000000000 --- a/workspaces/config/test/fixtures/flatten.js +++ /dev/null @@ -1,33 +0,0 @@ -// use the defined flattening function, and copy over any scoped -// registries and registry-specific "nerfdart" configs verbatim -// -// TODO: make these getters so that we only have to make dirty -// the thing that changed, and then flatten the fields that -// could have changed when a config.set is called. -// -// TODO: move nerfdart auth stuff into a nested object that -// is only passed along to paths that end up calling npm-registry-fetch. -const definitions = require('./definitions.js') -const flatten = (obj, flat = {}) => { - for (const [key, val] of Object.entries(obj)) { - const def = definitions[key] - if (def && def.flatten) { - def.flatten(key, obj, flat) - } else if (/@.*:registry$/i.test(key) || /^\/\//.test(key)) { - flat[key] = val - } - } - - // XXX make this the bin/npm-cli.js file explicitly instead - // otherwise using npm programmatically is a bit of a pain. - flat.npmBin = require.main ? require.main.filename - : /* istanbul ignore next - not configurable property */ undefined - flat.nodeBin = process.env.NODE || process.execPath - - // XXX should this be sha512? is it even relevant? - flat.hashAlgorithm = 'sha1' - - return flat -} - -module.exports = flatten diff --git a/workspaces/config/test/fixtures/shorthands.js b/workspaces/config/test/fixtures/shorthands.js deleted file mode 100644 index 5c460c6617175..0000000000000 --- a/workspaces/config/test/fixtures/shorthands.js +++ /dev/null @@ -1,41 +0,0 @@ -module.exports = { - 'enjoy-by': ['--before'], - a: ['--all'], - c: ['--call'], - s: ['--loglevel', 'silent'], - d: ['--loglevel', 'info'], - dd: ['--loglevel', 'verbose'], - ddd: ['--loglevel', 'silly'], - noreg: ['--no-registry'], - N: ['--no-registry'], - reg: ['--registry'], - 'no-reg': ['--no-registry'], - silent: ['--loglevel', 'silent'], - verbose: ['--loglevel', 'verbose'], - quiet: ['--loglevel', 'warn'], - q: ['--loglevel', 'warn'], - h: ['--usage'], - H: ['--usage'], - '?': ['--usage'], - help: ['--usage'], - v: ['--version'], - f: ['--force'], - desc: ['--description'], - 'no-desc': ['--no-description'], - local: ['--no-global'], - l: ['--long'], - m: ['--message'], - p: ['--parseable'], - porcelain: ['--parseable'], - readonly: ['--read-only'], - g: ['--global'], - S: ['--save'], - D: ['--save-dev'], - E: ['--save-exact'], - O: ['--save-optional'], - P: ['--save-prod'], - y: ['--yes'], - n: ['--no-yes'], - B: ['--save-bundle'], - C: ['--prefix'], -} diff --git a/workspaces/config/test/fixtures/types.js b/workspaces/config/test/fixtures/types.js deleted file mode 100644 index 0f8cedfd6e6ac..0000000000000 --- a/workspaces/config/test/fixtures/types.js +++ /dev/null @@ -1,151 +0,0 @@ -const { - String: { type: String }, - Boolean: { type: Boolean }, - url: { type: url }, - Number: { type: Number }, - path: { type: path }, - Date: { type: Date }, - semver: { type: semver }, - Umask: { type: Umask }, -} = require('../../lib/type-defs.js') - -const { networkInterfaces } = require('os') -const getLocalAddresses = () => { - try { - return Object.values(networkInterfaces()).map( - int => int.map(({ address }) => address) - ).reduce((set, addrs) => set.concat(addrs), [undefined]) - } catch (e) { - return [undefined] - } -} - -module.exports = { - access: [null, 'restricted', 'public'], - all: Boolean, - 'allow-same-version': Boolean, - 'always-auth': Boolean, - also: [null, 'dev', 'development'], - audit: Boolean, - 'audit-level': ['low', 'moderate', 'high', 'critical', 'none', null], - 'auth-type': ['legacy', 'sso', 'saml', 'oauth'], - before: [null, Date], - 'bin-links': Boolean, - browser: [null, Boolean, String], - ca: [null, String, Array], - cafile: path, - cache: path, - 'cache-lock-stale': Number, - 'cache-lock-retries': Number, - 'cache-lock-wait': Number, - 'cache-max': Number, - 'cache-min': Number, - cert: [null, String], - cidr: [null, String, Array], - color: ['always', Boolean], - call: String, - depth: Number, - description: Boolean, - dev: Boolean, - 'dry-run': Boolean, - editor: String, - 'engine-strict': Boolean, - force: Boolean, - fund: Boolean, - 'format-package-lock': Boolean, - 'fetch-retries': Number, - 'fetch-retry-factor': Number, - 'fetch-retry-mintimeout': Number, - 'fetch-retry-maxtimeout': Number, - git: String, - 'git-tag-version': Boolean, - 'commit-hooks': Boolean, - global: Boolean, - globalconfig: path, - 'global-style': Boolean, - 'https-proxy': [null, url], - 'user-agent': String, - heading: String, - 'if-present': Boolean, - include: [Array, 'prod', 'dev', 'optional', 'peer'], - 'include-staged': Boolean, - 'ignore-prepublish': Boolean, - 'ignore-scripts': Boolean, - 'init-module': path, - 'init-author-name': String, - 'init-author-email': String, - 'init-author-url': ['', url], - 'init-license': String, - 'init-version': semver, - json: Boolean, - key: [null, String], - 'legacy-bundling': Boolean, - 'legacy-peer-deps': Boolean, - link: Boolean, - 'local-address': getLocalAddresses(), - loglevel: ['silent', 'error', 'warn', 'notice', 'http', 'timing', 'info', 'verbose', 'silly'], - 'logs-max': Number, - long: Boolean, - 'multiple-numbers': [Array, Number], - maxsockets: Number, - message: String, - 'metrics-registry': [null, String], - 'node-options': [null, String], - 'node-version': [null, semver], - noproxy: [null, String, Array], - offline: Boolean, - omit: [Array, 'dev', 'optional', 'peer'], - only: [null, 'dev', 'development', 'prod', 'production'], - optional: Boolean, - otp: [null, String], - package: [String, Array], - 'package-lock': Boolean, - 'package-lock-only': Boolean, - parseable: Boolean, - 'prefer-offline': Boolean, - 'prefer-online': Boolean, - prefix: path, - preid: String, - production: Boolean, - progress: Boolean, - proxy: [null, false, url], // allow proxy to be disabled explicitly - 'read-only': Boolean, - 'rebuild-bundle': Boolean, - registry: [null, url], - rollback: Boolean, - save: Boolean, - 'save-bundle': Boolean, - 'save-dev': Boolean, - 'save-exact': Boolean, - 'save-optional': Boolean, - 'save-prefix': String, - 'save-prod': Boolean, - scope: String, - 'script-shell': [null, String], - 'scripts-prepend-node-path': [Boolean, 'auto', 'warn-only'], - searchopts: String, - searchexclude: [null, String], - searchlimit: Number, - searchstaleness: Number, - 'send-metrics': Boolean, - shell: String, - shrinkwrap: Boolean, - 'sign-git-commit': Boolean, - 'sign-git-tag': Boolean, - 'sso-poll-frequency': Number, - 'sso-type': [null, 'oauth', 'saml'], - 'strict-ssl': Boolean, - tag: String, - timing: Boolean, - tmp: path, - unicode: Boolean, - 'update-notifier': Boolean, - usage: Boolean, - userconfig: path, - umask: Umask, - version: Boolean, - 'tag-version-prefix': String, - versions: Boolean, - viewer: String, - _exit: Boolean, -} diff --git a/workspaces/config/test/parse-field.js b/workspaces/config/test/parse-field.js deleted file mode 100644 index 1c4193b73c986..0000000000000 --- a/workspaces/config/test/parse-field.js +++ /dev/null @@ -1,36 +0,0 @@ -const parseField = require('../lib/parse-field.js') -const t = require('tap') -const { resolve } = require('path') - -t.strictSame(parseField({ a: 1 }, 'a'), { a: 1 }) - -const opts = { - platform: 'posix', - types: require('./fixtures/types.js'), - home: '/home/user', - env: { foo: 'bar' }, -} - -t.equal(parseField('', 'global', opts), true, 'boolean flag') -t.equal(parseField('true', 'global', opts), true, 'boolean flag "true"') -t.equal(parseField('false', 'global', opts), false, 'boolean flag "false"') -t.equal(parseField('null', 'access', opts), null, '"null" is null') -t.equal(parseField('undefined', 'access', opts), undefined, '"undefined" is undefined') -t.equal(parseField('blerg', 'access', opts), 'blerg', '"blerg" just is a string') -t.equal(parseField('blerg', 'message', opts), 'blerg', '"blerg" just is a string') -t.strictSame(parseField([], 'global', opts), [], 'array passed to non-list type') -t.strictSame(parseField([' dev '], 'omit', opts), ['dev'], 'array to list type') -t.strictSame(parseField('dev\n\noptional', 'omit', opts), ['dev', 'optional'], - 'double-LF delimited list, like we support in env vals') -t.equal(parseField('~/foo', 'userconfig', opts), resolve('/home/user/foo'), - 'path supports ~/') -t.equal(parseField('~\\foo', 'userconfig', { ...opts, platform: 'win32' }), - resolve('/home/user/foo'), 'path supports ~\\ on windows') -t.equal(parseField('foo', 'userconfig', opts), resolve('foo'), - 'path gets resolved') - -t.equal(parseField('1234', 'maxsockets', opts), 1234, 'number is parsed') - -t.equal(parseField('0888', 'umask', opts), '0888', - 'invalid umask is not parsed (will warn later)') -t.equal(parseField('0777', 'umask', opts), 0o777, 'valid umask is parsed') diff --git a/workspaces/config/test/type-defs.js b/workspaces/config/test/type-defs.js deleted file mode 100644 index 2ce0ac91dc4cd..0000000000000 --- a/workspaces/config/test/type-defs.js +++ /dev/null @@ -1,22 +0,0 @@ -const typeDefs = require('../lib/type-defs.js') -const t = require('tap') -const { - semver: { - validate: validateSemver, - }, - path: { - validate: validatePath, - }, -} = typeDefs -const { resolve } = require('path') - -const d = { semver: 'foobar', somePath: true } -t.equal(validateSemver(d, 'semver', 'foobar'), false) -t.equal(validateSemver(d, 'semver', 'v1.2.3'), undefined) -t.equal(d.semver, '1.2.3') -t.equal(validatePath(d, 'somePath', true), false) -t.equal(validatePath(d, 'somePath', false), false) -t.equal(validatePath(d, 'somePath', null), false) -t.equal(validatePath(d, 'somePath', 1234), false) -t.equal(validatePath(d, 'somePath', 'false'), true) -t.equal(d.somePath, resolve('false')) diff --git a/workspaces/config/test/type-description.js b/workspaces/config/test/type-description.js deleted file mode 100644 index d487c118940ec..0000000000000 --- a/workspaces/config/test/type-description.js +++ /dev/null @@ -1,14 +0,0 @@ -const t = require('tap') -const typeDescription = require('../lib/type-description.js') -const types = require('./fixtures/types.js') -const descriptions = {} -for (const [name, type] of Object.entries(types)) { - const desc = typeDescription(type) - if (name === 'local-address') { - t.strictSame(desc.sort(), type.filter(t => t !== undefined).sort()) - } else { - descriptions[name] = desc - } -} - -t.matchSnapshot(descriptions) From 81549295454aeb93eb72e74a1f195278a024a8c3 Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Mon, 9 Jan 2023 11:43:48 -0700 Subject: [PATCH 4/9] more derived --- .eslintrc.local.js | 30 ++ .eslintrc.local.json | 5 - index.js | 2 +- lib/base-command.js | 125 ++++---- lib/cli.js | 157 ---------- lib/cli/index.js | 71 +++++ lib/cli/validate-engines.js | 62 ++++ lib/commands/get.js | 9 +- lib/commands/help.js | 2 +- lib/commands/run-script.js | 2 +- lib/commands/set.js | 12 +- lib/commands/version.js | 7 +- lib/npm.js | 289 +++++++----------- lib/utils/cmd-list.js | 23 +- lib/utils/did-you-mean.js | 59 ++-- lib/utils/exit-handler.js | 2 +- lib/utils/log-file.js | 15 +- lib/utils/npm-usage.js | 8 +- package-lock.json | 1 + test/fixtures/mock-npm.js | 25 +- workspaces/config/lib/config-data.js | 47 +-- workspaces/config/lib/config-locations.js | 18 +- .../lib/{config => definitions}/definition.js | 19 +- .../lib/{config => definitions}/index.js | 23 +- workspaces/config/lib/env-replace.js | 14 - workspaces/config/lib/index.js | 254 ++++++++------- .../config/lib}/replace-info.js | 0 .../lib/{set-envs.js => set-globals.js} | 36 ++- workspaces/config/package.json | 1 + 29 files changed, 662 insertions(+), 656 deletions(-) create mode 100644 .eslintrc.local.js delete mode 100644 .eslintrc.local.json delete mode 100644 lib/cli.js create mode 100644 lib/cli/index.js create mode 100644 lib/cli/validate-engines.js rename workspaces/config/lib/{config => definitions}/definition.js (95%) rename workspaces/config/lib/{config => definitions}/index.js (98%) delete mode 100644 workspaces/config/lib/env-replace.js rename {lib/utils => workspaces/config/lib}/replace-info.js (100%) rename workspaces/config/lib/{set-envs.js => set-globals.js} (68%) diff --git a/.eslintrc.local.js b/.eslintrc.local.js new file mode 100644 index 0000000000000..6aaeddbf38224 --- /dev/null +++ b/.eslintrc.local.js @@ -0,0 +1,30 @@ +const { resolve, relative } = require('path') + +// Create an override to lockdown a file to es6 syntax only +// and only allow it to require an allowlist of files +const res = (p) => resolve(__dirname, p) +const rel = (p) => relative(__dirname, res(p)) +const braces = (a) => a.length > 1 ? `{${a.map(rel).join(',')}}` : a[0] + +const es6Files = (e) => Object.entries(e).map(([file, allow]) => ({ + files: `./${rel(file)}`, + parserOptions: { + ecmaVersion: 6, + }, + rules: { + 'node/no-restricted-require': ['error', [{ + name: ['/**', `!${__dirname}/${braces(allow)}`], + message: `This file can only require: ${allow.join(',')}`, + }]], + }, +})) + +module.exports = { + rules: { + 'no-console': 'error', + }, + overrides: es6Files({ + 'index.js': ['lib/cli/validate-engines.js'], + 'lib/cli/validate-engines.js': ['package.json', 'lib/cli/index.js'], + }), +} diff --git a/.eslintrc.local.json b/.eslintrc.local.json deleted file mode 100644 index 2ff50f91ec326..0000000000000 --- a/.eslintrc.local.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "no-console": "error" - } -} diff --git a/index.js b/index.js index 5447643e04b66..9d6ac2b518bcf 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ if (require.main === module) { - require('./lib/cli.js')(process) + require('./lib/cli/validate-engines.js')(process) } else { throw new Error('The programmatic API was removed in npm v8.0.0') } diff --git a/lib/base-command.js b/lib/base-command.js index 46f193606256d..bfc410798582e 100644 --- a/lib/base-command.js +++ b/lib/base-command.js @@ -1,97 +1,88 @@ // Base class for npm commands const { relative } = require('path') - const { definitions } = require('@npmcli/config') const getWorkspaces = require('./workspaces/get-workspaces.js') - -const cmdAliases = require('./utils/cmd-list').aliases +const { aliases: cmdAliases } = require('./utils/cmd-list') class BaseCommand { static workspaces = false static ignoreImplicitWorkspace = true - constructor (npm) { - this.wrapWidth = 80 - this.npm = npm - - const { config } = this.npm + // these are all overridden by individual commands + static name = null + static description = null + static params = null - if (!this.constructor.skipConfigValidation) { - config.validate() - } + // this is a static so that we can read from it without instantiating a command + // which would require loading the config + static get usage () { + // XXX: this could use wrap util which reads from stdout.columns + const wrapWidth = 80 + const { description, usage = [''], name, params } = this - if (config.get('workspaces') === false && config.get('workspace').length) { - throw new Error('Can not use --no-workspaces and --workspace at the same time') - } - } - - get name () { - return this.constructor.name - } - - get description () { - return this.constructor.description - } - - get params () { - return this.constructor.params - } - - get usage () { - const usage = [ - `${this.description}`, + const fullUsage = [ + `${description}`, '', 'Usage:', + ...usage.map(u => `npm ${name} ${u}`.trim()), ] - if (!this.constructor.usage) { - usage.push(`npm ${this.name}`) - } else { - usage.push(...this.constructor.usage.map(u => `npm ${this.name} ${u}`)) - } - - if (this.params) { - usage.push('') - usage.push('Options:') - usage.push(this.wrappedParams) + if (params) { + let results = '' + let line = '' + for (const param of params) { + const paramUsage = `[${definitions[param].describeUsage()}]` + if (line.length + paramUsage.length > wrapWidth) { + results = [results, line].filter(Boolean).join('\n') + line = '' + } + line = [line, paramUsage].filter(Boolean).join(' ') + } + fullUsage.push('') + fullUsage.push('Options:') + fullUsage.push([results, line].filter(Boolean).join('\n')) } - const aliases = Object.keys(cmdAliases).reduce((p, c) => { - if (cmdAliases[c] === this.name) { - p.push(c) - } - return p + const aliases = Object.entries(cmdAliases).reduce((p, [k, v]) => { + return p.concat(v === name ? k : []) }, []) - if (aliases.length === 1) { - usage.push('') - usage.push(`alias: ${aliases.join(', ')}`) - } else if (aliases.length > 1) { - usage.push('') - usage.push(`aliases: ${aliases.join(', ')}`) + if (aliases.length) { + const plural = aliases.length === 1 ? '' : 'es' + fullUsage.push('') + fullUsage.push(`alias${plural}: ${aliases.join(', ')}`) } - usage.push('') - usage.push(`Run "npm help ${this.name}" for more info`) + fullUsage.push('') + fullUsage.push(`Run "npm help ${name}" for more info`) - return usage.join('\n') + return fullUsage.join('\n') } - get wrappedParams () { - let results = '' - let line = '' + constructor (npm) { + this.npm = npm - for (const param of this.params) { - const usage = `[${definitions[param].describeUsage()}]` - if (line.length && line.length + usage.length > this.wrapWidth) { - results = [results, line].filter(Boolean).join('\n') - line = '' - } - line = [line, usage].filter(Boolean).join(' ') + const { config } = this.npm + + if (!this.constructor.skipConfigValidation) { + config.validate() + } + + if (config.get('workspaces') === false && config.get('workspace').length) { + throw new Error('Can not use --no-workspaces and --workspace at the same time') + } + + // add getters for some statics to make reading them easier from an + // instantiated class + for (const key of ['name', 'description', 'params', 'usage']) { + Object.defineProperty(this, key, { + enumerable: true, + get () { + return this.constructor[key] + }, + }) } - results = [results, line].filter(Boolean).join('\n') - return results } usageError (prefix = '') { diff --git a/lib/cli.js b/lib/cli.js deleted file mode 100644 index c548ab38c4469..0000000000000 --- a/lib/cli.js +++ /dev/null @@ -1,157 +0,0 @@ -// This is separate to indicate that it should contain code we expect to work in -// all conceivably runnable versions of node. This is a best effort to catch -// syntax errors to give users a good error message if they are using a node -// version that doesn't allow syntax we are using such as private properties, etc -const createEnginesValidation = () => { - const node = process.version.replace(/-.*$/, '') - const pkg = require('../package.json') - const engines = pkg.engines.node - const npm = `v${pkg.version}` - - const cols = Math.min(Math.max(20, process.stdout.columns) || 80, 80) - const wrap = (lines) => lines - .join(' ') - .split(/[ \n]+/) - .reduce((left, right) => { - const last = left.split('\n').pop() - const join = last.length && last.length + right.length > cols ? '\n' : ' ' - return left + join + right - }) - .trim() - - const unsupportedMessage = wrap([ - `npm ${npm} does not support Node.js ${node}.`, - `You should probably upgrade to a newer version of node as we can't make any`, - `promises that npm will work with this version.`, - `This version of npm supports the following node versions: \`${engines}\`.`, - 'You can find the latest version at https://nodejs.org/.', - ]) - - const brokenMessage = wrap([ - `ERROR: npm ${npm} is known not to run on Node.js ${node}.`, - `You'll need to upgrade to a newer Node.js version in order to use this version of npm.`, - `This version of npm supports the following node versions: \`${engines}\`.`, - 'You can find the latest version at https://nodejs.org/.', - ]) - - // coverage ignored because this is only hit in very unsupported node versions - // and it's a best effort attempt to show something nice in those cases - /* istanbul ignore next */ - const syntaxErrorHandler = (err) => { - if (err instanceof SyntaxError) { - // eslint-disable-next-line no-console - console.error(`${brokenMessage}\n\nERROR:`) - // eslint-disable-next-line no-console - console.error(err) - return process.exit(1) - } - throw err - } - - process.on('uncaughtException', syntaxErrorHandler) - process.on('unhandledRejection', syntaxErrorHandler) - - return { - node, - engines, - unsupportedMessage, - off: () => { - process.off('uncaughtException', syntaxErrorHandler) - process.off('unhandledRejection', syntaxErrorHandler) - }, - } -} - -// Separated out for easier unit testing -module.exports = async process => { - // set it here so that regardless of what happens later, we don't - // leak any private CLI configs to other programs - process.title = 'npm' - - // if npm is called as "npmg" or "npm_g", then run in global mode. - if (process.argv[1][process.argv[1].length - 1] === 'g') { - process.argv.splice(1, 1, 'npm', '-g') - } - - // Nothing should happen before this line if we can't guarantee it will - // not have syntax errors in some version of node - const validateEngines = createEnginesValidation() - - const satisfies = require('semver/functions/satisfies') - const exitHandler = require('./utils/exit-handler.js') - const { dirname } = require('path') - const Npm = require('./npm.js') - const npm = new Npm({ - npmRoot: dirname(__dirname), - argv: process.argv, - // This is to prevent the config module from walking past this - // directory when looking for a parent package.json. This is currently - // only used in tests but if this feature would be added in the future - // it would need to be implemented something like this since the - // directory would need to be available before configs are parsed - cwdRoot: null, - }) - exitHandler.setNpm(npm) - - // only log node and npm paths in argv initially since argv can contain - // sensitive info. a cleaned version will be logged later - const log = require('./utils/log-shim.js') - log.verbose('cli', process.argv.slice(0, 2).join(' ')) - log.info('using', 'npm@%s', npm.version) - log.info('using', 'node@%s', process.version) - - // At this point we've required a few files and can be pretty sure - // we dont contain invalid syntax for this version of node. It's - // possible a lazy require would, but that's unlikely enough that - // it's not worth catching anymore and we attach the more important - // exit handlers. - validateEngines.off() - process.on('uncaughtException', exitHandler) - process.on('unhandledRejection', exitHandler) - - // It is now safe to log a warning if they are using a version of node - // that is not going to fail on syntax errors but is still unsupported - // and untested and might not work reliably. This is safe to use the logger - // now which we want since this will show up in the error log too. - if (!satisfies(validateEngines.node, validateEngines.engines)) { - log.warn('cli', validateEngines.unsupportedMessage) - } - - let cmd - // now actually fire up npm and run the command. - // this is how to use npm programmatically: - try { - await npm.load() - - if (npm.config.get('version', 'cli')) { - npm.output(npm.version) - return exitHandler() - } - - // npm --versions=cli - if (npm.config.get('versions', 'cli')) { - npm.argv = ['version'] - npm.config.set('usage', false, 'cli') - } - - cmd = npm.argv.shift() - if (!cmd) { - npm.output(await npm.usage) - process.exitCode = 1 - return exitHandler() - } - - await npm.exec(cmd) - return exitHandler() - } catch (err) { - if (err.code === 'EUNKNOWNCOMMAND') { - const didYouMean = require('./utils/did-you-mean.js') - const suggestions = await didYouMean(npm, npm.localPrefix, cmd) - npm.output(`Unknown command: "${cmd}"${suggestions}\n`) - npm.output('To see a list of supported npm commands, run:\n npm help') - process.exitCode = 1 - return exitHandler() - } - return exitHandler(err) - } -} diff --git a/lib/cli/index.js b/lib/cli/index.js new file mode 100644 index 0000000000000..7d931cfb19993 --- /dev/null +++ b/lib/cli/index.js @@ -0,0 +1,71 @@ +// Separated out for easier unit testing +module.exports = async (process, validateEngines) => { + // set it here so that regardless of what happens later, we don't + // leak any private CLI configs to other programs + process.title = 'npm' + + // if npm is called as "npmg" or "npm_g", then run in global mode. + if (process.argv[1].endsWith('g')) { + process.argv.splice(1, 1, 'npm', '-g') + } + + const { dirname } = require('path') + const satisfies = require('semver/functions/satisfies') + const exitHandler = require('../utils/exit-handler.js') + const log = require('../utils/log-shim.js') + const Npm = require('../npm.js') + + // At this point we've required a few files and can be pretty sure + // we dont contain invalid syntax for this version of node. It's + // possible a lazy require would, but that's unlikely enough that + // it's not worth catching anymore and we attach the more important + // exit handlers. + validateEngines.off() + process.on('uncaughtException', exitHandler) + process.on('unhandledRejection', exitHandler) + + const npm = exitHandler.setNpm(new Npm({ + npmRoot: dirname(__dirname), + argv: process.argv, + // This is to prevent the config module from walking past this + // directory when looking for a parent package.json. This is currently + // only used in tests but if this feature would be added in the future + // it would need to be implemented something like this since the + // directory would need to be available before configs are parsed + cwdRoot: null, + })) + + // only log node and npm paths in argv initially since argv can contain + // sensitive info. a cleaned version will be logged later + log.verbose('cli', process.argv.slice(0, 2).join(' ')) + log.info('using', 'npm@%s', npm.version) + log.info('using', 'node@%s', process.version) + + // It is now safe to log a warning if they are using a version of node + // that is not going to fail on syntax errors but is still unsupported + // and untested and might not work reliably. This is safe to use the logger + // now which we want since this will show up in the error log too. + if (!satisfies(validateEngines.node, validateEngines.engines)) { + log.warn('cli', validateEngines.unsupportedMessage) + } + + let exitErr + try { + // now actually fire up npm and run the command + // calling exec will read from the previously parsed proceess.argv + await npm.load() + await npm.exec() + } catch (err) { + if (err.code === 'EUNKNOWNCOMMAND') { + const didYouMean = require('../utils/did-you-mean.js') + const suggestions = await didYouMean(npm.localPrefix, npm.command) + npm.output(`Unknown command: "${npm.command}"${suggestions}\n`) + npm.output('To see a list of supported npm commands, run:\n npm help') + process.exitCode = 1 + } else { + exitErr = err + } + } + + return exitHandler(exitErr) +} diff --git a/lib/cli/validate-engines.js b/lib/cli/validate-engines.js new file mode 100644 index 0000000000000..1382d0079966c --- /dev/null +++ b/lib/cli/validate-engines.js @@ -0,0 +1,62 @@ +// This is separate to indicate that it should contain code we expect to work in +// all versions of node >= 6. This is a best effort to catch syntax errors to +// give users a good error message if they are using a node version that doesn't +// allow syntax we are using such as private properties, etc. This file is +// linted with ecmaVersion=6 so we don't use invalid syntax, which is set in the +// .eslintrc.local.json file + +const { engines: { node: supportNode }, version } = require('../../package.json') + +const wrap = (cols, lines) => lines.join(' ').split(/[ \n]+/).reduce((left, right) => { + const last = left.split('\n').pop() + const join = last.length && last.length + right.length > cols ? '\n' : ' ' + return left + join + right +}).trim() + +module.exports = (process) => { + const node = process.version.replace(/-.*$/, '') + const cols = Math.min(Math.max(20, process.stdout.columns) || 80, 80) + + const unsupportedMessage = wrap(cols, [ + `npm v${version} does not support Node.js ${node}.`, + `You should probably upgrade to a newer version of node as we can't make any`, + `promises that npm will work with this version.`, + `This version of npm supports the following node versions: \`${supportNode}\`.`, + 'You can find the latest version at https://nodejs.org/.', + ]) + + const brokenMessage = wrap(cols, [ + `ERROR: npm v${version} is known not to run on Node.js ${node}.`, + `You'll need to upgrade to a newer Node.js version in order to use this version of npm.`, + `This version of npm supports the following node versions: \`${supportNode}\`.`, + 'You can find the latest version at https://nodejs.org/.', + ]) + + // coverage ignored because this is only hit in very unsupported node versions + // and it's a best effort attempt to show something nice in those cases + /* istanbul ignore next */ + const syntaxErrorHandler = (err) => { + if (err instanceof SyntaxError) { + // eslint-disable-next-line no-console + console.error(`${brokenMessage}\n\nERROR:`) + // eslint-disable-next-line no-console + console.error(err) + return process.exit(1) + } + throw err + } + + process.on('uncaughtException', syntaxErrorHandler) + process.on('unhandledRejection', syntaxErrorHandler) + + return require('./index.js')(process, { + node, + npm: version, + engines: supportNode, + unsupportedMessage, + off: () => { + process.off('uncaughtException', syntaxErrorHandler) + process.off('unhandledRejection', syntaxErrorHandler) + }, + }) +} diff --git a/lib/commands/get.js b/lib/commands/get.js index 5e92e85a66382..6ac71a193ac79 100644 --- a/lib/commands/get.js +++ b/lib/commands/get.js @@ -4,17 +4,10 @@ class Get extends BaseCommand { static description = 'Get a value from the npm configuration' static name = 'get' static usage = ['[ ...] (See `npm config`)'] - static ignoreImplicitWorkspace = false - - // TODO - /* istanbul ignore next */ - async completion (opts) { - const config = await this.npm.cmd('config') - return config.completion(opts) - } async exec (args) { return this.npm.exec('config', ['get'].concat(args)) } } + module.exports = Get diff --git a/lib/commands/help.js b/lib/commands/help.js index 3ab2c56319868..f8eaa5f0093ec 100644 --- a/lib/commands/help.js +++ b/lib/commands/help.js @@ -50,7 +50,7 @@ class Help extends BaseCommand { const manSearch = /^\d+$/.test(args[0]) ? `man${args.shift()}` : 'man*' if (!args.length) { - return this.npm.output(await this.npm.usage) + return this.npm.output(this.npm.usage) } // npm help foo bar baz: search topics diff --git a/lib/commands/run-script.js b/lib/commands/run-script.js index 51746c5e5285d..81bd2333219fc 100644 --- a/lib/commands/run-script.js +++ b/lib/commands/run-script.js @@ -95,7 +95,7 @@ class RunScript extends BaseCommand { return } - const suggestions = await didYouMean(this.npm, path, event) + const suggestions = await didYouMean(path, event) throw new Error( `Missing script: "${event}"${suggestions}\n\nTo see a list of scripts, run:\n npm run` ) diff --git a/lib/commands/set.js b/lib/commands/set.js index b650026a599a9..07202614ec917 100644 --- a/lib/commands/set.js +++ b/lib/commands/set.js @@ -1,16 +1,9 @@ -const BaseCommand = require('../base-command.js') +const ConfigCommand = require('./config') -class Set extends BaseCommand { +class Set extends ConfigCommand { static description = 'Set a value in the npm configuration' static name = 'set' static usage = ['= [= ...] (See `npm config`)'] - static ignoreImplicitWorkspace = false - - // TODO - /* istanbul ignore next */ - async completion (opts) { - return this.npm.cmd('config').completion(opts) - } async exec (args) { if (!args.length) { @@ -19,4 +12,5 @@ class Set extends BaseCommand { return this.npm.exec('config', ['set'].concat(args)) } } + module.exports = Set diff --git a/lib/commands/version.js b/lib/commands/version.js index a523283671791..9c421ac9e91cf 100644 --- a/lib/commands/version.js +++ b/lib/commands/version.js @@ -99,9 +99,12 @@ class Version extends BaseCommand { } async list (results = {}) { - const pj = resolve(this.npm.prefix, 'package.json') + if (this.npm.config.get('version')) { + this.npm.output(this.npm.version) + return + } - const pkg = await readFile(pj, 'utf8') + const pkg = await readFile(resolve(this.npm.prefix, 'package.json'), 'utf8') .then(data => JSON.parse(data)) .catch(() => ({})) diff --git a/lib/npm.js b/lib/npm.js index 9b2e9fba22067..e46a172d7d83e 100644 --- a/lib/npm.js +++ b/lib/npm.js @@ -11,27 +11,34 @@ const LogFile = require('./utils/log-file.js') const Timers = require('./utils/timers.js') const Display = require('./utils/display.js') const log = require('./utils/log-shim') -const replaceInfo = require('./utils/replace-info.js') const updateNotifier = require('./utils/update-notifier.js') const pkg = require('../package.json') -const cmdList = require('./utils/cmd-list.js') +const { deref } = require('./utils/cmd-list.js') class Npm { - static get version () { - return pkg.version + static version = pkg.version + + static derefCommand (c) { + const command = deref(c) + if (!command) { + throw Object.assign(new Error(`Unknown command ${c}`), { + code: 'EUNKNOWNCOMMAND', + }) + } + return require(`./commands/${command}.js`) } updateNotification = null - argv = [] + + #argvCommand = null + #argvArgs = null + #argvClean = null #command = null + + #npmRoot = null #runId = new Date().toISOString().replace(/[.:]/g, '_') - #loadPromise = null - #title = 'npm' - #argvClean = [] #chalk = null - #npmRoot = null - #warnedNonDashArg = false #outputBuffer = [] #logFile = new LogFile() @@ -48,97 +55,68 @@ class Npm { constructor ({ npmRoot, argv, cwdRoot }) { this.#npmRoot = npmRoot this.config = new Config({ npmRoot, argv, cwdRoot }) + // config has parsed argv and now knows the command name + // and the remaining args + this.#argvCommand = this.config.command + this.#argvArgs = this.config.args + this.#argvClean = this.config.clean } get version () { return this.constructor.version } - deref (c) { - if (!c) { - return - } - if (c.match(/[A-Z]/)) { - c = c.replace(/([A-Z])/g, m => '-' + m.toLowerCase()) - } - if (cmdList.plumbing.indexOf(c) !== -1) { - return c - } - // first deref the abbrev, if there is one - // then resolve any aliases - // so `npm install-cl` will resolve to `install-clean` then to `ci` - let a = cmdList.abbrevs[c] - while (cmdList.aliases[a]) { - a = cmdList.aliases[a] - } - return a + get title () { + return this.config.title } - // Get an instantiated npm command - // npm.command is already taken as the currently running command, a refactor - // would be needed to change this - async cmd (cmd) { - await this.load() - - const cmdId = this.deref(cmd) - if (!cmdId) { - throw Object.assign(new Error(`Unknown command ${cmd}`), { - code: 'EUNKNOWNCOMMAND', - }) - } - - const Impl = require(`./commands/${cmdId}.js`) - const command = new Impl(this) + get command () { + return this.#command?.constructor?.name + } - // since 'test', 'start', 'stop', etc. commands re-enter this function - // to call the run-script command, we need to only set it one time. - if (!this.#command) { - this.#command = command - process.env.npm_command = this.command - } + get isShellout () { + return this.#command?.constructor?.isShellout + } - return command - } - - // Call an npm command - async exec (cmd, args = this.argv) { - const command = await this.cmd(cmd) - const timeEnd = this.time(`command:${cmd}`) - - // this is async but we dont await it, since its ok if it doesnt - // finish before the command finishes running. it uses command and argv - // so it must be initiated here, after the command name is set - // eslint-disable-next-line promise/catch-or-return - updateNotifier(this).then((msg) => (this.updateNotification = msg)) - - // Options are prefixed by a hyphen-minus (-, \u2d). - // Other dash-type chars look similar but are invalid. - if (!this.#warnedNonDashArg) { - const nonDashArgs = args.filter(a => /^[\u2010-\u2015\u2212\uFE58\uFE63\uFF0D]/.test(a)) - if (nonDashArgs.length) { - this.#warnedNonDashArg = true - log.error( - 'arg', - 'Argument starts with non-ascii dash, this is probably invalid:', - nonDashArgs.join(', ') - ) - } - } + get npmRoot () { + return this.#npmRoot + } - await command.cmdExec(args).finally(timeEnd) + get usage () { + return usage(this) } - async load () { - if (!this.#loadPromise) { - this.#loadPromise = this.time('npm:load', () => this.#load()) + // Execute an npm command + // XXX: tests call this method directly with a supplied + // command name and args, but the actual cli relies on the config module to + // parse the argv and then execute the resulting command. So we account for + // both methods here, but a future refactor should convert tests to run + // similar to how the CLI does by passing in an argv containing the command + // and args. + async exec (commandName = this.#argvCommand, args = this.#argvArgs) { + // exec can be re-entered by an already running command but if it is the + // first time we want to kickoff the update notifier + if (!this.#command) { + // this is async but we dont await it, since its ok if it doesnt + // finish before the command finishes running. it uses command and argv + // so it must be initiated here, after the command name is set + // eslint-disable-next-line promise/catch-or-return + updateNotifier(this).then((msg) => (this.updateNotification = msg)) } - return this.#loadPromise + + const Impl = Npm.derefCommand(commandName) + this.#command = new Impl(this) + return this.time(`command:${this.command}`, () => this.#command.cmdExec(args)) } get loaded () { return this.config.loaded } + async load () { + return this.time('npm:load', () => this.#load()) + } + // This gets called at the end of the exit handler and // during any tests to cleanup all of our listeners // Everything in here should be synchronous @@ -161,16 +139,12 @@ class Npm { }) } - get title () { - return this.#title - } - - set title (t) { - process.title = t - this.#title = t - } - async #load () { + if (this.config.get('version', Config.Locations.cli)) { + this.output(this.version) + return + } + await this.time('npm:load:configload', () => this.config.load()) // mkdir this separately since the logs dir can be set to @@ -187,73 +161,35 @@ class Npm { fs.mkdir(this.flatOptions.logsDir, { recursive: true }) .catch((e) => log.verbose('logfile', `could not create logs-dir: ${e}`))) - // note: this MUST be shorter than the actual argv length, because it - // uses the same memory, so node will truncate it if it's too long. - this.time('npm:load:setTitle', () => { - const { parsedArgv: { cooked, remain } } = this.config - this.argv = remain - // Secrets are mostly in configs, so title is set using only the positional args - // to keep those from being leaked. - this.title = ['npm'].concat(replaceInfo(remain)).join(' ').trim() - // The cooked argv is also logged separately for debugging purposes. It is - // cleaned as a best effort by replacing known secrets like basic auth - // password and strings that look like npm tokens. XXX: for this to be - // safer the config should create a sanitized version of the argv as it - // has the full context of what each option contains. - this.#argvClean = replaceInfo(cooked) - log.verbose('title', this.title) - log.verbose('argv', this.#argvClean.map(JSON.stringify).join(' ')) - }) - - this.time('npm:load:display', () => { - this.#display.load({ - // Use logColor since that is based on stderr - color: this.logColor, - progress: this.flatOptions.progress, - silent: this.silent, - timing: this.config.get('timing'), - loglevel: this.config.get('loglevel'), - unicode: this.config.get('unicode'), - heading: this.config.get('heading'), - }) - }) - - this.time('npm:load:logFile', () => { - this.#logFile.load({ - path: this.logPath, - logsMax: this.config.get('logs-max'), - }) - log.verbose('logfile', this.#logFile.files[0] || 'no logfile created') - }) - - this.time('npm:load:timers', () => - this.#timers.load({ - path: this.config.get('timing') ? this.logPath : null, - }) - ) - - const configScope = this.config.get('scope') - if (!/^@/.test(configScope)) { - this.config.set('scope', `@${configScope}`, this.config.find('scope')) - } + this.time('npm:load:display', () => this.#display.load({ + // Use logColor since that is based on stderr + color: this.logColor, + progress: this.flatOptions.progress, + silent: this.silent, + timing: this.config.get('timing'), + loglevel: this.config.get('loglevel'), + unicode: this.config.get('unicode'), + heading: this.config.get('heading'), + })) + + this.time('npm:load:logFile', () => this.#logFile.load({ + path: this.logPath, + logsMax: this.config.get('logs-max'), + })) + + this.time('npm:load:timers', () => this.#timers.load({ + path: this.config.get('timing') ? this.logPath : null, + })) if (this.config.get('force')) { log.warn('using --force', 'Recommended protections disabled.') } } - get isShellout () { - return this.#command?.constructor?.isShellout - } - - get command () { - return this.#command?.name - } - get flatOptions () { const { flat } = this.config if (this.command) { - flat.npmCommand = this.command + flat.npmCommand = this.commandName } return flat } @@ -271,17 +207,14 @@ class Npm { get chalk () { if (!this.#chalk) { - let level = chalk.level - if (!this.color) { - level = 0 - } + const level = this.color ? chalk.level : 0 this.#chalk = new chalk.Instance({ level }) } return this.#chalk } get global () { - return this.config.global + return this.flatOptions.global } get silent () { @@ -292,6 +225,10 @@ class Npm { return 2 } + get timingFile () { + return this.#timers.file + } + get unfinishedTimers () { return this.#timers.unfinished } @@ -316,18 +253,14 @@ class Npm { return resolve(this.logsDir, `${this.#runId}-`) } - get timingFile () { - return this.#timers.file - } - - get npmRoot () { - return this.#npmRoot - } - get cache () { return this.config.get('cache') } + get prefix () { + return this.global ? this.globalPrefix : this.localPrefix + } + get globalPrefix () { return this.config.globalPrefix } @@ -340,6 +273,10 @@ class Npm { return this.config.localPackage } + get dir () { + return this.global ? this.globalDir : this.localDir + } + get globalDir () { return resolve(this.globalPrefix, process.platform !== 'win32' ? 'lib' : '', 'node_modules') } @@ -348,8 +285,8 @@ class Npm { return resolve(this.localPrefix, 'node_modules') } - get dir () { - return this.global ? this.globalDir : this.localDir + get bin () { + return this.global ? this.globalBin : this.localBin } get globalBin () { @@ -360,18 +297,6 @@ class Npm { return resolve(this.dir, '.bin') } - get bin () { - return this.global ? this.globalBin : this.localBin - } - - get prefix () { - return this.global ? this.globalPrefix : this.localPrefix - } - - get usage () { - return usage(this) - } - // output to stdout in a progress bar compatible way output (...msg) { log.clearProgress() @@ -380,6 +305,13 @@ class Npm { log.showProgress() } + outputError (...msg) { + log.clearProgress() + // eslint-disable-next-line no-console + console.error(...msg) + log.showProgress() + } + outputBuffer (item) { this.#outputBuffer.push(item) } @@ -410,12 +342,5 @@ class Npm { this.#outputBuffer.length = 0 } - - outputError (...msg) { - log.clearProgress() - // eslint-disable-next-line no-console - console.error(...msg) - log.showProgress() - } } module.exports = Npm diff --git a/lib/utils/cmd-list.js b/lib/utils/cmd-list.js index 03fe8ed07c930..e15f8c5e2312b 100644 --- a/lib/utils/cmd-list.js +++ b/lib/utils/cmd-list.js @@ -68,7 +68,7 @@ const aliases = { 'add-user': 'adduser', } -// these are filenames in . +// these are filenames in lib/commands const commands = [ 'access', 'adduser', @@ -141,7 +141,28 @@ const plumbing = ['help-search'] const allCommands = [...commands, ...plumbing].sort(localeCompare) const abbrevs = abbrev(commands.concat(Object.keys(aliases))) +const deref = (c) => { + if (!c) { + return + } + if (c.match(/[A-Z]/)) { + c = c.replace(/([A-Z])/g, m => '-' + m.toLowerCase()) + } + if (plumbing.includes(c)) { + return c + } + // first deref the abbrev, if there is one + // then resolve any aliases + // so `npm install-cl` will resolve to `install-clean` then to `ci` + let a = abbrevs[c] + while (aliases[a]) { + a = aliases[a] + } + return a +} + module.exports = { + deref, abbrevs, aliases, commands, diff --git a/lib/utils/did-you-mean.js b/lib/utils/did-you-mean.js index 10b33d5f83a08..355a2e2a32d6a 100644 --- a/lib/utils/did-you-mean.js +++ b/lib/utils/did-you-mean.js @@ -1,40 +1,43 @@ const { distance } = require('fastest-levenshtein') const readJson = require('read-package-json-fast') -const { commands } = require('./cmd-list.js') +const { commands } = require('./cmd-nplist.js') +const Npm = require('../npm') + +// We would already be suggesting this in `npm x` so omit them here +const runScripts = ['stop', 'start', 'test', 'restart'] + +const didYouMean = async (path, scmd) => { + const suggest = [] -const didYouMean = async (npm, path, scmd) => { - // const cmd = await npm.cmd(str) const close = commands.filter(cmd => distance(scmd, cmd) < scmd.length * 0.4 && scmd !== cmd) - let best = [] for (const str of close) { - const cmd = await npm.cmd(str) - best.push(` npm ${str} # ${cmd.description}`) - } - // We would already be suggesting this in `npm x` so omit them here - const runScripts = ['stop', 'start', 'test', 'restart'] - try { - const { bin, scripts } = await readJson(`${path}/package.json`) - best = best.concat( - Object.keys(scripts || {}) - .filter(cmd => distance(scmd, cmd) < scmd.length * 0.4 && !runScripts.includes(cmd)) - .map(str => ` npm run ${str} # run the "${str}" package script`), - Object.keys(bin || {}) - .filter(cmd => distance(scmd, cmd) < scmd.length * 0.4) - /* eslint-disable-next-line max-len */ - .map(str => ` npm exec ${str} # run the "${str}" command from either this or a remote npm package`) - ) - } catch (_) { - // gracefully ignore not being in a folder w/ a package.json + const Command = Npm.derefCommand(str) + suggest.push([str, Command.description]) } - if (best.length === 0) { + // gracefully ignore not being in a folder w/ a package.json + const { bin = {}, scripts = {} } = await readJson(`${path}/package.json`).catch(() => ({})) + + suggest.concat(Object.keys(scripts) + .filter(cmd => distance(scmd, cmd) < scmd.length * 0.4 && !runScripts.includes(cmd)) + .map(str => [`run ${str}`, `run the "${str}" package script`])) + + suggest.concat(Object.keys(bin) + .filter(cmd => distance(scmd, cmd) < scmd.length * 0.4) + .map(str => [ + `exec ${str}`, + `run the "${str}" command from either this or a remote npm package`] + )) + + if (suggest.length === 0) { return '' } - const suggestion = - best.length === 1 - ? `\n\nDid you mean this?\n${best[0]}` - : `\n\nDid you mean one of these?\n${best.slice(0, 3).join('\n')}` - return suggestion + const best = suggest.slice(0, 3) + const plural = best.length === 1 ? 'this' : 'one of these' + const messages = best.map((b) => `npm ${b[0]} # ${b[1]}`) + + return `\n\nDid you mean ${plural}?\n${messages.join('\n')}` } + module.exports = didYouMean diff --git a/lib/utils/exit-handler.js b/lib/utils/exit-handler.js index 05395f04f2568..ea1591bdbe68e 100644 --- a/lib/utils/exit-handler.js +++ b/lib/utils/exit-handler.js @@ -154,7 +154,7 @@ const exitHandler = err => { } else { if (!err.code) { const matchErrorCode = err.message.match(/^(?:Error: )?(E[A-Z]+)/) - err.code = matchErrorCode && matchErrorCode[1] + err.code = matchErrorCode?.[1] } for (const k of ['type', 'stack', 'statusCode', 'pkgid']) { diff --git a/lib/utils/log-file.js b/lib/utils/log-file.js index f663997308ed6..11242a557d696 100644 --- a/lib/utils/log-file.js +++ b/lib/utils/log-file.js @@ -73,19 +73,23 @@ class LogFiles { // Log stream has already ended if (!this.#logStream) { + log.verbose('logfile', 'no logfile, logstream already closed') return } - log.verbose('logfile', `logs-max:${logsMax} dir:${this.#path}`) + log.verbose('logfile', `dir:${this.#path}`) // Pipe our initial stream to our new file stream and // set that as the new log logstream for future writes // if logs max is 0 then the user does not want a log file if (this.#logsMax > 0) { - const initialFile = this.#openLogFile() - if (initialFile) { - this.#logStream = this.#logStream.pipe(initialFile) + const initialStream = this.#openLogFile() + if (initialStream) { + log.verbose('logfile', initialStream.path) + this.#logStream = this.#logStream.pipe(initialStream) } + } else { + log.verbose('logfile', `no logfile, logs-max:${logsMax}`) } // Kickoff cleaning process, even if we aren't writing a logfile. @@ -213,7 +217,8 @@ class LogFiles { return } - log.silly('logfile', `start cleaning logs, removing ${toDelete} files`) + log.silly('logfile', + `start cleaning logs, files:${files.length} max:${this.#logsMax} removing:${toDelete}`) for (const file of files.slice(0, toDelete)) { try { diff --git a/lib/utils/npm-usage.js b/lib/utils/npm-usage.js index b04ad33f9dd79..dd6ce4029f53c 100644 --- a/lib/utils/npm-usage.js +++ b/lib/utils/npm-usage.js @@ -1,4 +1,4 @@ -const { commands } = require('./cmd-list') +const { commands, derefCommand } = require('./cmd-list') const COL_MAX = 60 const COL_MIN = 24 @@ -10,7 +10,7 @@ const indentNewline = (repeat) => `\n${indent(repeat)}` module.exports = async (npm) => { const browser = npm.config.get('viewer') === 'browser' ? ' (in a browser)' : '' - const allCommands = npm.config.get('long') ? await cmdUsages(npm) : cmdNames() + const allCommands = npm.config.get('long') ? cmdUsages() : cmdNames() return `npm @@ -57,12 +57,12 @@ const cmdNames = () => { return indentNewline() + out.join(indentNewline()).slice(2) } -const cmdUsages = async (npm) => { +const cmdUsages = () => { // return a string of : let maxLen = 0 const set = [] for (const c of commands) { - const { usage } = await npm.cmd(c) + const { usage } = derefCommand(c) set.push([c, usage.split('\n')]) maxLen = Math.max(maxLen, c.length) } diff --git a/package-lock.json b/package-lock.json index d7a9701ad27f4..be24aa5fb2141 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14397,6 +14397,7 @@ "ci-info": "^3.7.1", "ini": "^3.0.0", "nopt": "file:../../../nopt/nopt-7.0.0.tgz", + "npm-registry-fetch": "^14.0.3", "proc-log": "^3.0.0", "read-package-json-fast": "^3.0.0", "semver": "^7.3.5", diff --git a/test/fixtures/mock-npm.js b/test/fixtures/mock-npm.js index 942298c5a2dc5..9e4b354f2b741 100644 --- a/test/fixtures/mock-npm.js +++ b/test/fixtures/mock-npm.js @@ -10,6 +10,8 @@ const tmock = require('./tmock') const defExitCode = process.exitCode const npmRoot = path.resolve(__dirname, '../../') +// changes to the supplied directory and returns a function +// to reset at the end of a test const changeDir = (dir) => { if (dir) { const cwd = process.cwd() @@ -19,6 +21,10 @@ const changeDir = (dir) => { return () => {} } +// takes the passed in testdir options for the global directory and rewrites +// the nested depth and any symlinks to match the desired location based on the +// platform. windows wants everything at `GLOBAL/node_modules` and other platforms +// want `GLOBAL/lib/node_modules` const setGlobalNodeModules = (globalDir) => { const updateSymlinks = (obj, visit) => { for (const [key, value] of Object.entries(obj)) { @@ -58,12 +64,15 @@ const getMockNpm = async (t, { mocks, init, load, npm: npmOpts }) => { } const Npm = tmock(t, '{LIB}/npm.js', { - '{LIB}/utils/update-notifier.js': async () => {}, + // update-notifier is designed to not await until its finished + // so we always mock it to a sync noop function so tests will + // always complete. the actual update notifier is tested separately + '{LIB}/utils/update-notifier.js': () => {}, ...mocks, ...mock.logMocks, }) - class MockNpm extends Npm { + mock.Npm = class extends Npm { async exec (...args) { const [res, err] = await super.exec(...args).then((r) => [r]).catch(e => [null, e]) // This mimics how the exit handler flushes output for commands that have @@ -96,9 +105,8 @@ const getMockNpm = async (t, { mocks, init, load, npm: npmOpts }) => { } } - mock.Npm = MockNpm if (init) { - mock.npm = new MockNpm(npmOpts) + mock.npm = new mock.Npm(npmOpts) if (load) { await mock.npm.load() } @@ -220,10 +228,11 @@ const setupMockNpm = async (t, { mockGlobals(t, { 'process.env.HOME': dirs.home, - // global prefix cannot be (easily) set via argv so this is the easiest way - // to set it that also closely mimics the behavior a user would see since it - // will already be set while `npm.load()` is being run - // Note that this only sets the global prefix and the prefix is set via chdir + // global prefix cannot be set via argv without affecting other prefixes so + // this is the easiest way to set it that also closely mimics the behavior a + // user would see since it will already be set while `npm.load()` is being + // run Note that this only sets the global prefix and the prefix is set via + // chdir 'process.env.PREFIX': dirs.globalPrefix, ...withDirs(globals), ...env, diff --git a/workspaces/config/lib/config-data.js b/workspaces/config/lib/config-data.js index 7b0e4f82779dc..fea82e11e9883 100644 --- a/workspaces/config/lib/config-data.js +++ b/workspaces/config/lib/config-data.js @@ -4,9 +4,8 @@ const ini = require('ini') const fs = require('fs/promises') const { dirname } = require('path') const nerfDart = require('./nerf-dart') -const envReplace = require('./env-replace') const { typeDefs } = require('./type-defs') -const { definitions, shorthands, types } = require('./config') +const { definitions, shorthands, types } = require('./definitions') const SYMBOLS = { set: Symbol('set'), @@ -29,7 +28,7 @@ class ConfigData extends Map { #loaded = null #valid = true - constructor (type, { parent, data, env }) { + constructor (type, { parent, data, envReplace }) { super() this.#parent = parent @@ -40,8 +39,7 @@ class ConfigData extends Map { this.#where = where this.#description = description this.#type = opts - - this.#envReplace = (v) => envReplace(v, env) + this.#envReplace = envReplace for (const key of Object.keys(SYMBOLS)) { this[key] = () => { @@ -116,31 +114,23 @@ class ConfigData extends Map { load (data, error, file) { this.#assertLoaded(false) - if (file) { - this.#file = file - this.#loaded = `${this.description}, file: ${file}` - } else { - this.#loaded = this.description - } + this.#file = file + this.#loaded = this.description + (file ? `, file: ${file}` : '') - if (error?.code !== 'ENOENT') { - log.verbose('config', `error loading ${this.where} config`, error) + if (error) { + if (error.code !== 'ENOENT') { + log.verbose('config', `error loading ${this.where} config`, error) + } + return } - if (error || !data) { - return + if (!data) { + throw new Error(`Cannot load config location without data: ${this.where}`) } // an array comes from argv so we parse it in the standard nopt way if (Array.isArray(data)) { - const { argv, ...parsedData } = nopt.nopt(data, { - typeDefs, - shorthands, - types, - invalidHandler: (...args) => this.#invalidHandler(...args), - }) - this.#setAll(parsedData) - return { argv } + return this.#loadArgv(data) } // if its a string then it came from a file and we need to parse @@ -159,6 +149,17 @@ class ConfigData extends Map { this.#setAll(this.#clean(parsed)) } + #loadArgv (data) { + const { argv, ...parsedData } = nopt.nopt(data, { + typeDefs, + shorthands, + types, + invalidHandler: (...args) => this.#invalidHandler(...args), + }) + this.#setAll(parsedData) + return argv + } + #setAll (data) { for (const [key, value] of Object.entries(data)) { this.#set(key, value) diff --git a/workspaces/config/lib/config-locations.js b/workspaces/config/lib/config-locations.js index 1e1ad5b49cdaf..e9c4ad56ace75 100644 --- a/workspaces/config/lib/config-locations.js +++ b/workspaces/config/lib/config-locations.js @@ -1,5 +1,5 @@ const ConfigData = require('./config-data') -const { derived, definitions, camelCase, keys: configKeys } = require('./config') +const { derived, definitions, camelCase, keys: configKeys } = require('./definitions') // this is in order from least -> most precedence const LocationsList = Object.entries({ @@ -21,7 +21,8 @@ const Locations = LocationsList.reduce((acc, [location]) => { class ConfigLocations extends Map { static Locations = Locations - #env = null + #envReplace = null + #config = null #list = [] #revList = [] @@ -33,10 +34,11 @@ class ConfigLocations extends Map { #base = new Map() #derived = new Map() - constructor ({ env }) { + constructor ({ envReplace, config }) { super() - this.#env = env + this.#envReplace = envReplace + this.#config = config for (const key of configKeys) { this.#createBaseDescriptor(key) @@ -71,7 +73,11 @@ class ConfigLocations extends Map { } add (location, configData) { - const data = new ConfigData(location, { parent: this, data: configData, env: this.#env }) + const data = new ConfigData(location, { + parent: this, + data: configData, + envReplace: this.#envReplace, + }) this.#indexes[data.where] = this.#list.push(data.where) - 1 this.#revList.unshift(data.where) @@ -174,7 +180,7 @@ class ConfigLocations extends Map { if (this.#derived.has(k)) { return this.#derived.get(k) } - const value = derive.set(this.#baseData) + const value = derive.set(this.#baseData, this.#config) this.#derived.set(k, value) return value }, diff --git a/workspaces/config/lib/config/definition.js b/workspaces/config/lib/definitions/definition.js similarity index 95% rename from workspaces/config/lib/config/definition.js rename to workspaces/config/lib/definitions/definition.js index 4b21fc8f73b7e..15d04739b43bb 100644 --- a/workspaces/config/lib/config/definition.js +++ b/workspaces/config/lib/definitions/definition.js @@ -42,14 +42,19 @@ class Definition { } } - for (const req of required) { - if (!hasOwn(this.#def, req)) { - throw new Error(`config \`${this.#key}\` lacks key \`${req}\``) - } - } + // needs a key if (!this.#key) { throw new Error(`config lacks key: ${this.#key}`) } + + // needs required keys + for (const req of required) { + if (typeof req === 'string' && !this.#hasOwn(req)) { + throw new Error(`config \`${this.#key}\` lacks required key: \`${req}\``) + } + } + + // only allowed fields for (const field of Object.keys(this.#def)) { if (!allowed.includes(field)) { throw new Error(`config defines unknown field ${field}: ${this.#key}`) @@ -57,6 +62,10 @@ class Definition { } } + #hasOwn (k) { + return hasOwn(this.#def, k) + } + get default () { return this.#def.default } diff --git a/workspaces/config/lib/config/index.js b/workspaces/config/lib/definitions/index.js similarity index 98% rename from workspaces/config/lib/config/index.js rename to workspaces/config/lib/definitions/index.js index e5704a05ae3e7..a875f5ea0491c 100644 --- a/workspaces/config/lib/config/index.js +++ b/workspaces/config/lib/definitions/index.js @@ -1,6 +1,6 @@ const ciInfo = require('ci-info') const { tmpdir } = require('os') -const { join } = require('path') +const { join, resolve } = require('path') const fs = require('fs') const Arborist = require('@npmcli/arborist') const { Types } = require('../type-defs') @@ -764,7 +764,7 @@ define('global', { define('globalconfig', { type: Types.Path, - default: '', + default: '', // default is derived when config loads defaultDescription: ` The global --prefix setting plus 'etc/npmrc'. For example, '/usr/local/etc/npmrc' @@ -1430,11 +1430,10 @@ define('prefer-online', { flatten: true, }) -// `prefix` has its default defined outside of this module define('prefix', { type: Types.Path, short: 'C', - default: '', + default: '', // default is derived when config loads, defaultDescription: ` In global mode, the folder where the node executable is installed. Otherwise, the nearest parent folder containing either a package.json @@ -2126,6 +2125,22 @@ derive(['global', 'location'], ({ global, location }) => { return isGlobal ? { global: true, location: 'global' } : { global, location } }) +derive('prefix', ({ prefix, globalconfig }) => { + // if the prefix is set on cli, env, or userconfig, then we need to + // default the globalconfig file to that location, instead of the default + // global prefix. It's weird that `npm get globalconfig --prefix=/foo` + // returns `/foo/etc/npmrc`, but better to not change it at this point. + return globalconfig ?? resolve(prefix, 'etc/npmrc') +}, ['prefix']) + +derive('globalconfig', ({ prefix, globalconfig }) => { + // if the prefix is set on cli, env, or userconfig, then we need to + // default the globalconfig file to that location, instead of the default + // global prefix. It's weird that `npm get globalconfig --prefix=/foo` + // returns `/foo/etc/npmrc`, but better to not change it at this point. + return globalconfig ?? resolve(prefix, 'etc/npmrc') +}, ['prefix']) + derive(['cache', 'npx-cache', 'logs-dir'], ({ cache, logsDir }) => { return { cache: join(cache, '_cacache'), diff --git a/workspaces/config/lib/env-replace.js b/workspaces/config/lib/env-replace.js deleted file mode 100644 index c851f6e4d1501..0000000000000 --- a/workspaces/config/lib/env-replace.js +++ /dev/null @@ -1,14 +0,0 @@ -// replace any ${ENV} values with the appropriate environ. - -const envExpr = /(? f.replace(envExpr, (orig, esc, name) => { - const val = env[name] !== undefined ? env[name] : `$\{${name}}` - - // consume the escape chars that are relevant. - if (esc.length % 2) { - return orig.slice((esc.length + 1) / 2) - } - - return (esc.slice(esc.length / 2)) + val -}) diff --git a/workspaces/config/lib/index.js b/workspaces/config/lib/index.js index ca7b558592c08..92bf45a7f05d0 100644 --- a/workspaces/config/lib/index.js +++ b/workspaces/config/lib/index.js @@ -8,13 +8,14 @@ const { resolve, dirname, join } = require('path') const { homedir } = require('os') const fs = require('fs/promises') const TypeDefs = require('./type-defs.js') -const SetEnv = require('./set-envs.js') +const SetGlobal = require('./set-globals.js') const { ErrInvalidAuth } = require('./errors') const Credentials = require('./credentials.js') const ConfigTypes = require('./config-locations') -const { definitions, defaults } = require('./config') +const { definitions, defaults } = require('./definitions') const { isNerfed } = require('./nerf-dart.js') -const ConfTypes = ConfigTypes.ConfTypes +const replaceInfo = require('./replace-info') +const Locations = ConfigTypes.Locations const fileExists = (...p) => fs.stat(resolve(...p)) .then((st) => st.isFile()) @@ -24,28 +25,12 @@ const dirExists = (...p) => fs.stat(resolve(...p)) .then((st) => st.isDirectory()) .catch(() => false) -// define a custom getter, but turn into a normal prop -// if we set it. otherwise it can't be set on child objects -const settableGetter = (get, ...args) => Object.defineProperty(...args, { - configurable: true, - enumerable: true, - get, - set (value) { - Object.defineProperty(...args, { - value, - configurable: true, - writable: true, - enumerable: true, - }) - }, -}) - class Config { static TypeDefs = TypeDefs static Types = TypeDefs.Types - static ConfigTypes = ConfigTypes.ConfigTypes - static EnvKeys = [...SetEnv.ALLOWED_ENV_KEYS.values()] - static ProcessKeys = [...SetEnv.ALLOWED_ENV_KEYS.values()] + static Locations = Locations + static EnvKeys = [...SetGlobal.EnvKeys.values()] + static ProcessKeys = [...SetGlobal.ProcessKeys.values()] static nerfDarts = Credentials.nerfDarts // state @@ -63,17 +48,23 @@ class Config { #execPath = null #cwd = null + // set during init which is called in ctor + #command = null + #args = null + #clean = null + #title = null + // set when we load configs #globalPrefix = null #localPrefix = null #localPackage = null #loaded = false #home = null - #parsedArgv = null // functions #setEnv = null #setNpmEnv = null + #setProc = null #credentials = null constructor ({ @@ -104,11 +95,18 @@ class Config { TypeDefs.typeDefs.path.HOME = this.#home TypeDefs.typeDefs.path.PLATFORM = this.#platform - this.#configData = new ConfigTypes({ env: this.#env }) + this.#configData = new ConfigTypes({ + envReplace: (k) => SetGlobal.replaceEnv(this.#env), + config: this, + }) this.#credentials = new Credentials(this) - this.#setEnv = (...args) => SetEnv.setEnv(this.#env, ...args) - this.#setNpmEnv = (...args) => SetEnv.npm.setEnv(this.#env, ...args) + + this.#setProc = (...args) => SetGlobal.setProcess(this.#process, ...args) + this.#setEnv = (...args) => SetGlobal.setEnv(this.#env, ...args) + this.#setNpmEnv = (...args) => SetGlobal.npm.setEnv(this.#env, ...args) + + this.#init() } // ============================================= @@ -120,38 +118,23 @@ class Config { return this.#loaded } - get prefix () { - this.#assertLoaded() - return this.#global ? this.globalPrefix : this.localPrefix - } - get globalPrefix () { - this.#assertLoaded() return this.#globalPrefix } get localPrefix () { - this.#assertLoaded() return this.#localPrefix } get localPackage () { - this.#assertLoaded() return this.#localPackage } - get parsedArgv () { - this.#assertLoaded() - return this.#parsedArgv - } - get flat () { - this.#assertLoaded() return this.#configData.data } get valid () { - this.#assertLoaded() for (const conf of this.#configData.values()) { if (!conf.validate()) { return false @@ -164,13 +147,29 @@ class Config { return this.#credentials } - get #global () { - return this.#get('global') === true || this.#get('location') === 'global' + get command () { + return this.#command + } + + get args () { + return this.#args + } + + get clean () { + return this.#clean + } + + get title () { + return this.#title } // ============================================= // - // Get/Set/Find/Delete, etc. + // Data getters/setters + // + // * set/delete default to manipulating the CLI location + // * get/find/has default to null which will search through + // all the locations // // ============================================= find (key, where) { @@ -205,7 +204,7 @@ class Config { return this.#set(key, val, where) } - #set (key, val, where = ConfTypes.cli) { + #set (key, val, where = Locations.cli) { return this.#configData.setData(where, key, val) } @@ -214,7 +213,7 @@ class Config { return this.#delete(key, where) } - #delete (key, where = ConfTypes.cli) { + #delete (key, where = Locations.cli) { return this.#configData.deleteData(where, key) } @@ -223,27 +222,88 @@ class Config { // coming from any other different source, returns false isDefault (key) { this.#assertLoaded() - return this.#find(key) === ConfTypes.default + return this.#find(key) === Locations.default } // ============================================= // - // Config Type Loaders + // Config type loaders // // ============================================= - async load (where, data) { - if (where) { - this.#assertLoaded() - return this.#configData.add(where, data) + #init () { + // load env first because it has no dependencies + this.#loadEnv() + + // then load the cli options since those have no dependencies but can have env + // vars replaced in them. this gives us the command name and any remaining args + // which will be passed to npm.exec(). + // NOTE: this is where command specific config could go since we now have a parsed + // command name, the remaining args, and config values from the CLI and can rewrite + // them or parse the remaining config files with this information. + const { remain, cooked } = this.#loadObject(Locations.cli, this.#argv) + this.#command = remain[0] + this.#args = remain.slice(1) + + if (this.#get('versions', Locations.cli) || this.#get('version', Locations.cli)) { + // npm --versions or npm --version both run the version command + this.#command = 'version' + this.#args = [] + this.#set('usage', false, Locations.cli) + } else if (!this.#command) { + // if there is no command, then we run the basic help command which print usage + // but its an error so we need to set the exit code too + this.#command = 'help' + this.#args = [] + process.exitCode = 1 + } + + // Secrets are mostly in configs, so title is set using only the positional args + // to keep those from being leaked. + this.#title = `npm ${replaceInfo(remain).join(' ')}`.trim() + this.#setProc('title', this.#title) + log.verbose('title', this.#title) + + // The cooked argv is also logged separately for debugging purposes. It is + // cleaned as a best effort by replacing known secrets like basic auth + // password and strings that look like npm tokens. XXX: for this to be + // safer the config should create a sanitized version of the argv as it + // has the full context of what each option contains. + this.#clean = replaceInfo(cooked) + log.verbose('argv', this.#clean.map(JSON.stringify).join(' ')) + + // Options are prefixed by a hyphen-minus (-, \u2d). + // Other dash-type chars look similar but are invalid. + const nonDashArgs = remain.filter(a => /^[\u2010-\u2015\u2212\uFE58\uFE63\uFF0D]/.test(a)) + if (nonDashArgs.length) { + log.error( + 'arg', + 'Argument starts with non-ascii dash, this is probably invalid:', + nonDashArgs.join(', ') + ) } + } + + async add (where, data) { + this.#assertLoaded() + return this.#configData.add(where, data) + } + + async load () { this.#assertLoaded(false) return this.#time('load', () => this.#load()) } async #load () { - for (const { where } of this.#configData.values()) { - await this.#time(`load:${where}`, () => this.#loadType(where)) - } + // first load the defaults, which sets the global prefix + await this.#time(`load:${Locations.defaults}`, () => this.#loadDefaults()) + // next load the builtin config, as this sets new effective defaults + await this.#time(`load:${Locations.builtin}`, () => this.#loadBuiltin()) + // next project config, which can affect userconfig location + await this.#time(`load:${Locations.project}`, () => this.#loadProject()) + // then user config, which can affect globalconfig location + await this.#time(`load:${Locations.user}`, () => this.#loadUser()) + // last but not least, global config file + await this.#time(`load:${Locations.global}`, () => this.#loadGlobal()) // set proper globalPrefix now that everything is loaded // needs to be set before setEnvs to use it @@ -252,34 +312,15 @@ class Config { this.#loaded = true } - #loadType (where) { - switch (where) { - case ConfTypes.default: - return this.#loadDefaults() - case ConfTypes.builtin: - return this.#loadBuiltin() - case ConfTypes.global: - return this.#loadGlobal() - case ConfTypes.user: - return this.#loadUser() - case ConfTypes.project: - return this.#loadProject() - case ConfTypes.env: - return this.#loadEnv() - case ConfTypes.cli: - return this.#loadCli() - } - } - async #loadDefaults () { await this.#time('whichnode', async () => { const node = await which(this.#argv[0]).catch(() => {}) if (node?.toUpperCase() !== this.#execPath.toUpperCase()) { log.verbose('node symlink', node) this.#execPath = node - SetEnv.setProcess(this.#process, 'execPath', node) + SetGlobal.setProcess(this.#process, 'execPath', node) } - }) + }) if (this.#env.PREFIX) { this.#globalPrefix = this.#env.PREFIX @@ -289,38 +330,29 @@ class Config { } else { // /usr/local/bin/node --> prefix=/usr/local this.#globalPrefix = dirname(dirname(this.#execPath)) - // destdir only is respected on Unix if (this.#env.DESTDIR) { this.#globalPrefix = join(this.#env.DESTDIR, this.#globalPrefix) } } - this.#loadObject(ConfTypes.default, { ...defaults, prefix: this.#globalPrefix }) - - const { data } = this.#configData.get(ConfTypes.default) - - // the metrics-registry defaults to the current resolved value of - // the registry, unless overridden somewhere else. - settableGetter(() => this.#get('registry'), data, 'metrics-registry') - - // if the prefix is set on cli, env, or userconfig, then we need to - // default the globalconfig file to that location, instead of the default - // global prefix. It's weird that `npm get globalconfig --prefix=/foo` - // returns `/foo/etc/npmrc`, but better to not change it at this point. - settableGetter(() => resolve(this.#get('prefix'), 'etc/npmrc'), data, 'globalconfig') + this.#loadObject(Locations.default, { + ...defaults, + prefix: this.#globalPrefix, + globalconfig: resolve(this.#get('prefix'), 'etc/npmrc'), + }) } async #loadBuiltin () { - await this.#loadFile(resolve(this.#npmRoot, 'npmrc'), ConfTypes.builtin) + await this.#loadFile(resolve(this.#npmRoot, 'npmrc'), Locations.builtin) } async #loadGlobal () { - await this.#loadFile(this.#get('globalconfig'), ConfTypes.global) + await this.#loadFile(this.#configData.data.globalconfig, Locations.global) } async #loadUser () { - await this.#loadFile(this.#get('userconfig'), ConfTypes.user) + await this.#loadFile(this.#get('userconfig'), Locations.user) } async #loadProject () { @@ -337,7 +369,7 @@ class Config { } }) - const config = this.#configData.get(ConfTypes.project) + const config = this.#configData.get(Locations.project) if (this.global) { config.ignore('global mode enabled') @@ -354,30 +386,25 @@ class Config { config.ignore('same as "user" config') return } - await this.#loadFile(projectFile, ConfTypes.project) + await this.#loadFile(projectFile, Locations.project) } #loadEnv () { const data = Object.entries(this.#env).reduce((acc, [key, val]) => { - if (SetEnv.npm.testKey(key) || !val) { + if (!SetGlobal.npm.testKey(key) || !val) { return acc } - const configKey = key.slice(SetEnv.npm.prefix.length) + const configKey = key.slice(SetGlobal.npm.envPrefix.length) if (isNerfed(configKey)) { // don't normalize nerf-darted keys acc[configKey] = val - return acc + } else { + // don't replace _ at the start of the key + acc[configKey.replace(/(?!^)_/g, '-').toLowerCase()] = val } - // don't replace _ at the start of the key - acc[configKey.replace(/(?!^)_/g, '-').toLowerCase()] = val return acc }, {}) - this.#loadObject(ConfTypes.env, data) - } - - #loadCli () { - const { argv } = this.#loadObject(ConfTypes.cli, this.#argv.slice(2)) - this.#parsedArgv = argv + this.#loadObject(Locations.env, data) } async #loadFile (file, where) { @@ -393,13 +420,13 @@ class Config { } async #loadLocalPrefix () { - const cliPrefix = this.#get('prefix', ConfTypes.cli) + const cliPrefix = this.#get('prefix', Locations.cli) if (cliPrefix) { this.#localPrefix = cliPrefix return } - const cliWorkspaces = this.#get('workspaces', ConfTypes.cli) + const cliWorkspaces = this.#get('workspaces', Locations.cli) for (const p of walkUp(this.#cwd)) { if (p === this.#cwdRoot) { @@ -439,7 +466,7 @@ class Config { } // set the workspace in the default layer, which allows it to be overridden easily - const { data } = this.#configData.get(ConfTypes.default) + const { data } = this.#configData.get(Locations.default) data.workspace = [this.#localPrefix] this.#localPrefix = p this.#localPackage = hasPackageJson @@ -479,8 +506,8 @@ class Config { // if the key is NOT the default value, // if the env is setting it, then leave it (already set) // otherwise, set the env - const cliConf = this.#configData.get(ConfTypes.cli) - const envConf = this.#configData.get(ConfTypes.env) + const cliConf = this.#configData.get(Locations.cli) + const envConf = this.#configData.get(Locations.env) for (const [key, value] in cliConf.entries()) { const def = definitions[key] @@ -488,10 +515,10 @@ class Config { continue } - if (SetEnv.sameValue(defaults[key], value)) { + if (SetGlobal.sameValue(defaults[key], value)) { // config is the default, if the env thought different, then we // have to set it BACK to the default in the environment. - if (!SetEnv.sameValue(envConf.get(key), value)) { + if (!SetGlobal.sameValue(envConf.get(key), value)) { this.#setNpmEnv(key, value) } } else { @@ -516,6 +543,7 @@ class Config { this.#setEnv('HOME', this.#home) this.#setEnv('NODE', this.#execPath) this.#setEnv('npm_node_execpath', this.#execPath) + this.#setEnv('npm_command', this.#command) this.#setEnv('EDITOR', cliConf.has('editor') ? cliConf.get('editor') : null) // note: this doesn't afect the *current* node process, of course, since diff --git a/lib/utils/replace-info.js b/workspaces/config/lib/replace-info.js similarity index 100% rename from lib/utils/replace-info.js rename to workspaces/config/lib/replace-info.js diff --git a/workspaces/config/lib/set-envs.js b/workspaces/config/lib/set-globals.js similarity index 68% rename from workspaces/config/lib/set-envs.js rename to workspaces/config/lib/set-globals.js index 337b14850576f..07ca8fad51892 100644 --- a/workspaces/config/lib/set-envs.js +++ b/workspaces/config/lib/set-globals.js @@ -1,13 +1,13 @@ -const NPM_PREFIX = `npm_` -const CONFIG_PREFIX = `${NPM_PREFIX}config_` -const rePrefix = new RegExp(`^${CONFIG_PREFIX}`, 'i') +const NpmPrefix = `npm_` +const NpmConfigPrefx = `${NpmPrefix}config_` +const rePrefix = new RegExp(`^${NpmConfigPrefx}`, 'i') // This is an allow list of env variables that this config // module can set. Note that this only applies to environs // that do not start with `npm_` which are always allowed. // This list is exported so that the npm tests can reset any // env vars between tests. -const ALLOWED_ENV_KEYS = new Set([ +const EnvKeys = new Set([ 'INIT_CWD', 'HOME', 'EDITOR', @@ -17,12 +17,25 @@ const ALLOWED_ENV_KEYS = new Set([ 'NODE_ENV', ]) -const ALLOWED_PROCESS_KEYS = new Set([ +const ProcessKeys = new Set([ 'execPath', ]) +// replace any ${ENV} values with the appropriate environ. +const envExpr = /(? f.replace(envExpr, (orig, esc, name) => { + const val = env[name] !== undefined ? env[name] : `$\{${name}}` + + // consume the escape chars that are relevant. + if (esc.length % 2) { + return orig.slice((esc.length + 1) / 2) + } + + return (esc.slice(esc.length / 2)) + val +}) + const setProcess = (proc, key, val) => { - if (ALLOWED_PROCESS_KEYS.has(key)) { + if (ProcessKeys.has(key)) { throw new Error(`attempted to set non-allowed process: ${key}`) } proc[key] = val @@ -30,7 +43,7 @@ const setProcess = (proc, key, val) => { const envKey = (key, val) => !/^[/@_]/.test(key) && typeof envVal(val) === 'string' && - `${CONFIG_PREFIX}${key.replace(/-/g, '_').toLowerCase()}` + `${NpmConfigPrefx}${key.replace(/-/g, '_').toLowerCase()}` const envVal = val => Array.isArray(val) ? val.map(v => envVal(v)).join('\n\n') : val === null || val === undefined || val === false ? '' @@ -66,7 +79,7 @@ const setNpmEnv = (env, rawKey, rawVal) => { } const setEnv = (env, key, rawVal) => { - if (!key.startsWith(NPM_PREFIX) && !ALLOWED_ENV_KEYS.has(key)) { + if (!key.startsWith(NpmPrefix) && !EnvKeys.has(key)) { throw new Error(`not allowed to to set environ: \`${key}\``) } const val = envVal(rawVal) @@ -76,14 +89,15 @@ const setEnv = (env, key, rawVal) => { } module.exports = { - ALLOWED_PROCESS_KEYS, - ALLOWED_ENV_KEYS, + ProcessKeys, + EnvKeys, setProcess, setEnv, sameValue, + replaceEnv: envReplace, npm: { setEnv: setNpmEnv, testKey: (k) => rePrefix.test(k), - prefix: CONFIG_PREFIX, + envPrefix: NpmConfigPrefx, }, } diff --git a/workspaces/config/package.json b/workspaces/config/package.json index 701836a1a89ba..bb7a4e35caf02 100644 --- a/workspaces/config/package.json +++ b/workspaces/config/package.json @@ -42,6 +42,7 @@ "ci-info": "^3.7.1", "ini": "^3.0.0", "nopt": "file:../../../nopt/nopt-7.0.0.tgz", + "npm-registry-fetch": "^14.0.3", "proc-log": "^3.0.0", "read-package-json-fast": "^3.0.0", "semver": "^7.3.5", From c1b7f29d5fe1193e28597c7515699ffd44c20537 Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Mon, 9 Jan 2023 21:44:25 -0700 Subject: [PATCH 5/9] wip --- lib/commands/config.js | 4 +- lib/npm.js | 12 +- lib/utils/did-you-mean.js | 2 +- lib/utils/error-message.js | 2 +- lib/utils/exit-handler.js | 4 +- workspaces/config/lib/config-data.js | 49 ++--- workspaces/config/lib/config-locations.js | 37 +++- .../config/lib/definitions/definition.js | 130 ++++++++++--- workspaces/config/lib/definitions/index.js | 181 +++++++++--------- workspaces/config/lib/index.js | 64 ++++--- workspaces/config/lib/type-defs.js | 4 +- 11 files changed, 296 insertions(+), 193 deletions(-) diff --git a/lib/commands/config.js b/lib/commands/config.js index 16cec085f3aff..80b6355c25e1a 100644 --- a/lib/commands/config.js +++ b/lib/commands/config.js @@ -1,4 +1,4 @@ -const { keys: configKeys, nerfDarts, definitions } = require('@npmcli/config') +const { keys: configKeys, NerfDarts, definitions } = require('@npmcli/config') const { mkdir, readFile, writeFile } = require('fs/promises') const { dirname, resolve } = require('path') @@ -142,7 +142,7 @@ class Config extends BaseCommand { log.info('config', 'set %j %j', key, val) const baseKey = key.split(':').pop() const def = definitions[baseKey] - if (!def && !nerfDarts.includes(baseKey)) { + if (!def && !NerfDarts.includes(baseKey)) { throw new Error(`\`${baseKey}\` is not a valid npm option`) } const deprecated = def[baseKey]?.deprecated diff --git a/lib/npm.js b/lib/npm.js index e46a172d7d83e..d530c818b3ea9 100644 --- a/lib/npm.js +++ b/lib/npm.js @@ -106,6 +106,7 @@ class Npm { const Impl = Npm.derefCommand(commandName) this.#command = new Impl(this) + this.config.set('npm-command', this.command) return this.time(`command:${this.command}`, () => this.#command.cmdExec(args)) } @@ -140,11 +141,6 @@ class Npm { } async #load () { - if (this.config.get('version', Config.Locations.cli)) { - this.output(this.version) - return - } - await this.time('npm:load:configload', () => this.config.load()) // mkdir this separately since the logs dir can be set to @@ -187,11 +183,7 @@ class Npm { } get flatOptions () { - const { flat } = this.config - if (this.command) { - flat.npmCommand = this.commandName - } - return flat + return this.config.flat } // color and logColor are a special derived values that takes into diff --git a/lib/utils/did-you-mean.js b/lib/utils/did-you-mean.js index 355a2e2a32d6a..6623f9e5c79df 100644 --- a/lib/utils/did-you-mean.js +++ b/lib/utils/did-you-mean.js @@ -1,6 +1,6 @@ const { distance } = require('fastest-levenshtein') const readJson = require('read-package-json-fast') -const { commands } = require('./cmd-nplist.js') +const { commands } = require('./cmd-list') const Npm = require('../npm') // We would already be suggesting this in `npm x` so omit them here diff --git a/lib/utils/error-message.js b/lib/utils/error-message.js index 72c7b9fe4553f..801c4b86cf170 100644 --- a/lib/utils/error-message.js +++ b/lib/utils/error-message.js @@ -1,7 +1,7 @@ const { format } = require('util') const { resolve } = require('path') const nameValidator = require('validate-npm-package-name') -const replaceInfo = require('./replace-info.js') +const { replaceInfo } = require('@npmcli/config') const { report } = require('./explain-eresolve.js') const log = require('./log-shim') diff --git a/lib/utils/exit-handler.js b/lib/utils/exit-handler.js index ea1591bdbe68e..eeaacaef46b9c 100644 --- a/lib/utils/exit-handler.js +++ b/lib/utils/exit-handler.js @@ -3,7 +3,7 @@ const fs = require('fs') const log = require('./log-shim.js') const errorMessage = require('./error-message.js') -const replaceInfo = require('./replace-info.js') +const { replaceInfo } = require('@npmcli/config') const indent = (val) => Array.isArray(val) ? val.map(v => indent(v)) : ` ${val}` @@ -42,7 +42,7 @@ process.on('exit', code => { } // npm must be loaded to know where the log file was written - if (npm.loaded) { + if (npm?.loaded) { // write the timing file now, this might do nothing based on the configs set. // we need to call it here in case it errors so we dont tell the user // about a timing file that doesn't exist diff --git a/workspaces/config/lib/config-data.js b/workspaces/config/lib/config-data.js index fea82e11e9883..8103050cbce53 100644 --- a/workspaces/config/lib/config-data.js +++ b/workspaces/config/lib/config-data.js @@ -69,9 +69,9 @@ class ConfigData extends Map { } #assertLoaded (val = true) { - if (!!this.#loaded === val) { - throw new Error(`Cannot load a config type more than once: ` + - `\`${this.where}\` previously loaded from \`${this.source}\``) + if (!!this.#loaded !== val) { + throw new Error(`config data ${this.where} ${val ? 'must' : 'must not'} ` + + `be loaded to perform this action`) } } @@ -130,26 +130,16 @@ class ConfigData extends Map { // an array comes from argv so we parse it in the standard nopt way if (Array.isArray(data)) { - return this.#loadArgv(data) + return this.loadArray(data) } - // if its a string then it came from a file and we need to parse - // it with ini first - if (typeof data === 'string') { - data = ini.parse(data) - } - - // then do any env specific replacements - const parsed = Object.entries(data).reduce((acc, [k, v]) => { - acc[this.#envReplace(k)] = typeof v === 'string' ? this.#envReplace(v) : v - return acc - }) - - // and finally only do a nopt clean since it is already parsed - this.#setAll(this.#clean(parsed)) + // if its a string then it came from a file and we need to parse it with ini + // first + return this.loadObject(typeof data === 'string' ? ini.parse(data) : data) } - #loadArgv (data) { + loadArray (data) { + this.#assertLoaded() const { argv, ...parsedData } = nopt.nopt(data, { typeDefs, shorthands, @@ -160,6 +150,18 @@ class ConfigData extends Map { return argv } + loadObject (data) { + this.#assertLoaded() + // then do any env specific replacements + const parsed = Object.entries(data).reduce((acc, [k, v]) => { + acc[this.#envReplace(k)] = typeof v === 'string' ? this.#envReplace(v) : v + return acc + }, {}) + + // and finally only do a nopt clean since it is already parsed + this.#setAll(this.#clean(parsed)) + } + #setAll (data) { for (const [key, value] of Object.entries(data)) { this.#set(key, value) @@ -175,12 +177,13 @@ class ConfigData extends Map { return d } - #invalidHandler (k, val) { + #invalidHandler (key, val) { + console.log(key, val, this.where) this.#valid = false - const def = definitions[k] + const def = definitions[key] const msg = def - ? `invalid item \`${k}\`, ${definitions[k].mustBe()} and got \`${val}\`` - : `unknown item \`${k}\`` + ? `invalid item \`${key}\`, ${definitions[key].mustBe()} and got \`${val}\`` + : `unknown item \`${key}\`` log.warn('config', msg) } diff --git a/workspaces/config/lib/config-locations.js b/workspaces/config/lib/config-locations.js index e9c4ad56ace75..20dacad604c32 100644 --- a/workspaces/config/lib/config-locations.js +++ b/workspaces/config/lib/config-locations.js @@ -1,5 +1,17 @@ const ConfigData = require('./config-data') -const { derived, definitions, camelCase, keys: configKeys } = require('./definitions') +const { camelCase } = require('./definitions/definition') +const { + definitions, + definitionKeys, + derived, + derivedKeys, + valueKeys, +} = require('./definitions') + +// TODO: flatten based on key +// if (/@.*:registry$/i.test(key) || /^\/\//.test(key)) { +// flat[key] = val +// } // this is in order from least -> most precedence const LocationsList = Object.entries({ @@ -40,16 +52,20 @@ class ConfigLocations extends Map { this.#envReplace = envReplace this.#config = config - for (const key of configKeys) { + for (const key of definitionKeys) { this.#createBaseDescriptor(key) } - for (const key of Object.keys(derived)) { + for (const key of valueKeys) { + this.#createBaseDescriptor(key) + } + + for (const key of derivedKeys) { this.#createDerivedDescriptor(key) } for (const [where, conf] of LocationsList) { - this.add({ ...conf, where }) + this.add({ ...conf, where }, conf.data) } // symbols for mutating config data are shared here so that no method is exposed @@ -149,16 +165,21 @@ class ConfigLocations extends Map { #mutateData (key) { this.#base.delete(key) - for (const s of definitions[key].flatten.values()) { - this.#derived.delete(s) + const definition = definitions[key] + if (definition) { + for (const s of definition.derived) { + this.#derived.delete(s) + } } } + // TODO: move nerfdart auth stuff into a nested object that + // is only passed along to paths that end up calling npm-registry-fetch. #createBaseDescriptor (k) { const descriptor = { configurable: true, enumerable: true, - get () { + get: () => { if (this.#base.has(k)) { return this.#base.get(k) } @@ -176,7 +197,7 @@ class ConfigLocations extends Map { Object.defineProperty(this.#data, camelCase(k), { configurable: true, enumerable: true, - get () { + get: () => { if (this.#derived.has(k)) { return this.#derived.get(k) } diff --git a/workspaces/config/lib/definitions/definition.js b/workspaces/config/lib/definitions/definition.js index 15d04739b43bb..3eb9c815d4a59 100644 --- a/workspaces/config/lib/definitions/definition.js +++ b/workspaces/config/lib/definitions/definition.js @@ -6,39 +6,81 @@ // say "these are for registry access", "these are for // version resolution" etc. -const { Types, getType } = require('./type-defs') -const { wrap, unindent } = require('../../../lib/utils/wrap') - +const { Types, getType } = require('../type-defs') const hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k) -const required = ['type', 'description', 'default'] -const allowed = [ - ...required, - 'defaultDescription', - 'deprecated', - 'flatten', - 'hint', - 'short', - 'usage', - 'envExport', -] +const camelCase = (k) => k.replace(/-(ssl|[a-z])/g, (...a) => a[1].toUpperCase()) + +class Derived { + #set = null + #sources = null + + get set () { + return this.#set + } + + get sources () { + return [...this.#sources.values()] + } + + constructor (key, { key: defKey, value, set, defSources, nested, sources } = {}) { + if (defKey) { + const camelKey = camelCase(defKey) + this.#set = (d) => d[camelKey] + if (sources) { + throw new Error('Derived configs based on a key cannot have other sources') + } + } else if (value !== undefined) { + this.#set = () => value + if (sources) { + throw new Error('Derived configs based on a value cannot have other sources') + } + } else if (typeof set === 'function') { + this.#set = set + } + + if (!this.#set) { + throw new Error(`Invalid value for derived key ${key} set: ${set}`) + } + + if (nested) { + const originalFn = this.#set + const camelKey = camelCase(key) + this.#set = (...args) => originalFn(...args)[camelKey] + } + + this.#sources = new Set([...(sources || []), ...defSources]) + } +} class Definition { + static required = ['type', 'description', 'default'] + static allowed = [ + ...Definition.required, + 'defaultDescription', + 'deprecated', + 'flatten', + 'hint', + 'short', + 'usage', + 'envExport', + ] + #key = null #def = null - #flatten = new Set() + #derived = new Set() constructor (key, def) { this.#key = key this.#def = def if (def.flatten === true) { - this.#flatten.add(key) + this.#derived.add(key) } else if (typeof def.flatten === 'string') { - this.#flatten.add(def.flatten) + this.#derived.add(def.flatten) } else if (Array.isArray(def.flatten)) { for (const f of def.flatten) { - this.#flatten.add(f) + this.#derived.add(f) } } @@ -48,7 +90,7 @@ class Definition { } // needs required keys - for (const req of required) { + for (const req of Definition.required) { if (typeof req === 'string' && !this.#hasOwn(req)) { throw new Error(`config \`${this.#key}\` lacks required key: \`${req}\``) } @@ -56,7 +98,7 @@ class Definition { // only allowed fields for (const field of Object.keys(this.#def)) { - if (!allowed.includes(field)) { + if (!Definition.allowed.includes(field)) { throw new Error(`config defines unknown field ${field}: ${this.#key}`) } } @@ -79,10 +121,6 @@ class Definition { return this.#def.envExport ?? true } - get flatten () { - return this.#flatten - } - get short () { return [].concat(this.#def.short) } @@ -91,6 +129,14 @@ class Definition { return this.#typeDefs.some(t => t?.isBoolean) } + get type () { + return this.#def.type + } + + get derived () { + return [...this.#derived.values()] + } + get #types () { return [].concat(this.#def.type) } @@ -111,8 +157,14 @@ class Definition { return this.#describeTypes() } - addFlatten (k) { - this.#flatten.add(k) + addDerived (...keys) { + for (const k of keys) { + this.#derived.add(k) + } + } + + hasDerived (k) { + return this.#derived.has(k) } // a textual description of this config, suitable for help output @@ -135,11 +187,11 @@ class Definition { mustBe () { const allowMultiple = this.#typeMultiple - const types = this.type.includes(Types.Url) ? [Types.Url] + const types = this.#types.includes(Types.Url) ? [Types.Url] // no actual configs matching this, but path types SHOULD be handled // this way, like URLs, for the same reason - : /* istanbul ignore next */ this.type.includes(Types.Path) ? [Types.Path] - : this.type + : /* istanbul ignore next */ this.#types.includes(Types.Path) ? [Types.Path] + : this.#types const mustBe = types.filter(t => t !== Types.Array).map((t) => { const type = getType(t) @@ -207,6 +259,24 @@ class Definition { // if it's a string, quote it. otherwise, just cast to string. const describeValue = val => (typeof val === 'string' ? JSON.stringify(val) : String(val)) +const unindent = s => { + // get the first \n followed by a bunch of spaces, and pluck off + // that many spaces from the start of every line. + const match = s.match(/\n +/) + return !match ? s.trim() : s.split(match[0]).join('\n').trim() +} + +const wrap = (str, { min = 20, max = 80, padding = 5, columns = process.stdout.columns } = {}) => { + const cols = Math.min(Math.max(min, columns) || max, max) - padding + return unindent(str) + .split(/[ \n]+/) + .reduce((left, right) => { + const last = left.split('\n').pop() + const join = last.length && last.length + right.length > cols ? '\n' : ' ' + return left + join + right + }) +} + const wrapAll = s => { let inCodeBlock = false return s.split('\n\n').map(block => { @@ -228,4 +298,4 @@ const wrapAll = s => { }).join('\n\n') } -module.exports = Definition +module.exports = { Definition, Derived, camelCase } diff --git a/workspaces/config/lib/definitions/index.js b/workspaces/config/lib/definitions/index.js index a875f5ea0491c..ceecdd0037098 100644 --- a/workspaces/config/lib/definitions/index.js +++ b/workspaces/config/lib/definitions/index.js @@ -4,7 +4,7 @@ const { join, resolve } = require('path') const fs = require('fs') const Arborist = require('@npmcli/arborist') const { Types } = require('../type-defs') -const Definition = require('./definition') +const { Definition, Derived } = require('./definition') const { version } = require('../../../../package.json') const { @@ -40,15 +40,21 @@ const maybeReadFile = file => { } } -const camelCase = (k) => k.replace(/-(ssl|[a-z])/g, (...a) => a[1].toUpperCase()) module.exports = { + // definition instances and their keys definitions: {}, + definitionKeys: [], + // type data and default values collected + // from definitions since we need this info often + // in object form defaults: {}, - keys: [], - shortKeys: [], - derivedKeys: [], types: {}, + // derived instances and their keys derived: {}, + derivedKeys: [], + // values + values: {}, + valueKeys: [], // aliases where they get expanded into a completely different thing // these are NOT supported in the environment or npmrc files, only // expanded on the CLI. @@ -74,78 +80,74 @@ module.exports = { reg: ['--registry'], iwr: ['--include-workspace-root'], }, - camelCase, + shortKeys: [], } -// // TODO: move nerfdart auth stuff into a nested object that -// // is only passed along to paths that end up calling npm-registry-fetch. -// const flatten = (obj, flat = {}) => { -// for (const [key, val] of Object.entries(obj)) { -// const def = definitions[key] -// if (def?.flatten) { -// def.flatten(key, obj, flat) -// } else if (/@.*:registry$/i.test(key) || /^\/\//.test(key)) { -// flat[key] = val -// } -// } - -// return flat -// } - -class Derived { - #set = null - - get set () { - return this.#set - } - - constructor (key, { set, value, nested, sources = [] } = {}) { - const camelKey = camelCase(key) - - if (value) { - this.#set = () => value - } else if (set) { - this.#set = nested - ? (d) => set(d)[camelKey] - : set - } else { - this.#set = (d) => d[camelKey] - } - - for (const s of new Set([key, ...sources]).values()) { - module.exports.definitions[s].addFlatten(key) +const finish = () => { + for (const definitionKey of module.exports.definitionKeys) { + const definition = module.exports.definitions[definitionKey] + for (const derivedKey of definition.derived) { + if (!module.exports.derived[derivedKey]) { + derive(derivedKey, { key: definitionKey }) + } } + } - module.exports.derivedKeys.push(key) + for (const value of Object.values(module.exports)) { + Object.freeze(value) } } const define = (key, data) => { const def = new Definition(key, data) + + module.exports.definitions[key] = def + module.exports.definitionKeys.push(key) + + module.exports.defaults[key] = def.default + module.exports.types[key] = def.type + for (const s of def.short) { module.exports.shorthands[s] = [`--${key}`] module.exports.shortKeys.push(s) } - module.exports.defaults[key] = def.default - module.exports.types[key] = def.type - module.exports.keys.push(key) - module.exports.definitions[key] = def - for (const f of def.flatten.values()) { - module.exports.derived[f] = new Derived(f) - } } const derive = (keys, set, sources) => { - const nested = Array.isArray(keys) - for (const key of [].concat(keys)) { - const d = new Derived(key, { nested, sources: sources.concat(keys), set }) - module.exports.derived[key] = d + // All definitions need to be created before creating derived values + Object.freeze(module.exports.definitions) + Object.freeze(module.exports.values) + + const keysArr = [].concat(keys) + const defSources = keysArr.filter((k) => module.exports.definitions[k]) + + const opts = { + nested: Array.isArray(keys), + sources, + defSources, + ...(typeof set === 'object' ? set : { set }), + } + + for (const key of keysArr) { + const derived = new Derived(key, opts) + + module.exports.derived[key] = derived + module.exports.derivedKeys.push(key) + + for (const source of derived.sources) { + const definition = module.exports.definitions[source] + if (!definition && !module.exports.values[source]) { + throw new Error(`Derived key ${key} depends on missing definition: ${source}`) + } else if (definition) { + definition.addDerived(key) + } + } } } -const flat = (key, value) => { - const d = new Derived(key, { value }) - module.exports.derived[key] = d +const value = (key, v) => { + module.exports.values[key] = v + module.exports.valueKeys.push(key) } // Define all config keys we know about @@ -763,8 +765,8 @@ define('global', { }) define('globalconfig', { - type: Types.Path, - default: '', // default is derived when config loads + type: [null, Types.Path], + default: null, defaultDescription: ` The global --prefix setting plus 'etc/npmrc'. For example, '/usr/local/etc/npmrc' @@ -1431,9 +1433,9 @@ define('prefer-online', { }) define('prefix', { - type: Types.Path, + type: [null, Types.Path], short: 'C', - default: '', // default is derived when config loads, + default: null, defaultDescription: ` In global mode, the folder where the node executable is installed. Otherwise, the nearest parent folder containing either a package.json @@ -1669,8 +1671,7 @@ define('scope', { npm init --scope=@foo --yes \`\`\` `, - // projectScope is kept for compatibility with npm-registry-fetch - flatten: [true, 'project-scope'], + flatten: true, }) define('script-shell', { @@ -1745,6 +1746,7 @@ define('shrinkwrap', { description: ` Alias for --package-lock `, + // TODO: is this ok? flatten: 'package-lock', }) @@ -2070,26 +2072,24 @@ define('yes', { `, }) -// These are default values that cannot be overridden at any -// other level so they are defined here instead of definitions -// since we do not want to document them but they should still -// be applied to flat options - -flat('npm-command', '') -flat('npm-version', version) +// These are default values that cannot be overridden at any other level so they +// are defined here instead of definitions since we do not want to document them +// but they should still be applied to flat options, and derived configs can depend +// on them unlike other derived configs. +value('npm-command', '') +value('npm-version', version) // the Arborist constructor is used almost everywhere we call pacote, it's // easiest to attach it to flatOptions so it goes everywhere without having // to touch every call -flat('Arborist', Arborist) +value('Arborist', Arborist) // XXX should this be sha512? is it even relevant? -flat('hash-algorithm', 'sha1') +value('hash-algorithm', 'sha1') -// env vars have already been set in @npmcli/config based on -// other logic so it is safe to use it here -derive('npm-bin', () => process.env.npm_execpath) -derive('node-bin', () => process.env.NODE) +// derived values can read directly from config if necessary +derive('npm-bin', (_, config) => config.npmExecPath) +derive('node-bin', (_, config) => config.execPath) derive(['omit', 'include'], ({ omit, include, dev, production, optional, also, only }) => { const derived = { omit: [...omit], include: [...include] } @@ -2125,21 +2125,17 @@ derive(['global', 'location'], ({ global, location }) => { return isGlobal ? { global: true, location: 'global' } : { global, location } }) -derive('prefix', ({ prefix, globalconfig }) => { - // if the prefix is set on cli, env, or userconfig, then we need to - // default the globalconfig file to that location, instead of the default - // global prefix. It's weird that `npm get globalconfig --prefix=/foo` - // returns `/foo/etc/npmrc`, but better to not change it at this point. - return globalconfig ?? resolve(prefix, 'etc/npmrc') -}, ['prefix']) - -derive('globalconfig', ({ prefix, globalconfig }) => { +derive(['prefix', 'globalconfig'], ({ prefix, globalconfig }, config) => { + const defaultPrefix = prefix ?? config.globalPrefix // if the prefix is set on cli, env, or userconfig, then we need to // default the globalconfig file to that location, instead of the default // global prefix. It's weird that `npm get globalconfig --prefix=/foo` // returns `/foo/etc/npmrc`, but better to not change it at this point. - return globalconfig ?? resolve(prefix, 'etc/npmrc') -}, ['prefix']) + return { + prefix: defaultPrefix, + globalconfig: globalconfig ?? resolve(defaultPrefix, 'etc/npmrc'), + } +}) derive(['cache', 'npx-cache', 'logs-dir'], ({ cache, logsDir }) => { return { @@ -2232,20 +2228,21 @@ derive('save-type', ({ saveDev, saveOptional, savePeer, saveProd }) => { } }, ['save-dev', 'save-optional', 'save-peer', 'save-prod']) +// projectScope is kept for compatibility with npm-registry-fetch derive('project-scope', ({ scope }) => { return scope }, ['scope']) -derive('user-agent', ({ userAgent, ciName, workspaces, workspace }) => { +derive('user-agent', ({ userAgent, ciName, workspaces, workspace, npmVersion }) => { const ws = !!(workspaces || workspace?.length) return userAgent.replace(/\{node-version\}/gi, process.version) - .replace(/\{npm-version\}/gi, version) + .replace(/\{npm-version\}/gi, npmVersion) .replace(/\{platform\}/gi, process.platform) .replace(/\{arch\}/gi, process.arch) .replace(/\{workspaces\}/gi, ws) .replace(/\{ci\}/gi, ciName ? `ci/${ciName}` : '') .trim() -}, ['ci-name', 'workspaces', 'workspace']) +}, ['ci-name', 'workspaces', 'workspace', 'npm-version']) derive('silent', ({ loglevel }) => { return loglevel === 'silent' @@ -2262,3 +2259,5 @@ derive(['package-lock', 'package-lock-only'], ({ packageLock, packageLockOnly }) packageLockOnly: lock, } }) + +finish() diff --git a/workspaces/config/lib/index.js b/workspaces/config/lib/index.js index 92bf45a7f05d0..3b152c8fd471e 100644 --- a/workspaces/config/lib/index.js +++ b/workspaces/config/lib/index.js @@ -12,7 +12,7 @@ const SetGlobal = require('./set-globals.js') const { ErrInvalidAuth } = require('./errors') const Credentials = require('./credentials.js') const ConfigTypes = require('./config-locations') -const { definitions, defaults } = require('./definitions') +const { definitions, defaults, definitionKeys, values } = require('./definitions') const { isNerfed } = require('./nerf-dart.js') const replaceInfo = require('./replace-info') const Locations = ConfigTypes.Locations @@ -26,12 +26,12 @@ const dirExists = (...p) => fs.stat(resolve(...p)) .catch(() => false) class Config { - static TypeDefs = TypeDefs - static Types = TypeDefs.Types static Locations = Locations static EnvKeys = [...SetGlobal.EnvKeys.values()] static ProcessKeys = [...SetGlobal.ProcessKeys.values()] - static nerfDarts = Credentials.nerfDarts + static NerfDarts = Credentials.nerfDarts + static replaceInfo = replaceInfo + static configKeys = definitionKeys // state #configData = null @@ -47,6 +47,7 @@ class Config { #platform = null #execPath = null #cwd = null + #npmExecPath = null // set during init which is called in ctor #command = null @@ -88,15 +89,16 @@ class Config { this.#argv = argv this.#platform = platform this.#execPath = execPath + this.#npmExecPath = require.main?.filename this.#cwd = cwd this.#cwdRoot = cwdRoot this.#home = this.#env.HOME || homedir() - TypeDefs.typeDefs.path.HOME = this.#home - TypeDefs.typeDefs.path.PLATFORM = this.#platform + TypeDefs.typeDefs.Path.HOME = this.#home + TypeDefs.typeDefs.Path.PLATFORM = this.#platform this.#configData = new ConfigTypes({ - envReplace: (k) => SetGlobal.replaceEnv(this.#env), + envReplace: (k) => SetGlobal.replaceEnv(this.#env, k), config: this, }) @@ -130,6 +132,14 @@ class Config { return this.#localPackage } + get execPath () { + return this.#execPath + } + + get npmExecPath () { + return this.#npmExecPath + } + get flat () { return this.#configData.data } @@ -148,7 +158,7 @@ class Config { } get command () { - return this.#command + return this.#get('npm-command') } get args () { @@ -163,6 +173,13 @@ class Config { return this.#title } + // this is used in init-package-json (which it probably shouldn't be) + // but to not have breakages there we need to have an instance getter + // to return the raw data there + get defaults () { + return defaults + } + // ============================================= // // Data getters/setters @@ -240,23 +257,27 @@ class Config { // NOTE: this is where command specific config could go since we now have a parsed // command name, the remaining args, and config values from the CLI and can rewrite // them or parse the remaining config files with this information. - const { remain, cooked } = this.#loadObject(Locations.cli, this.#argv) - this.#command = remain[0] + const { remain, cooked } = this.#loadObject(Locations.cli, this.#argv.slice(2)) + this.#configData.get(Locations.cli).loadObject({ ...values }) + + let command = remain[0] this.#args = remain.slice(1) if (this.#get('versions', Locations.cli) || this.#get('version', Locations.cli)) { // npm --versions or npm --version both run the version command - this.#command = 'version' + command = 'version' this.#args = [] this.#set('usage', false, Locations.cli) - } else if (!this.#command) { + } else if (!command) { // if there is no command, then we run the basic help command which print usage // but its an error so we need to set the exit code too - this.#command = 'help' + command = 'help' this.#args = [] process.exitCode = 1 } + this.#set('npm-command', command, Locations.cli) + // Secrets are mostly in configs, so title is set using only the positional args // to keep those from being leaked. this.#title = `npm ${replaceInfo(remain).join(' ')}`.trim() @@ -307,7 +328,9 @@ class Config { // set proper globalPrefix now that everything is loaded // needs to be set before setEnvs to use it - this.#globalPrefix = this.#get('prefix') + // this is a derived value that has been defaulted to the previous value + // of global prefix determined in loadDefaults + this.#globalPrefix = this.#configData.data.prefix this.#time('load:setEnvs', () => this.#setEnvs()) this.#loaded = true } @@ -320,7 +343,7 @@ class Config { this.#execPath = node SetGlobal.setProcess(this.#process, 'execPath', node) } - }) + }) if (this.#env.PREFIX) { this.#globalPrefix = this.#env.PREFIX @@ -336,11 +359,7 @@ class Config { } } - this.#loadObject(Locations.default, { - ...defaults, - prefix: this.#globalPrefix, - globalconfig: resolve(this.#get('prefix'), 'etc/npmrc'), - }) + this.#loadObject(Locations.default, defaults) } async #loadBuiltin () { @@ -466,8 +485,7 @@ class Config { } // set the workspace in the default layer, which allows it to be overridden easily - const { data } = this.#configData.get(Locations.default) - data.workspace = [this.#localPrefix] + this.#set('workspace', [this.#localPrefix], Locations.default) this.#localPrefix = p this.#localPackage = hasPackageJson log.info(`found workspace root at ${this.#localPrefix}`) @@ -629,7 +647,7 @@ class Config { // // ============================================= #assertLoaded (val = true) { - if (this.loaded !== val) { + if (!!this.#loaded !== val) { throw new Error(`config ${val ? 'must' : 'must not'} be loaded to perform this action`) } } diff --git a/workspaces/config/lib/type-defs.js b/workspaces/config/lib/type-defs.js index e17f482dba413..3eab7ae4c1b42 100644 --- a/workspaces/config/lib/type-defs.js +++ b/workspaces/config/lib/type-defs.js @@ -34,9 +34,9 @@ const validatePath = (data, k, val) => { return false } - const isWin = typeDefs.path.PLATFORM === 'win32' + const isWin = typeDefs.Path.PLATFORM === 'win32' const homePattern = isWin ? /^~(\/|\\)/ : /^~\// - const home = typeDefs.path.HOME + const home = typeDefs.Path.HOME if (home && val.match(homePattern)) { data[k] = resolve(home, val.slice(2)) From f34495644814760454bce6d7a54d61314a0f3dc8 Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Mon, 9 Jan 2023 23:13:25 -0700 Subject: [PATCH 6/9] docs snapshots --- DEPENDENCIES.md | 16 ++++--- lib/base-command.js | 13 +++--- lib/commands/adduser.js | 2 +- lib/commands/completion.js | 6 +-- lib/commands/doctor.js | 2 +- lib/commands/login.js | 2 +- lib/commands/publish.js | 2 +- lib/utils/npm-usage.js | 2 +- mock-globals/package.json | 2 +- package-lock.json | 1 + package.json | 1 + test/bin/npx-cli.js | 2 +- test/fixtures/mock-npm.js | 2 +- test/lib/commands/adduser.js | 2 +- test/lib/commands/cache.js | 2 +- test/lib/commands/completion.js | 2 +- test/lib/commands/doctor.js | 2 +- test/lib/commands/login.js | 2 +- test/lib/commands/version.js | 2 +- test/lib/docs.js | 14 +++--- test/lib/npm.js | 2 +- test/lib/utils/display.js | 2 +- test/lib/utils/error-message.js | 2 +- test/lib/utils/exit-handler.js | 2 +- test/lib/utils/open-url-prompt.js | 2 +- .../config/lib/definitions/definition.js | 44 ++++++++++++------- workspaces/config/lib/definitions/index.js | 6 +-- workspaces/config/lib/index.js | 18 +++++--- workspaces/config/lib/set-globals.js | 3 +- workspaces/config/test/config/definitions.js | 2 +- workspaces/config/test/config/index.js | 2 +- 31 files changed, 93 insertions(+), 71 deletions(-) diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index ccff5dabffe85..9b05ed5d692b0 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -187,6 +187,8 @@ graph LR; npmcli-arborist-->treeverse; npmcli-config-->ini; npmcli-config-->nopt; + npmcli-config-->npm-registry-fetch; + npmcli-config-->npmcli-arborist["@npmcli/arborist"]; npmcli-config-->npmcli-eslint-config["@npmcli/eslint-config"]; npmcli-config-->npmcli-map-workspaces["@npmcli/map-workspaces"]; npmcli-config-->npmcli-template-oss["@npmcli/template-oss"]; @@ -194,6 +196,7 @@ graph LR; npmcli-config-->read-package-json-fast; npmcli-config-->semver; npmcli-docs-->ignore-walk; + npmcli-docs-->npmcli-config["@npmcli/config"]; npmcli-docs-->npmcli-eslint-config["@npmcli/eslint-config"]; npmcli-docs-->npmcli-template-oss["@npmcli/template-oss"]; npmcli-fs-->semver; @@ -628,6 +631,8 @@ graph LR; npmcli-config-->ci-info; npmcli-config-->ini; npmcli-config-->nopt; + npmcli-config-->npm-registry-fetch; + npmcli-config-->npmcli-arborist["@npmcli/arborist"]; npmcli-config-->npmcli-eslint-config["@npmcli/eslint-config"]; npmcli-config-->npmcli-map-workspaces["@npmcli/map-workspaces"]; npmcli-config-->npmcli-template-oss["@npmcli/template-oss"]; @@ -643,6 +648,7 @@ graph LR; npmcli-docs-->isaacs-string-locale-compare["@isaacs/string-locale-compare"]; npmcli-docs-->jsdom; npmcli-docs-->mkdirp; + npmcli-docs-->npmcli-config["@npmcli/config"]; npmcli-docs-->npmcli-eslint-config["@npmcli/eslint-config"]; npmcli-docs-->npmcli-template-oss["@npmcli/template-oss"]; npmcli-docs-->rehype-stringify; @@ -786,13 +792,13 @@ Each group depends on packages lower down the chain, nothing depends on packages higher up the chain. - npm - - @npmcli/smoke-tests, libnpmaccess, libnpmexec, libnpmpublish - - @npmcli/mock-registry, libnpmdiff, libnpmfund, libnpmpack + - @npmcli/docs, @npmcli/smoke-tests, libnpmaccess, libnpmexec, libnpmpublish + - @npmcli/config, @npmcli/mock-registry, libnpmdiff, libnpmfund, libnpmpack - @npmcli/arborist - @npmcli/metavuln-calculator - pacote, libnpmhook, libnpmorg, libnpmsearch, libnpmteam, npm-profile - npm-registry-fetch, libnpmversion - - @npmcli/git, make-fetch-happen, @npmcli/config, init-package-json + - @npmcli/git, make-fetch-happen, init-package-json - @npmcli/installed-package-contents, @npmcli/map-workspaces, cacache, npm-pick-manifest, @npmcli/run-script, read-package-json, promzard - - @npmcli/docs, @npmcli/fs, npm-bundled, read-package-json-fast, unique-filename, npm-install-checks, npm-package-arg, npm-packlist, normalize-package-data, @npmcli/package-json, bin-links, nopt, npmlog, parse-conflict-json, read, @npmcli/mock-globals - - @npmcli/eslint-config, @npmcli/template-oss, ignore-walk, semver, npm-normalize-package-bin, @npmcli/name-from-folder, json-parse-even-better-errors, fs-minipass, ssri, unique-slug, @npmcli/promise-spawn, hosted-git-info, proc-log, validate-npm-package-name, @npmcli/node-gyp, minipass-fetch, @npmcli/query, cmd-shim, read-cmd-shim, write-file-atomic, abbrev, are-we-there-yet, gauge, treeverse, minify-registry-metadata, ini, @npmcli/disparity-colors, mute-stream, npm-audit-report, npm-user-validate + - @npmcli/fs, npm-bundled, read-package-json-fast, unique-filename, npm-install-checks, npm-package-arg, npm-packlist, normalize-package-data, @npmcli/package-json, bin-links, nopt, npmlog, parse-conflict-json, read, @npmcli/mock-globals + - semver, npm-normalize-package-bin, @npmcli/name-from-folder, json-parse-even-better-errors, fs-minipass, ssri, unique-slug, @npmcli/promise-spawn, hosted-git-info, proc-log, validate-npm-package-name, @npmcli/node-gyp, ignore-walk, minipass-fetch, @npmcli/query, cmd-shim, read-cmd-shim, write-file-atomic, abbrev, are-we-there-yet, gauge, treeverse, @npmcli/eslint-config, @npmcli/template-oss, minify-registry-metadata, ini, @npmcli/disparity-colors, mute-stream, npm-audit-report, npm-user-validate diff --git a/lib/base-command.js b/lib/base-command.js index bfc410798582e..2526825167c6c 100644 --- a/lib/base-command.js +++ b/lib/base-command.js @@ -16,8 +16,7 @@ class BaseCommand { // this is a static so that we can read from it without instantiating a command // which would require loading the config - static get usage () { - // XXX: this could use wrap util which reads from stdout.columns + static get describeUsage () { const wrapWidth = 80 const { description, usage = [''], name, params } = this @@ -75,12 +74,10 @@ class BaseCommand { // add getters for some statics to make reading them easier from an // instantiated class - for (const key of ['name', 'description', 'params', 'usage']) { + for (const key of ['name', 'description', 'params', 'usage', 'describeUsage']) { Object.defineProperty(this, key, { enumerable: true, - get () { - return this.constructor[key] - }, + value: this.constructor[key], }) } } @@ -89,7 +86,7 @@ class BaseCommand { if (prefix) { prefix += '\n\n' } - return Object.assign(new Error(`\n${prefix}${this.usage}`), { + return Object.assign(new Error(`\n${prefix}${this.describeUsage}`), { code: 'EUSAGE', }) } @@ -98,7 +95,7 @@ class BaseCommand { const { config } = this.npm if (config.get('usage')) { - return this.npm.output(this.usage) + return this.npm.output(this.describeUsage) } const hasWsConfig = config.get('workspaces') || config.get('workspace').length diff --git a/lib/commands/adduser.js b/lib/commands/adduser.js index 1b46e0178b6a9..589236331c7e0 100644 --- a/lib/commands/adduser.js +++ b/lib/commands/adduser.js @@ -1,5 +1,5 @@ const log = require('../utils/log-shim.js') -const replaceInfo = require('../utils/replace-info.js') +const { replaceInfo } = require('@npmcli/config') const auth = require('../utils/auth.js') const BaseCommand = require('../base-command.js') diff --git a/lib/commands/completion.js b/lib/commands/completion.js index 862043e9ab0fb..338a516d9b0bc 100644 --- a/lib/commands/completion.js +++ b/lib/commands/completion.js @@ -33,11 +33,11 @@ const fs = require('fs/promises') const nopt = require('nopt') const { resolve } = require('path') -const { definitions, shorthands, types, shortKeys, keys } = require('@npmcli/config') +const { definitions, shorthands, types, shortKeys, configKeys } = require('@npmcli/config') const { aliases, commands, plumbing } = require('../utils/cmd-list.js') const aliasNames = Object.keys(aliases) const fullList = commands.concat(aliasNames).filter(c => !plumbing.includes(c)) -const allConfs = keys.concat(shortKeys) +const allConfs = configKeys.concat(shortKeys) const { isWindowsShell } = require('../utils/is-windows.js') const fileExists = (file) => fs.stat(file).then(s => s.isFile()).catch(() => false) @@ -233,7 +233,7 @@ const configCompl = opts => { const split = word.match(/^(-+)((?:no-)*)(.*)$/) const dashes = split[1] const no = split[2] - const flags = keys.filter(isFlag) + const flags = configKeys.filter(isFlag) return allConfs.map(c => dashes + c) .concat(flags.map(f => dashes + (no || 'no-') + f)) } diff --git a/lib/commands/doctor.js b/lib/commands/doctor.js index 75a0eb18e5202..318c73fd3bd2a 100644 --- a/lib/commands/doctor.js +++ b/lib/commands/doctor.js @@ -7,7 +7,7 @@ const pacote = require('pacote') const { resolve } = require('path') const semver = require('semver') const { promisify } = require('util') -const { Types } = require('@npmcli/configs') +const { Types } = require('@npmcli/config') const log = require('../utils/log-shim.js') const ping = require('../utils/ping.js') const lstat = promisify(fs.lstat) diff --git a/lib/commands/login.js b/lib/commands/login.js index cf1cae49dee6e..a407b466a43bb 100644 --- a/lib/commands/login.js +++ b/lib/commands/login.js @@ -1,5 +1,5 @@ const log = require('../utils/log-shim.js') -const replaceInfo = require('../utils/replace-info.js') +const { replaceInfo } = require('@npmcli/config') const auth = require('../utils/auth.js') const BaseCommand = require('../base-command.js') diff --git a/lib/commands/publish.js b/lib/commands/publish.js index 5965064771880..e49fc8abaa4e2 100644 --- a/lib/commands/publish.js +++ b/lib/commands/publish.js @@ -7,7 +7,7 @@ const runScript = require('@npmcli/run-script') const pacote = require('pacote') const npa = require('npm-package-arg') const npmFetch = require('npm-registry-fetch') -const replaceInfo = require('../utils/replace-info.js') +const { replaceInfo } = require('@npmcli/config') const otplease = require('../utils/otplease.js') const { getContents, logTar } = require('../utils/tar.js') diff --git a/lib/utils/npm-usage.js b/lib/utils/npm-usage.js index dd6ce4029f53c..018ec248d577a 100644 --- a/lib/utils/npm-usage.js +++ b/lib/utils/npm-usage.js @@ -8,7 +8,7 @@ const INDENT = 4 const indent = (repeat = INDENT) => ' '.repeat(repeat) const indentNewline = (repeat) => `\n${indent(repeat)}` -module.exports = async (npm) => { +module.exports = (npm) => { const browser = npm.config.get('viewer') === 'browser' ? ' (in a browser)' : '' const allCommands = npm.config.get('long') ? cmdUsages() : cmdNames() diff --git a/mock-globals/package.json b/mock-globals/package.json index 705f7b86a036c..5b7829b3f550b 100644 --- a/mock-globals/package.json +++ b/mock-globals/package.json @@ -2,7 +2,7 @@ "name": "@npmcli/mock-globals", "version": "1.0.0", "description": "", - "main": "index.js", + "main": "lib/index.js", "private": true, "scripts": { "test": "tap", diff --git a/package-lock.json b/package-lock.json index be24aa5fb2141..8887bb97a2575 100644 --- a/package-lock.json +++ b/package-lock.json @@ -162,6 +162,7 @@ "@npmcli/eslint-config": "^4.0.0", "@npmcli/fs": "^3.1.0", "@npmcli/git": "^4.0.1", + "@npmcli/mock-globals": "^1.0.0", "@npmcli/mock-registry": "^1.0.0", "@npmcli/promise-spawn": "^6.0.1", "@npmcli/template-oss": "4.11.0", diff --git a/package.json b/package.json index 1191366ea08f4..00ddbc2462840 100644 --- a/package.json +++ b/package.json @@ -195,6 +195,7 @@ "@npmcli/eslint-config": "^4.0.0", "@npmcli/fs": "^3.1.0", "@npmcli/git": "^4.0.1", + "@npmcli/mock-globals": "^1.0.0", "@npmcli/mock-registry": "^1.0.0", "@npmcli/promise-spawn": "^6.0.1", "@npmcli/template-oss": "4.11.0", diff --git a/test/bin/npx-cli.js b/test/bin/npx-cli.js index 5670f24f07b77..67a8d3319fc18 100644 --- a/test/bin/npx-cli.js +++ b/test/bin/npx-cli.js @@ -1,5 +1,5 @@ const t = require('tap') -const mockGlobals = require('../fixtures/mock-globals') +const mockGlobals = require('@npmcli/mock-globals') const tmock = require('../fixtures/tmock') const npm = require.resolve('../../bin/npm-cli.js') diff --git a/test/fixtures/mock-npm.js b/test/fixtures/mock-npm.js index 9e4b354f2b741..16276dd02838d 100644 --- a/test/fixtures/mock-npm.js +++ b/test/fixtures/mock-npm.js @@ -5,7 +5,7 @@ const tap = require('tap') const { EnvKeys, ProcessKeys } = require('@npmcli/config') const errorMessage = require('../../lib/utils/error-message') const mockLogs = require('./mock-logs') -const mockGlobals = require('./mock-globals') +const mockGlobals = require('@npmcli/mock-globals') const tmock = require('./tmock') const defExitCode = process.exitCode const npmRoot = path.resolve(__dirname, '../../') diff --git a/test/lib/commands/adduser.js b/test/lib/commands/adduser.js index 90ed10df70274..3851b1e4b9669 100644 --- a/test/lib/commands/adduser.js +++ b/test/lib/commands/adduser.js @@ -4,7 +4,7 @@ const path = require('path') const ini = require('ini') const { load: loadMockNpm } = require('../../fixtures/mock-npm.js') -const mockGlobals = require('../../fixtures/mock-globals.js') +const mockGlobals = require('@npmcli/mock-globals') const MockRegistry = require('@npmcli/mock-registry') const stream = require('stream') diff --git a/test/lib/commands/cache.js b/test/lib/commands/cache.js index fe2854f9aa626..3a7438831009f 100644 --- a/test/lib/commands/cache.js +++ b/test/lib/commands/cache.js @@ -1,7 +1,7 @@ const t = require('tap') const { load: loadMockNpm } = require('../../fixtures/mock-npm.js') const MockRegistry = require('@npmcli/mock-registry') -const mockGlobals = require('../../fixtures/mock-globals') +const mockGlobals = require('@npmcli/mock-globals') const cacache = require('cacache') const fs = require('fs') diff --git a/test/lib/commands/completion.js b/test/lib/commands/completion.js index 6cc1677552e8a..3cd73edc5c5f9 100644 --- a/test/lib/commands/completion.js +++ b/test/lib/commands/completion.js @@ -7,7 +7,7 @@ const completionScript = fs .replace(/^#!.*?\n/, '') const { load: loadMockNpm } = require('../../fixtures/mock-npm') -const mockGlobals = require('../../fixtures/mock-globals') +const mockGlobals = require('@npmcli/mock-globals') const loadMockCompletion = async (t, o = {}) => { const { globals = {}, windows, ...options } = o diff --git a/test/lib/commands/doctor.js b/test/lib/commands/doctor.js index d1a88299e69ae..1682a6cccfa48 100644 --- a/test/lib/commands/doctor.js +++ b/test/lib/commands/doctor.js @@ -4,7 +4,7 @@ const path = require('path') const { load: loadMockNpm } = require('../../fixtures/mock-npm') const tnock = require('../../fixtures/tnock.js') -const mockGlobals = require('../../fixtures/mock-globals') +const mockGlobals = require('@npmcli/mock-globals') const { cleanCwd, cleanDate } = require('../../fixtures/clean-snapshot.js') const cleanCacheSha = (str) => diff --git a/test/lib/commands/login.js b/test/lib/commands/login.js index ae7e3ffab0f02..683ba96639704 100644 --- a/test/lib/commands/login.js +++ b/test/lib/commands/login.js @@ -4,7 +4,7 @@ const path = require('path') const ini = require('ini') const { load: loadMockNpm } = require('../../fixtures/mock-npm.js') -const mockGlobals = require('../../fixtures/mock-globals.js') +const mockGlobals = require('@npmcli/mock-globals') const MockRegistry = require('@npmcli/mock-registry') const stream = require('stream') diff --git a/test/lib/commands/version.js b/test/lib/commands/version.js index c48ff827fa28c..0f92282f7d1b7 100644 --- a/test/lib/commands/version.js +++ b/test/lib/commands/version.js @@ -2,7 +2,7 @@ const { readFileSync, statSync } = require('fs') const { resolve } = require('path') const t = require('tap') const _mockNpm = require('../../fixtures/mock-npm') -const mockGlobals = require('../../fixtures/mock-globals.js') +const mockGlobals = require('@npmcli/mock-globals') const mockNpm = async (t, opts = {}) => { const res = await _mockNpm(t, { diff --git a/test/lib/docs.js b/test/lib/docs.js index 7eb33dd392431..275b379245539 100644 --- a/test/lib/docs.js +++ b/test/lib/docs.js @@ -3,10 +3,10 @@ const { join, resolve, basename, extname, dirname } = require('path') const fs = require('fs/promises') const localeCompare = require('@isaacs/string-locale-compare')('en') const docs = require('@npmcli/docs') +const { definitions } = require('@npmcli/config') +const mockGlobals = require('@npmcli/mock-globals') const { load: loadMockNpm } = require('../fixtures/mock-npm.js') -const mockGlobals = require('../fixtures/mock-globals.js') -const { definitions } = require('../../lib/utils/config/index.js') const cmdList = require('../../lib/utils/cmd-list.js') const pkg = require('../../package.json') @@ -22,7 +22,7 @@ t.test('shorthands', async t => { t.test('config', async t => { const keys = Object.keys(definitions) - const flat = Object.entries(definitions).filter(([_, d]) => d.flatten).map(([k]) => k) + const flat = Object.entries(definitions).filter(([k, d]) => d.derived.length).map(([k]) => k) const notFlat = keys.filter(k => !flat.includes(k)) t.matchSnapshot(keys, 'all keys') t.matchSnapshot(flat, 'keys that are flattened') @@ -46,7 +46,7 @@ t.test('basic usage', async t => { config: { userconfig: '/some/config/file/.npmrc' }, }) - t.matchSnapshot(await npm.usage) + t.matchSnapshot(npm.usage) }) t.test('usage', async t => { @@ -81,9 +81,9 @@ t.test('usage', async t => { t.test(cmd, async t => { let output = null if (!bareCommands.includes(cmd)) { - const { npm } = await loadMockNpm(t) - const impl = await npm.cmd(cmd) - output = impl.usage + const { Npm } = await loadMockNpm(t, { load: false }) + const { describeUsage } = Npm.derefCommand(cmd) + output = describeUsage } const usage = docs.usage(docs.TAGS.USAGE, { path: cmd }) diff --git a/test/lib/npm.js b/test/lib/npm.js index dd3ca307c72c9..ad7b5c97b9630 100644 --- a/test/lib/npm.js +++ b/test/lib/npm.js @@ -2,7 +2,7 @@ const t = require('tap') const { resolve, dirname, join } = require('path') const fs = require('fs') const { load: loadMockNpm } = require('../fixtures/mock-npm.js') -const mockGlobals = require('../fixtures/mock-globals') +const mockGlobals = require('@npmcli/mock-globals') const { commands } = require('../../lib/utils/cmd-list.js') t.test('not yet loaded', async t => { diff --git a/test/lib/utils/display.js b/test/lib/utils/display.js index cfe0181e23e79..21a114e892f01 100644 --- a/test/lib/utils/display.js +++ b/test/lib/utils/display.js @@ -1,7 +1,7 @@ const t = require('tap') const log = require('../../../lib/utils/log-shim') const mockLogs = require('../../fixtures/mock-logs') -const mockGlobals = require('../../fixtures/mock-globals') +const mockGlobals = require('@npmcli/mock-globals') const tmock = require('../../fixtures/tmock') const mockDisplay = (t, mocks) => { diff --git a/test/lib/utils/error-message.js b/test/lib/utils/error-message.js index 9d07693989ea8..101b724035848 100644 --- a/test/lib/utils/error-message.js +++ b/test/lib/utils/error-message.js @@ -2,7 +2,7 @@ const t = require('tap') const { resolve } = require('path') const fs = require('fs/promises') const { load: _loadMockNpm } = require('../../fixtures/mock-npm.js') -const mockGlobals = require('../../fixtures/mock-globals.js') +const mockGlobals = require('@npmcli/mock-globals') const tmock = require('../../fixtures/tmock') const { cleanCwd, cleanDate } = require('../../fixtures/clean-snapshot.js') diff --git a/test/lib/utils/exit-handler.js b/test/lib/utils/exit-handler.js index 76d5fec4c099a..f698f61348d08 100644 --- a/test/lib/utils/exit-handler.js +++ b/test/lib/utils/exit-handler.js @@ -6,7 +6,7 @@ const { join, resolve } = require('path') const EventEmitter = require('events') const { format } = require('../../../lib/utils/log-file') const { load: loadMockNpm } = require('../../fixtures/mock-npm') -const mockGlobals = require('../../fixtures/mock-globals') +const mockGlobals = require('@npmcli/mock-globals') const { cleanCwd, cleanDate } = require('../../fixtures/clean-snapshot') const tmock = require('../../fixtures/tmock') diff --git a/test/lib/utils/open-url-prompt.js b/test/lib/utils/open-url-prompt.js index faf2ab32587af..0d21f4a9f4953 100644 --- a/test/lib/utils/open-url-prompt.js +++ b/test/lib/utils/open-url-prompt.js @@ -1,5 +1,5 @@ const t = require('tap') -const mockGlobals = require('../../fixtures/mock-globals.js') +const mockGlobals = require('@npmcli/mock-globals') const EventEmitter = require('events') const tmock = require('../../fixtures/tmock') diff --git a/workspaces/config/lib/definitions/definition.js b/workspaces/config/lib/definitions/definition.js index 3eb9c815d4a59..a3799664cbc2a 100644 --- a/workspaces/config/lib/definitions/definition.js +++ b/workspaces/config/lib/definitions/definition.js @@ -122,7 +122,7 @@ class Definition { } get short () { - return [].concat(this.#def.short) + return [].concat(this.#def.short ?? []) } get isBoolean () { @@ -142,7 +142,7 @@ class Definition { } get #typeMultiple () { - return this.#typeDefs.includes(Types.Array) + return this.#types.includes(Types.Array) } get #typeDefs () { @@ -153,10 +153,6 @@ class Definition { return this.#def.defaultDescription ?? describeValue(this.#def.default) } - get #typeDescription () { - return this.#describeTypes() - } - addDerived (...keys) { for (const k of keys) { this.#derived.add(k) @@ -171,18 +167,21 @@ class Definition { describe () { const sections = [ ['Default', this.#defaultDescription], - ['Type', this.#typeDescription], + ['Type', this.#describeTypes()], this.deprecated ? ['DEPRECATED', this.deprecated] : null, + '', this.#def.description, - !this.envExport && 'This value is not exported to the environment for child processes.', + ...(this.envExport ? [] : ['', + 'This value is not exported to the environment for child processes.', + ]), ].map((s) => { if (Array.isArray(s)) { return `* ${s[0]}: ${unindent(s[1])}` } - return s ? unindent(s) : null + return typeof s === 'string' ? unindent(s) : null }) - return wrapAll(`#### \`${this.key}\`\n\n${sections.filter(Boolean).join('\n')}`) + return wrapAll(`#### \`${this.#key}\`\n\n${sections.filter(v => v != null).join('\n')}`) } mustBe () { @@ -224,7 +223,7 @@ class Definition { if (valueTypes.some(t => typeof t !== 'string' && typeof t !== 'number')) { // Generic values, use hint - description = this.#def.hint ? [].concat(this.#def.hint) : this.typeDefs.map(t => t?.hint) + description = this.#def.hint ? [].concat(this.#def.hint) : this.#typeDefs.map(t => t?.hint) } else { // Specific values, use specifics given description = valueTypes @@ -234,14 +233,27 @@ class Definition { const d = description.filter(Boolean).join('|') const usageDesc = `${usage.join('|')} ${d ? `<${d}>` : ''}`.trim() - return this.$typeMultiple ? `${usageDesc} [${usageDesc} ...]` : usageDesc + return this.#typeMultiple ? `${usageDesc} [${usageDesc} ...]` : usageDesc } #describeTypes () { - const descriptions = this.#typeDefs - .filter(t => t?.type !== Types.Array) - .flatMap(t => t?.typeDescription ?? t) - .map(describeValue) + let descriptions + + const type = getType(this.#def.type) + if (type) { + descriptions = [].concat(type.typeDescription) + } else { + const types = this.#typeDefs.filter(t => t?.type !== Types.Array) + descriptions = types.flatMap(t => { + if (t?.typeDescription) { + return [].concat(t.typeDescription) + } + return { value: t } + }) + } + + descriptions = descriptions + .map(v => hasOwn(v, 'value') ? JSON.stringify(v.value) : v) // [a] => "a" // [a, b] => "a or b" diff --git a/workspaces/config/lib/definitions/index.js b/workspaces/config/lib/definitions/index.js index ceecdd0037098..c575b89127746 100644 --- a/workspaces/config/lib/definitions/index.js +++ b/workspaces/config/lib/definitions/index.js @@ -363,7 +363,7 @@ define('cafile', { default: null, type: [null, Types.Path], description: ` - A Types.Path to a file containing one or multiple Certificate Authority signing + A path to a file containing one or multiple Certificate Authority signing certificates. Similar to the \`ca\` setting, but allows for multiple CA's, as well as for the CA information to be stored in a file on disk. `, @@ -529,7 +529,7 @@ define('diff-no-prefix', { define('diff-dst-prefix', { default: 'b/', - type: Types.Path, + type: Types.String, description: ` Destination prefix to be used in \`npm diff\` output. `, @@ -538,7 +538,7 @@ define('diff-dst-prefix', { define('diff-src-prefix', { default: 'a/', - type: Types.Path, + type: Types.String, description: ` Source prefix to be used in \`npm diff\` output. `, diff --git a/workspaces/config/lib/index.js b/workspaces/config/lib/index.js index 3b152c8fd471e..3dc46ac8e456f 100644 --- a/workspaces/config/lib/index.js +++ b/workspaces/config/lib/index.js @@ -12,7 +12,7 @@ const SetGlobal = require('./set-globals.js') const { ErrInvalidAuth } = require('./errors') const Credentials = require('./credentials.js') const ConfigTypes = require('./config-locations') -const { definitions, defaults, definitionKeys, values } = require('./definitions') +const Definitions = require('./definitions') const { isNerfed } = require('./nerf-dart.js') const replaceInfo = require('./replace-info') const Locations = ConfigTypes.Locations @@ -31,7 +31,11 @@ class Config { static ProcessKeys = [...SetGlobal.ProcessKeys.values()] static NerfDarts = Credentials.nerfDarts static replaceInfo = replaceInfo - static configKeys = definitionKeys + static configKeys = Definitions.definitionKeys + static definitions = Definitions.definitions + static shortKeys = Definitions.shortKeys + static shorthands = Definitions.shorthands + static types = Definitions.types // state #configData = null @@ -177,7 +181,7 @@ class Config { // but to not have breakages there we need to have an instance getter // to return the raw data there get defaults () { - return defaults + return Definitions.defaults } // ============================================= @@ -258,7 +262,7 @@ class Config { // command name, the remaining args, and config values from the CLI and can rewrite // them or parse the remaining config files with this information. const { remain, cooked } = this.#loadObject(Locations.cli, this.#argv.slice(2)) - this.#configData.get(Locations.cli).loadObject({ ...values }) + this.#configData.get(Locations.cli).loadObject({ ...Definitions.values }) let command = remain[0] this.#args = remain.slice(1) @@ -359,7 +363,7 @@ class Config { } } - this.#loadObject(Locations.default, defaults) + this.#loadObject(Locations.default, Definitions.defaults) } async #loadBuiltin () { @@ -528,12 +532,12 @@ class Config { const envConf = this.#configData.get(Locations.env) for (const [key, value] in cliConf.entries()) { - const def = definitions[key] + const def = Definitions.definitions[key] if (def?.deprecated || !def?.envExport) { continue } - if (SetGlobal.sameValue(defaults[key], value)) { + if (SetGlobal.sameValue(Definitions.defaults[key], value)) { // config is the default, if the env thought different, then we // have to set it BACK to the default in the environment. if (!SetGlobal.sameValue(envConf.get(key), value)) { diff --git a/workspaces/config/lib/set-globals.js b/workspaces/config/lib/set-globals.js index 07ca8fad51892..f3043e8d43e0c 100644 --- a/workspaces/config/lib/set-globals.js +++ b/workspaces/config/lib/set-globals.js @@ -19,6 +19,7 @@ const EnvKeys = new Set([ const ProcessKeys = new Set([ 'execPath', + 'title', ]) // replace any ${ENV} values with the appropriate environ. @@ -35,7 +36,7 @@ const envReplace = (env, f) => f.replace(envExpr, (orig, esc, name) => { }) const setProcess = (proc, key, val) => { - if (ProcessKeys.has(key)) { + if (!ProcessKeys.has(key)) { throw new Error(`attempted to set non-allowed process: ${key}`) } proc[key] = val diff --git a/workspaces/config/test/config/definitions.js b/workspaces/config/test/config/definitions.js index 2c10139938266..a5c321e61ead2 100644 --- a/workspaces/config/test/config/definitions.js +++ b/workspaces/config/test/config/definitions.js @@ -1,6 +1,6 @@ const t = require('tap') const { resolve } = require('path') -const mockGlobals = require('../../../fixtures/mock-globals') +const mockGlobals = require('@npmcli/mock-globals') const tmock = require('../../../fixtures/tmock') const pkg = require('../../../../package.json') diff --git a/workspaces/config/test/config/index.js b/workspaces/config/test/config/index.js index e38beb4f013ce..9c28b4c57400e 100644 --- a/workspaces/config/test/config/index.js +++ b/workspaces/config/test/config/index.js @@ -1,7 +1,7 @@ const t = require('tap') const config = require('../../../../lib/utils/config/index.js') const { definitions } = require('../../../../lib/utils/config/definitions.js') -const mockGlobals = require('../../../../test/fixtures/mock-globals.js') +const mockGlobals = require('@npmcli/mock-globals') t.test('defaults', t => { // just spot check a few of these to show that we got defaults assembled From 345d7d3bafdb4b847c2a82e1ad38781fc9f198fd Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Tue, 10 Jan 2023 13:07:18 -0700 Subject: [PATCH 7/9] convert tests to new static cmd/completion methods --- bin/npx-cli.js | 4 +- docs/package.json | 4 +- lib/base-command.js | 2 +- lib/commands/access.js | 2 +- lib/commands/audit.js | 2 +- lib/commands/cache.js | 2 +- lib/commands/completion.js | 14 +- lib/commands/config.js | 7 +- lib/commands/deprecate.js | 6 +- lib/commands/dist-tag.js | 2 +- lib/commands/edit.js | 4 +- lib/commands/explain.js | 4 +- lib/commands/explore.js | 2 +- lib/commands/fund.js | 4 +- lib/commands/get.js | 4 +- lib/commands/help.js | 2 +- lib/commands/install.js | 12 +- lib/commands/link.js | 4 +- lib/commands/ls.js | 2 +- lib/commands/org.js | 2 +- lib/commands/owner.js | 8 +- lib/commands/profile.js | 2 +- lib/commands/rebuild.js | 4 +- lib/commands/run-script.js | 2 +- lib/commands/team.js | 2 +- lib/commands/token.js | 2 +- lib/commands/uninstall.js | 4 +- lib/commands/update.js | 2 +- lib/commands/version.js | 2 +- lib/commands/view.js | 8 +- lib/npm.js | 14 +- lib/utils/did-you-mean.js | 2 +- lib/utils/npm-usage.js | 10 +- lib/utils/reify-finish.js | 12 +- test/fixtures/mock-npm.js | 15 +- test/lib/arborist-cmd.js | 4 +- test/lib/commands/access.js | 3 +- test/lib/commands/adduser.js | 3 +- test/lib/commands/audit.js | 3 +- test/lib/commands/bugs.js | 3 +- test/lib/commands/cache.js | 7 +- test/lib/commands/completion.js | 3 +- test/lib/commands/deprecate.js | 7 +- test/lib/commands/dist-tag.js | 10 +- test/lib/commands/explain.js | 11 +- test/lib/commands/help.js | 3 +- test/lib/commands/install.js | 25 ++-- test/lib/commands/link.js | 5 +- test/lib/commands/login.js | 3 +- test/lib/commands/org.js | 6 +- test/lib/commands/owner.js | 18 ++- test/lib/commands/profile.js | 11 +- test/lib/commands/publish.js | 3 +- test/lib/commands/run-script.js | 11 +- test/lib/commands/team.js | 3 +- test/lib/commands/token.js | 3 +- test/lib/commands/unpublish.js | 4 +- test/lib/commands/version.js | 3 +- test/lib/commands/view.js | 6 +- test/lib/docs.js | 2 +- test/lib/npm.js | 56 ++++---- test/lib/utils/exit-handler.js | 7 +- test/lib/utils/open-url-prompt.js | 207 +++++++++++---------------- test/lib/utils/reify-finish.js | 92 ++++++------ test/lib/utils/update-notifier.js | 2 - workspaces/config/lib/config-data.js | 15 +- workspaces/config/lib/index.js | 4 + 67 files changed, 310 insertions(+), 407 deletions(-) diff --git a/bin/npx-cli.js b/bin/npx-cli.js index 683f0f2b451a5..a5e4819e82cce 100755 --- a/bin/npx-cli.js +++ b/bin/npx-cli.js @@ -24,8 +24,8 @@ const removed = new Set([ ...removedOpts, ]) -const { definitions, shorthands, keys } = require('@npmcli/config') -const npmSwitches = keys.filter(k => definitions[k]?.isBoolean) +const { definitions, shorthands, configKeys } = require('@npmcli/config') +const npmSwitches = configKeys.filter(k => definitions[k].isBoolean) // things that don't take a value const switches = new Set([ diff --git a/docs/package.json b/docs/package.json index 168c4b2f6f262..cdf8ded34924a 100644 --- a/docs/package.json +++ b/docs/package.json @@ -21,6 +21,7 @@ }, "devDependencies": { "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/config": "^6.1.0", "@npmcli/eslint-config": "^4.0.0", "@npmcli/template-oss": "4.11.0", "front-matter": "^4.0.2", @@ -63,8 +64,5 @@ ".github/workflows/ci-{{ pkgNameFs }}.yml": "ci-npmcli-docs.yml" } } - }, - "dependencies": { - "@npmcli/config": "^6.1.0" } } diff --git a/lib/base-command.js b/lib/base-command.js index 2526825167c6c..8c178242d0245 100644 --- a/lib/base-command.js +++ b/lib/base-command.js @@ -74,7 +74,7 @@ class BaseCommand { // add getters for some statics to make reading them easier from an // instantiated class - for (const key of ['name', 'description', 'params', 'usage', 'describeUsage']) { + for (const key of ['name', 'description', 'params', 'usage']) { Object.defineProperty(this, key, { enumerable: true, value: this.constructor[key], diff --git a/lib/commands/access.js b/lib/commands/access.js index 23e51f071b112..999c215778404 100644 --- a/lib/commands/access.js +++ b/lib/commands/access.js @@ -47,7 +47,7 @@ class Access extends BaseCommand { 'revoke []', ] - async completion (opts) { + static async completion (opts) { const argv = opts.conf.argv.remain if (argv.length === 2) { return commands diff --git a/lib/commands/audit.js b/lib/commands/audit.js index 13886ea6350b6..60d95e4407d62 100644 --- a/lib/commands/audit.js +++ b/lib/commands/audit.js @@ -350,7 +350,7 @@ class Audit extends ArboristWorkspaceCmd { static usage = ['[fix|signatures]'] - async completion (opts) { + static async completion (opts) { const argv = opts.conf.argv.remain if (argv.length === 2) { diff --git a/lib/commands/cache.js b/lib/commands/cache.js index 0ab40b9ed44a9..e85328703a19f 100644 --- a/lib/commands/cache.js +++ b/lib/commands/cache.js @@ -74,7 +74,7 @@ class Cache extends BaseCommand { 'verify', ] - async completion (opts) { + static async completion (opts) { const argv = opts.conf.argv.remain if (argv.length === 2) { return ['add', 'clean', 'verify', 'ls', 'delete'] diff --git a/lib/commands/completion.js b/lib/commands/completion.js index 338a516d9b0bc..4984670e1aa3b 100644 --- a/lib/commands/completion.js +++ b/lib/commands/completion.js @@ -48,7 +48,7 @@ class Completion extends BaseCommand { static name = 'completion' // completion for the completion command - async completion (opts) { + static async completion (opts) { if (opts.w > 2) { return } @@ -153,10 +153,14 @@ class Completion extends BaseCommand { // at this point, if words[1] is some kind of npm command, // then complete on it. // otherwise, do nothing - const impl = await this.npm.cmd(cmd) - if (impl.completion) { - const comps = await impl.completion(opts) - return this.wrap(opts, comps) + try { + const Cmd = this.npm.constructor.cmd(cmd) + if (Cmd.completion) { + const comps = await Cmd.completion(opts, this.npm) + return this.wrap(opts, comps) + } + } catch { + // it wasnt a valid command, so do nothing } } diff --git a/lib/commands/config.js b/lib/commands/config.js index 80b6355c25e1a..bdbd68a511c94 100644 --- a/lib/commands/config.js +++ b/lib/commands/config.js @@ -1,5 +1,3 @@ -const { keys: configKeys, NerfDarts, definitions } = require('@npmcli/config') - const { mkdir, readFile, writeFile } = require('fs/promises') const { dirname, resolve } = require('path') const { spawn } = require('child_process') @@ -8,6 +6,7 @@ const ini = require('ini') const localeCompare = require('@isaacs/string-locale-compare')('en') const rpj = require('read-package-json-fast') const log = require('../utils/log-shim.js') +const { keys: configKeys, NerfDarts, definitions, defaults } = require('@npmcli/config') // take an array of `[key, value, k2=v2, k3, v3, ...]` and turn into // { key: value, k2: v2, k3: v3 } @@ -61,7 +60,7 @@ class Config extends BaseCommand { static skipConfigValidation = true - async completion (opts) { + static async completion (opts) { const argv = opts.conf.argv.remain if (argv[1] !== 'config') { argv.unshift('config') @@ -201,7 +200,7 @@ class Config extends BaseCommand { const data = ( await readFile(file, 'utf8').catch(() => '') ).replace(/\r\n/g, '\n') - const entries = Object.entries(this.npm.config.defaults) + const entries = Object.entries(defaults) const defData = entries.reduce((str, [key, val]) => { const obj = { [key]: val } const i = ini.stringify(obj) diff --git a/lib/commands/deprecate.js b/lib/commands/deprecate.js index 1e1b8994c55a3..62428d21bcdc5 100644 --- a/lib/commands/deprecate.js +++ b/lib/commands/deprecate.js @@ -17,13 +17,13 @@ class Deprecate extends BaseCommand { static ignoreImplicitWorkspace = false - async completion (opts) { + static async completion (opts, npm) { if (opts.conf.argv.remain.length > 1) { return [] } - const username = await getIdentity(this.npm, this.npm.flatOptions) - const packages = await libaccess.getPackages(username, this.npm.flatOptions) + const username = await getIdentity(npm, npm.flatOptions) + const packages = await libaccess.getPackages(username, npm.flatOptions) return Object.keys(packages) .filter((name) => packages[name] === 'write' && diff --git a/lib/commands/dist-tag.js b/lib/commands/dist-tag.js index bc61a4691e55a..0b92631125a31 100644 --- a/lib/commands/dist-tag.js +++ b/lib/commands/dist-tag.js @@ -20,7 +20,7 @@ class DistTag extends BaseCommand { static workspaces = true static ignoreImplicitWorkspace = false - async completion (opts) { + static async completion (opts) { const argv = opts.conf.argv.remain if (argv.length === 2) { return ['add', 'rm', 'ls'] diff --git a/lib/commands/edit.js b/lib/commands/edit.js index a671a5d6bad5d..fbc7840a39876 100644 --- a/lib/commands/edit.js +++ b/lib/commands/edit.js @@ -38,8 +38,8 @@ class Edit extends BaseCommand { // TODO /* istanbul ignore next */ - async completion (opts) { - return completion(this.npm, opts) + static async completion (opts, npm) { + return completion(npm, opts) } async exec (args) { diff --git a/lib/commands/explain.js b/lib/commands/explain.js index a06ad24152a1e..e2b75a71ec0bf 100644 --- a/lib/commands/explain.js +++ b/lib/commands/explain.js @@ -20,8 +20,8 @@ class Explain extends ArboristWorkspaceCmd { // TODO /* istanbul ignore next */ - async completion (opts) { - return completion(this.npm, opts) + static async completion (opts, npm) { + return completion(npm, opts) } async exec (args) { diff --git a/lib/commands/explore.js b/lib/commands/explore.js index 0d915cb4c6958..6714c8e4d4ca0 100644 --- a/lib/commands/explore.js +++ b/lib/commands/explore.js @@ -17,7 +17,7 @@ class Explore extends BaseCommand { // TODO /* istanbul ignore next */ - async completion (opts) { + static async completion (opts) { return completion(this.npm, opts) } diff --git a/lib/commands/fund.js b/lib/commands/fund.js index 12762533c123e..4b0da4928c287 100644 --- a/lib/commands/fund.js +++ b/lib/commands/fund.js @@ -39,8 +39,8 @@ class Fund extends ArboristWorkspaceCmd { // TODO /* istanbul ignore next */ - async completion (opts) { - return completion(this.npm, opts) + static async completion (opts, npm) { + return completion(npm, opts) } async exec (args) { diff --git a/lib/commands/get.js b/lib/commands/get.js index 6ac71a193ac79..9b714a675fcdc 100644 --- a/lib/commands/get.js +++ b/lib/commands/get.js @@ -1,6 +1,6 @@ -const BaseCommand = require('../base-command.js') +const ConfigCommand = require('./config') -class Get extends BaseCommand { +class Get extends ConfigCommand { static description = 'Get a value from the npm configuration' static name = 'get' static usage = ['[ ...] (See `npm config`)'] diff --git a/lib/commands/help.js b/lib/commands/help.js index f8eaa5f0093ec..945aa098f8372 100644 --- a/lib/commands/help.js +++ b/lib/commands/help.js @@ -29,7 +29,7 @@ class Help extends BaseCommand { static usage = [' []'] static params = ['viewer'] - async completion (opts) { + static async completion (opts) { if (opts.conf.argv.remain.length > 2) { return [] } diff --git a/lib/commands/install.js b/lib/commands/install.js index 246d7fdf8f02e..0adb2bb2c6245 100644 --- a/lib/commands/install.js +++ b/lib/commands/install.js @@ -37,7 +37,7 @@ class Install extends ArboristWorkspaceCmd { static usage = ['[ ...]'] - async completion (opts) { + static async completion (opts) { const { partialWord } = opts // install can complete to a folder with a package.json, or any package. // if it has a slash, then it's gotta be a folder @@ -65,7 +65,7 @@ class Install extends ArboristWorkspaceCmd { const contents = await readdir(join(partialPath, sibling)) const result = (contents.indexOf('package.json') !== -1) return result - } catch (er) { + } catch { return false } } @@ -81,11 +81,11 @@ class Install extends ArboristWorkspaceCmd { if (matches.length === 1) { return [join(partialPath, matches[0])] } - // no matches - return [] - } catch (er) { - return [] // invalid dir: no matching + } catch { + // invalid dir: no matching } + // no matches or invalid dir + return [] } // Note: there used to be registry completion here, // but it stopped making sense somewhere around diff --git a/lib/commands/link.js b/lib/commands/link.js index 5af2c7c269f8f..9818e235ef496 100644 --- a/lib/commands/link.js +++ b/lib/commands/link.js @@ -36,8 +36,8 @@ class Link extends ArboristWorkspaceCmd { ...super.params, ] - async completion (opts) { - const dir = this.npm.globalDir + static async completion (opts, npm) { + const dir = npm.globalDir const files = await readdir(dir) return files.filter(f => !/^[._-]/.test(f)) } diff --git a/lib/commands/ls.js b/lib/commands/ls.js index 2213e7937407a..aed0031723357 100644 --- a/lib/commands/ls.js +++ b/lib/commands/ls.js @@ -44,7 +44,7 @@ class LS extends ArboristWorkspaceCmd { // TODO /* istanbul ignore next */ - async completion (opts) { + static async completion (opts) { return completion(this.npm, opts) } diff --git a/lib/commands/org.js b/lib/commands/org.js index 575ff75e2a6cf..1f32d41ff7306 100644 --- a/lib/commands/org.js +++ b/lib/commands/org.js @@ -14,7 +14,7 @@ class Org extends BaseCommand { static params = ['registry', 'otp', 'json', 'parseable'] - async completion (opts) { + static async completion (opts) { const argv = opts.conf.argv.remain if (argv.length === 2) { return ['set', 'rm', 'ls'] diff --git a/lib/commands/owner.js b/lib/commands/owner.js index 40f16332b2922..75b4e0e834eb5 100644 --- a/lib/commands/owner.js +++ b/lib/commands/owner.js @@ -35,7 +35,7 @@ class Owner extends BaseCommand { static workspaces = true static ignoreImplicitWorkspace = false - async completion (opts) { + static async completion (opts, npm) { const argv = opts.conf.argv.remain if (argv.length > 3) { return [] @@ -51,17 +51,17 @@ class Owner extends BaseCommand { // reaches registry in order to autocomplete rm if (argv[2] === 'rm') { - if (this.npm.global) { + if (npm.global) { return [] } - const { name } = await readJson(resolve(this.npm.prefix, 'package.json')) + const { name } = await readJson(resolve(npm.prefix, 'package.json')) if (!name) { return [] } const spec = npa(name) const data = await pacote.packument(spec, { - ...this.npm.flatOptions, + ...npm.flatOptions, fullMetadata: true, }) if (data && data.maintainers && data.maintainers.length) { diff --git a/lib/commands/profile.js b/lib/commands/profile.js index c0981beb50158..c04bbb4c278fa 100644 --- a/lib/commands/profile.js +++ b/lib/commands/profile.js @@ -54,7 +54,7 @@ class Profile extends BaseCommand { 'otp', ] - async completion (opts) { + static async completion (opts) { var argv = opts.conf.argv.remain if (!argv[2]) { diff --git a/lib/commands/rebuild.js b/lib/commands/rebuild.js index df791106fdd21..6486022cfec1e 100644 --- a/lib/commands/rebuild.js +++ b/lib/commands/rebuild.js @@ -20,8 +20,8 @@ class Rebuild extends ArboristWorkspaceCmd { // TODO /* istanbul ignore next */ - async completion (opts) { - return completion(this.npm, opts) + static async completion (opts, npm) { + return completion(npm, opts) } async exec (args) { diff --git a/lib/commands/run-script.js b/lib/commands/run-script.js index 81bd2333219fc..9fc1a3c01bd7e 100644 --- a/lib/commands/run-script.js +++ b/lib/commands/run-script.js @@ -45,7 +45,7 @@ class RunScript extends BaseCommand { static ignoreImplicitWorkspace = false static isShellout = true - async completion (opts) { + static async completion (opts) { const argv = opts.conf.argv.remain if (argv.length === 2) { // find the script name diff --git a/lib/commands/team.js b/lib/commands/team.js index 2d4fc663715e4..3c6cf305a6e5f 100644 --- a/lib/commands/team.js +++ b/lib/commands/team.js @@ -24,7 +24,7 @@ class Team extends BaseCommand { static ignoreImplicitWorkspace = false - async completion (opts) { + static async completion (opts) { const { conf: { argv: { remain: argv } } } = opts const subcommands = ['create', 'destroy', 'add', 'rm', 'ls'] diff --git a/lib/commands/token.js b/lib/commands/token.js index 021a708508a32..a29aed19882af 100644 --- a/lib/commands/token.js +++ b/lib/commands/token.js @@ -15,7 +15,7 @@ class Token extends BaseCommand { static usage = ['list', 'revoke ', 'create [--read-only] [--cidr=list]'] static params = ['read-only', 'cidr', 'registry', 'otp'] - async completion (opts) { + static async completion (opts) { const argv = opts.conf.argv.remain const subcommands = ['list', 'revoke', 'create'] if (argv.length === 2) { diff --git a/lib/commands/uninstall.js b/lib/commands/uninstall.js index 8c44f2e32106c..8e1707e6e7e10 100644 --- a/lib/commands/uninstall.js +++ b/lib/commands/uninstall.js @@ -15,8 +15,8 @@ class Uninstall extends ArboristWorkspaceCmd { // TODO /* istanbul ignore next */ - async completion (opts) { - return completion(this.npm, opts) + static async completion (opts, npm) { + return completion(npm, opts) } async exec (args) { diff --git a/lib/commands/update.js b/lib/commands/update.js index fd30bcb41e2b3..bfac6e29506b8 100644 --- a/lib/commands/update.js +++ b/lib/commands/update.js @@ -33,7 +33,7 @@ class Update extends ArboristWorkspaceCmd { // TODO /* istanbul ignore next */ - async completion (opts) { + static async completion (opts) { return completion(this.npm, opts) } diff --git a/lib/commands/version.js b/lib/commands/version.js index 9c421ac9e91cf..dcabb18ad1820 100644 --- a/lib/commands/version.js +++ b/lib/commands/version.js @@ -28,7 +28,7 @@ class Version extends BaseCommand { /* eslint-disable-next-line max-len */ static usage = ['[ | major | minor | patch | premajor | preminor | prepatch | prerelease | from-git]'] - async completion (opts) { + static async completion (opts) { const { conf: { argv: { remain }, diff --git a/lib/commands/view.js b/lib/commands/view.js index 855b37b81d42f..f702320719501 100644 --- a/lib/commands/view.js +++ b/lib/commands/view.js @@ -30,21 +30,19 @@ class View extends BaseCommand { static ignoreImplicitWorkspace = false static usage = ['[] [[.subfield]...]'] - async completion (opts) { + static async completion (opts, npm) { if (opts.conf.argv.remain.length <= 2) { - // There used to be registry completion here, but it stopped - // making sense somewhere around 50,000 packages on the registry return } // have the package, get the fields const config = { - ...this.npm.flatOptions, + ...npm.flatOptions, fullMetadata: true, preferOnline: true, } const spec = npa(opts.conf.argv.remain[2]) const pckmnt = await packument(spec, config) - const defaultTag = this.npm.config.get('tag') + const defaultTag = npm.config.get('tag') const dv = pckmnt.versions[pckmnt['dist-tags'][defaultTag]] pckmnt.versions = Object.keys(pckmnt.versions).sort(semver.compareLoose) diff --git a/lib/npm.js b/lib/npm.js index d530c818b3ea9..a06f124058af7 100644 --- a/lib/npm.js +++ b/lib/npm.js @@ -18,7 +18,7 @@ const { deref } = require('./utils/cmd-list.js') class Npm { static version = pkg.version - static derefCommand (c) { + static cmd (c) { const command = deref(c) if (!command) { throw Object.assign(new Error(`Unknown command ${c}`), { @@ -94,9 +94,14 @@ class Npm { // similar to how the CLI does by passing in an argv containing the command // and args. async exec (commandName = this.#argvCommand, args = this.#argvArgs) { + const Impl = Npm.cmd(commandName) + const command = new Impl(this) + // exec can be re-entered by an already running command but if it is the - // first time we want to kickoff the update notifier + // first time we want to kickoff the update notifier and set the command if (!this.#command) { + this.#command = command + this.config.set('npm-command', this.command) // this is async but we dont await it, since its ok if it doesnt // finish before the command finishes running. it uses command and argv // so it must be initiated here, after the command name is set @@ -104,10 +109,7 @@ class Npm { updateNotifier(this).then((msg) => (this.updateNotification = msg)) } - const Impl = Npm.derefCommand(commandName) - this.#command = new Impl(this) - this.config.set('npm-command', this.command) - return this.time(`command:${this.command}`, () => this.#command.cmdExec(args)) + return this.time(`command:${command.name}`, () => command.cmdExec(args)) } get loaded () { diff --git a/lib/utils/did-you-mean.js b/lib/utils/did-you-mean.js index 6623f9e5c79df..e3ef73ed8bec3 100644 --- a/lib/utils/did-you-mean.js +++ b/lib/utils/did-you-mean.js @@ -11,7 +11,7 @@ const didYouMean = async (path, scmd) => { const close = commands.filter(cmd => distance(scmd, cmd) < scmd.length * 0.4 && scmd !== cmd) for (const str of close) { - const Command = Npm.derefCommand(str) + const Command = Npm.cmd(str) suggest.push([str, Command.description]) } diff --git a/lib/utils/npm-usage.js b/lib/utils/npm-usage.js index 018ec248d577a..d848d6c3d3116 100644 --- a/lib/utils/npm-usage.js +++ b/lib/utils/npm-usage.js @@ -1,4 +1,4 @@ -const { commands, derefCommand } = require('./cmd-list') +const { commands } = require('./cmd-list') const COL_MAX = 60 const COL_MIN = 24 @@ -10,7 +10,7 @@ const indentNewline = (repeat) => `\n${indent(repeat)}` module.exports = (npm) => { const browser = npm.config.get('viewer') === 'browser' ? ' (in a browser)' : '' - const allCommands = npm.config.get('long') ? cmdUsages() : cmdNames() + const allCommands = npm.config.get('long') ? cmdUsages(npm.constructor) : cmdNames() return `npm @@ -57,13 +57,13 @@ const cmdNames = () => { return indentNewline() + out.join(indentNewline()).slice(2) } -const cmdUsages = () => { +const cmdUsages = (Npm) => { // return a string of : let maxLen = 0 const set = [] for (const c of commands) { - const { usage } = derefCommand(c) - set.push([c, usage.split('\n')]) + const { describeUsage } = Npm.cmd(c) + set.push([c, describeUsage.split('\n')]) maxLen = Math.max(maxLen, c.length) } diff --git a/lib/utils/reify-finish.js b/lib/utils/reify-finish.js index 9b43abcb7610a..dda5d0b4bf52d 100644 --- a/lib/utils/reify-finish.js +++ b/lib/utils/reify-finish.js @@ -1,6 +1,5 @@ const reifyOutput = require('./reify-output.js') -const ini = require('ini') -const { writeFile } = require('fs').promises +const { Locations } = require('@npmcli/config') const { resolve } = require('path') const reifyFinish = async (npm, arb) => { @@ -21,13 +20,8 @@ const saveBuiltinConfig = async (npm, arb) => { return } - const builtinConf = npm.config.data.get('builtin') - if (builtinConf.loadError) { - return - } - - const content = ini.stringify(builtinConf.raw).trim() + '\n' - await writeFile(resolve(npmNode.path, 'npmrc'), content) + const builtinConf = npm.config.get(Locations.builtin) + await builtinConf.save(resolve(npmNode.path, 'npmrc')) } module.exports = reifyFinish diff --git a/test/fixtures/mock-npm.js b/test/fixtures/mock-npm.js index 16276dd02838d..e26de369c304e 100644 --- a/test/fixtures/mock-npm.js +++ b/test/fixtures/mock-npm.js @@ -238,7 +238,7 @@ const setupMockNpm = async (t, { ...env, }) - const { npm, ...mockNpm } = await getMockNpm(t, { + const { npm, Npm, ...mockNpm } = await getMockNpm(t, { init, load, mocks: withDirs(mocks), @@ -258,13 +258,13 @@ const setupMockNpm = async (t, { const mockCommand = {} if (command) { - const cmd = await npm.cmd(command) - const usage = await cmd.usage - mockCommand.cmd = cmd - mockCommand[command] = { - usage, + const camelCommand = command.replace(/-[a-z]/g, (...a) => a[1].toUpperCase()) + const Cmd = Npm.cmd(command) + mockCommand.Cmd = Cmd + mockCommand[camelCommand] = { + usage: Cmd.describeUsage, exec: (args) => npm.exec(command, args), - completion: (args) => cmd.completion(args), + completion: (args) => Cmd.completion(args, npm), } if (exec) { await mockCommand[command].exec(exec) @@ -275,6 +275,7 @@ const setupMockNpm = async (t, { } return { + Npm, npm, ...mockNpm, ...dirs, diff --git a/test/lib/arborist-cmd.js b/test/lib/arborist-cmd.js index 36c697cd9e8fd..76df6aefc57ec 100644 --- a/test/lib/arborist-cmd.js +++ b/test/lib/arborist-cmd.js @@ -113,11 +113,11 @@ t.test('arborist-cmd', async t => { }) await t.test('prefix inside cwd', async t => { - const { npm, cmd, prefix } = await mockArboristCmd(t, null, ['a', 'c'], { + const { cmd } = await mockArboristCmd(t, null, ['a', 'c'], { chdir: (dirs) => dirs.testdir, + config: ({ prefix }) => ({ prefix }), }) - npm.localPrefix = prefix await cmd.execWorkspaces([]) t.same(cmd.workspaceNames, ['a', 'c'], 'should set array with single ws name') diff --git a/test/lib/commands/access.js b/test/lib/commands/access.js index b0057545ba026..8847fb33c3e3e 100644 --- a/test/lib/commands/access.js +++ b/test/lib/commands/access.js @@ -7,8 +7,7 @@ const token = 'test-auth-token' const auth = { '//registry.npmjs.org/:_authToken': 'test-auth-token' } t.test('completion', async t => { - const { npm } = await loadMockNpm(t) - const access = await npm.cmd('access') + const { access } = await loadMockNpm(t, { command: 'access' }) const testComp = (argv, expect) => { const res = access.completion({ conf: { argv: { remain: argv } } }) t.resolves(res, expect, argv.join(' ')) diff --git a/test/lib/commands/adduser.js b/test/lib/commands/adduser.js index 3851b1e4b9669..8a2cc837a6773 100644 --- a/test/lib/commands/adduser.js +++ b/test/lib/commands/adduser.js @@ -9,8 +9,7 @@ const MockRegistry = require('@npmcli/mock-registry') const stream = require('stream') t.test('usage', async t => { - const { npm } = await loadMockNpm(t) - const adduser = await npm.cmd('adduser') + const { adduser } = await loadMockNpm(t, { command: 'adduser' }) t.match(adduser.usage, 'adduser', 'usage has command name in it') }) diff --git a/test/lib/commands/audit.js b/test/lib/commands/audit.js index bba74407cb3fe..3994c96016864 100644 --- a/test/lib/commands/audit.js +++ b/test/lib/commands/audit.js @@ -210,8 +210,7 @@ t.test('audit fix - bulk endpoint', async t => { }) t.test('completion', async t => { - const { npm } = await loadMockNpm(t) - const audit = await npm.cmd('audit') + const { audit } = await loadMockNpm(t, { command: 'audit' }) t.test('fix', async t => { await t.resolveMatch( audit.completion({ conf: { argv: { remain: ['npm', 'audit'] } } }), diff --git a/test/lib/commands/bugs.js b/test/lib/commands/bugs.js index bf45b9eee81ab..953c8e6345a2a 100644 --- a/test/lib/commands/bugs.js +++ b/test/lib/commands/bugs.js @@ -43,8 +43,7 @@ const pacote = { } t.test('usage', async (t) => { - const { npm } = await loadMockNpm(t) - const bugs = await npm.cmd('bugs') + const { bugs } = await loadMockNpm(t, { command: 'bugs' }) t.match(bugs.usage, 'bugs', 'usage has command name in it') }) diff --git a/test/lib/commands/cache.js b/test/lib/commands/cache.js index 3a7438831009f..f08651e964abf 100644 --- a/test/lib/commands/cache.js +++ b/test/lib/commands/cache.js @@ -302,12 +302,11 @@ t.test('cache verify w/ extra output', async t => { }) t.test('cache completion', async t => { - const { npm } = await loadMockNpm(t) - const cache = await npm.cmd('cache') - const { completion } = cache + const { cache } = await loadMockNpm(t, { command: 'cache' }) const testComp = (argv, expect) => { - return t.resolveMatch(completion({ conf: { argv: { remain: argv } } }), expect, argv.join(' ')) + const opts = { conf: { argv: { remain: argv } } } + return t.resolveMatch(cache.completion(opts), expect, argv.join(' ')) } await Promise.all([ diff --git a/test/lib/commands/completion.js b/test/lib/commands/completion.js index 3cd73edc5c5f9..2d5a2bf7c11e1 100644 --- a/test/lib/commands/completion.js +++ b/test/lib/commands/completion.js @@ -20,11 +20,10 @@ const loadMockCompletion = async (t, o = {}) => { }).reset const res = await loadMockNpm(t, { ...options, + command: 'completion', }) - const completion = await res.npm.cmd('completion') return { resetGlobals, - completion, ...res, } } diff --git a/test/lib/commands/deprecate.js b/test/lib/commands/deprecate.js index 22ddfe182de6e..9de06b0c5c6fb 100644 --- a/test/lib/commands/deprecate.js +++ b/test/lib/commands/deprecate.js @@ -12,16 +12,15 @@ const versions = ['1.0.0', '1.0.1', '1.0.1-pre'] const packages = { foo: 'write', bar: 'write', baz: 'write', buzz: 'read' } t.test('completion', async t => { - const { npm } = await loadMockNpm(t, { + const { npm, deprecate } = await loadMockNpm(t, { config: { ...auth, }, + command: 'deprecate', }) - const deprecate = await npm.cmd('deprecate') const testComp = async (argv, expect) => { - const res = - await deprecate.completion({ conf: { argv: { remain: argv } } }) + const res = await deprecate.completion({ conf: { argv: { remain: argv } } }) t.strictSame(res, expect, `completion: ${argv}`) } diff --git a/test/lib/commands/dist-tag.js b/test/lib/commands/dist-tag.js index 4cc241f74582d..e927697a3aee2 100644 --- a/test/lib/commands/dist-tag.js +++ b/test/lib/commands/dist-tag.js @@ -80,19 +80,11 @@ const mockDist = async (t, { ...npmOpts } = {}) => { mocks: { 'npm-registry-fetch': Object.assign(nrf, realFetch, { json: getTag }), }, + command: 'dist-tag', }) - const usage = await mock.npm.cmd('dist-tag').then(c => c.usage) - return { ...mock, - distTag: { - exec: (args) => mock.npm.exec('dist-tag', args), - usage, - completion: (remain) => mock.npm.cmd('dist-tag').then(c => c.completion({ - conf: { argv: { remain } }, - })), - }, fetchOpts: () => fetchOpts, result: () => mock.joinedOutput(), logs: () => { diff --git a/test/lib/commands/explain.js b/test/lib/commands/explain.js index 3262dfdce87af..159ee96fca25a 100644 --- a/test/lib/commands/explain.js +++ b/test/lib/commands/explain.js @@ -13,17 +13,10 @@ const mockExplain = async (t, opts) => { }, }, ...opts, + command: 'explain', }) - const usage = await mock.npm.cmd('explain').then(c => c.usage) - - return { - ...mock, - explain: { - usage, - exec: (args) => mock.npm.exec('explain', args), - }, - } + return mock } t.test('no args throws usage', async t => { diff --git a/test/lib/commands/help.js b/test/lib/commands/help.js index d4e7a81f84a4c..ed16df41d2ad9 100644 --- a/test/lib/commands/help.js +++ b/test/lib/commands/help.js @@ -61,17 +61,16 @@ const mockHelp = async (t, { mocks: { '@npmcli/promise-spawn': mockSpawn }, otherDirs: { ...manPages.fixtures }, config, + command: 'help', ...opts, }) - const help = await npm.cmd('help') const exec = execArgs ? await npm.exec('help', execArgs) : (...a) => npm.exec('help', a) return { npm, - help, exec, manPages: manPages.pages, getArgs: () => args, diff --git a/test/lib/commands/install.js b/test/lib/commands/install.js index 1be42d6e6125f..9c0c59d6afb26 100644 --- a/test/lib/commands/install.js +++ b/test/lib/commands/install.js @@ -361,9 +361,8 @@ t.test('location detection and audit', async (t) => { 'other-dir': { a: 'a' }, }, }) - const install = await npm.cmd('install') - t.equal(install.npm.config.get('location'), 'user') - t.equal(install.npm.config.get('audit'), false) + t.equal(npm.config.get('location'), 'user') + t.equal(npm.config.get('audit'), false) }) await t.test('audit true with package.json', async t => { @@ -373,9 +372,8 @@ t.test('location detection and audit', async (t) => { 'readme.txt': 'just a file', }, }) - const install = await npm.cmd('install') - t.equal(install.npm.config.get('location'), 'user') - t.equal(install.npm.config.get('audit'), true) + t.equal(npm.config.get('location'), 'user') + t.equal(npm.config.get('audit'), true) }) await t.test('audit true without package.json when set', async t => { @@ -389,9 +387,8 @@ t.test('location detection and audit', async (t) => { audit: true, }, }) - const install = await npm.cmd('install') - t.equal(install.npm.config.get('location'), 'user') - t.equal(install.npm.config.get('audit'), true) + t.equal(npm.config.get('location'), 'user') + t.equal(npm.config.get('audit'), true) }) await t.test('audit true in root config without package.json', async t => { @@ -405,9 +402,8 @@ t.test('location detection and audit', async (t) => { otherDirs: { npmrc: 'audit=true' }, npm: ({ other }) => ({ npmRoot: other }), }) - const install = await npm.cmd('install') - t.equal(install.npm.config.get('location'), 'user') - t.equal(install.npm.config.get('audit'), true) + t.equal(npm.config.get('location'), 'user') + t.equal(npm.config.get('audit'), true) }) await t.test('test for warning when --global & --audit', async t => { @@ -422,9 +418,8 @@ t.test('location detection and audit', async (t) => { global: true, }, }) - const install = await npm.cmd('install') - t.equal(install.npm.config.get('location'), 'user') - t.equal(install.npm.config.get('audit'), true) + t.equal(npm.config.get('location'), 'user') + t.equal(npm.config.get('audit'), true) t.equal(logs.warn[0][0], 'config') t.equal(logs.warn[0][1], 'includes both --global and --audit, which is currently unsupported.') }) diff --git a/test/lib/commands/link.js b/test/lib/commands/link.js index feae75a4b9096..65d5490addcbc 100644 --- a/test/lib/commands/link.js +++ b/test/lib/commands/link.js @@ -15,6 +15,7 @@ const mockLink = async (t, { globalPrefixDir, ...opts } = {}) => { ...opts.mocks, '{LIB}/utils/reify-output.js': async () => {}, }, + command: 'link', }) const printLinks = async ({ global = false } = {}) => { @@ -36,10 +37,6 @@ const mockLink = async (t, { globalPrefixDir, ...opts } = {}) => { return { ...mock, - link: { - exec: (args = []) => mock.npm.exec('link', args), - completion: (o) => mock.npm.cmd('link').then(c => c.completion(o)), - }, printLinks, } } diff --git a/test/lib/commands/login.js b/test/lib/commands/login.js index 683ba96639704..f0dc05248db33 100644 --- a/test/lib/commands/login.js +++ b/test/lib/commands/login.js @@ -9,8 +9,7 @@ const MockRegistry = require('@npmcli/mock-registry') const stream = require('stream') t.test('usage', async t => { - const { npm } = await loadMockNpm(t) - const login = await npm.cmd('login') + const { login } = await loadMockNpm(t, { command: 'login' }) t.match(login.usage, 'login', 'usage has command name in it') }) diff --git a/test/lib/commands/org.js b/test/lib/commands/org.js index d3700304328ee..0f412bf667eeb 100644 --- a/test/lib/commands/org.js +++ b/test/lib/commands/org.js @@ -34,15 +34,11 @@ const mockOrg = async (t, { orgSize = 1, orgList = {}, ...npmOpts } = {}) => { libnpmorg, ...npmOpts.mocks, }, + commands: 'org', }) return { ...mock, - org: { - exec: (args) => mock.npm.exec('org', args), - completion: (arg) => mock.npm.cmd('org').then(c => c.completion(arg)), - usage: () => mock.npm.cmd('org').then(c => c.usage), - }, setArgs: () => setArgs, rmArgs: () => rmArgs, lsArgs: () => lsArgs, diff --git a/test/lib/commands/owner.js b/test/lib/commands/owner.js index f9399a60cdf81..f06f624b014e8 100644 --- a/test/lib/commands/owner.js +++ b/test/lib/commands/owner.js @@ -614,8 +614,7 @@ t.test('workspaces', async t => { t.test('completion', async t => { t.test('basic commands', async t => { - const { npm } = await loadMockNpm(t) - const owner = await npm.cmd('owner') + const { owner } = await loadMockNpm(t, { command: 'owner' }) const testComp = async (argv, expect) => { const res = await owner.completion({ conf: { argv: { remain: argv } } }) t.strictSame(res, expect, argv.join(' ')) @@ -631,10 +630,10 @@ t.test('completion', async t => { }) t.test('completion npm owner rm', async t => { - const { npm } = await loadMockNpm(t, { + const { npm, owner } = await loadMockNpm(t, { prefixDir: { 'package.json': JSON.stringify({ name: packageName }) }, + command: 'owner', }) - const owner = await npm.cmd('owner') const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry'), @@ -649,26 +648,25 @@ t.test('completion', async t => { }) t.test('completion npm owner rm no cwd package', async t => { - const { npm } = await loadMockNpm(t) - const owner = await npm.cmd('owner') + const { owner } = await loadMockNpm(t, { command: 'owner' }) const res = await owner.completion({ conf: { argv: { remain: ['npm', 'owner', 'rm'] } } }) t.strictSame(res, [], 'should have no owners to autocomplete if not cwd package') }) t.test('completion npm owner rm global', async t => { - const { npm } = await loadMockNpm(t, { + const { owner } = await loadMockNpm(t, { config: { global: true }, + command: 'owner', }) - const owner = await npm.cmd('owner') const res = await owner.completion({ conf: { argv: { remain: ['npm', 'owner', 'rm'] } } }) t.strictSame(res, [], 'should have no owners to autocomplete if global') }) t.test('completion npm owner rm no owners found', async t => { - const { npm } = await loadMockNpm(t, { + const { npm, owner } = await loadMockNpm(t, { + command: 'owner', prefixDir: { 'package.json': JSON.stringify({ name: packageName }) }, }) - const owner = await npm.cmd('owner') const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry'), diff --git a/test/lib/commands/profile.js b/test/lib/commands/profile.js index 00ccf2607524a..c8c986841b586 100644 --- a/test/lib/commands/profile.js +++ b/test/lib/commands/profile.js @@ -28,15 +28,12 @@ const mockProfile = async (t, { npmProfile, readUserInfo, qrcode, ...opts } = {} ...mocks, ...opts.mocks, }, + command: 'profile', }) return { ...mock, result: () => mock.joinedOutput(), - profile: { - exec: (args) => mock.npm.exec('profile', args), - usage: () => mock.npm.cmd('profile').then(c => c.usage), - }, } } @@ -1067,8 +1064,7 @@ t.test('unknown subcommand', async t => { t.test('completion', async t => { const testComp = async (t, { argv, expect, title } = {}) => { - const { npm } = await mockProfile(t) - const profile = await npm.cmd('profile') + const { profile } = await mockProfile(t) t.resolveMatch(profile.completion({ conf: { argv: { remain: argv } } }), expect, title) } @@ -1100,8 +1096,7 @@ t.test('completion', async t => { }) t.test('npm profile unknown subcommand autocomplete', async t => { - const { npm } = await mockProfile(t) - const profile = await npm.cmd('profile') + const { profile } = await mockProfile(t) t.rejects( profile.completion({ conf: { argv: { remain: ['npm', 'profile', 'asdf'] } } }), { message: 'asdf not recognized' }, diff --git a/test/lib/commands/publish.js b/test/lib/commands/publish.js index 39696066130f9..0781d94374cc2 100644 --- a/test/lib/commands/publish.js +++ b/test/lib/commands/publish.js @@ -172,8 +172,7 @@ t.test('dry-run', async t => { }) t.test('shows usage with wrong set of arguments', async t => { - const { npm } = await loadMockNpm(t) - const publish = await npm.cmd('publish') + const { publish } = await loadMockNpm(t, { command: 'publish' }) await t.rejects(publish.exec(['a', 'b', 'c']), publish.usage) }) diff --git a/test/lib/commands/run-script.js b/test/lib/commands/run-script.js index a265db3cc040d..3f9d10847182f 100644 --- a/test/lib/commands/run-script.js +++ b/test/lib/commands/run-script.js @@ -35,11 +35,12 @@ const mockRs = async (t, { windows = false, runScript, ...opts } = {}) => { t.test('completion', async t => { const completion = async (t, remain, pkg) => { - const { npm } = await mockRs(t, - pkg ? { prefixDir: { 'package.json': JSON.stringify(pkg) } } : {} - ) - const cmd = await npm.cmd('run-script') - return cmd.completion({ conf: { argv: { remain } } }) + const { runScript } = await mockRs(t, { + command: 'run-script', + ...(pkg ? { prefixDir: { 'package.json': JSON.stringify(pkg) } } : {}), + }) + + return runScript.completion({ conf: { argv: { remain } } }) } t.test('already have a script name', async t => { diff --git a/test/lib/commands/team.js b/test/lib/commands/team.js index a13a56d986e35..c838adc606e5c 100644 --- a/test/lib/commands/team.js +++ b/test/lib/commands/team.js @@ -384,8 +384,7 @@ t.test('team rm ', async t => { }) t.test('completion', async t => { - const { npm } = await mockTeam(t) - const { completion } = await npm.cmd('team') + const { team: { completion } } = await mockTeam(t, { command: 'team' }) t.test('npm team autocomplete', async t => { const res = await completion({ diff --git a/test/lib/commands/token.js b/test/lib/commands/token.js index 1fd686a4427c9..f6d9653baa117 100644 --- a/test/lib/commands/token.js +++ b/test/lib/commands/token.js @@ -33,8 +33,7 @@ const mockToken = async (t, { profile, getCredentialsByURI, readUserInfo, ...opt } t.test('completion', async t => { - const { npm } = await mockToken(t) - const { completion } = await npm.cmd('token') + const { token: { completion } } = await mockToken(t, { command: 'token' }) const testComp = (argv, expect) => { t.resolveMatch(completion({ conf: { argv: { remain: argv } } }), expect, argv.join(' ')) diff --git a/test/lib/commands/unpublish.js b/test/lib/commands/unpublish.js index cba7298475133..0af88afa209a8 100644 --- a/test/lib/commands/unpublish.js +++ b/test/lib/commands/unpublish.js @@ -397,13 +397,13 @@ t.test('publishConfig with spec', async t => { }) t.test('completion', async t => { - const { npm } = await loadMockNpm(t, { + const { npm, unpublish } = await loadMockNpm(t, { config: { ...auth, }, + command: 'unpublish', }) - const unpublish = await npm.cmd('unpublish') const testComp = async (t, { argv, partialWord, expect, title }) => { const res = await unpublish.completion( diff --git a/test/lib/commands/version.js b/test/lib/commands/version.js index 0f92282f7d1b7..929c0105eb6e4 100644 --- a/test/lib/commands/version.js +++ b/test/lib/commands/version.js @@ -55,8 +55,7 @@ t.test('node@1', async t => { }) t.test('completion', async t => { - const { npm } = await mockNpm(t) - const version = await npm.cmd('version') + const { version } = await mockNpm(t, { command: 'version' }) const testComp = async (argv, expect) => { const res = await version.completion({ conf: { argv: { remain: argv } } }) t.strictSame(res, expect, argv.join(' ')) diff --git a/test/lib/commands/view.js b/test/lib/commands/view.js index c6a4bf8fb79f4..762a56a5e725c 100644 --- a/test/lib/commands/view.js +++ b/test/lib/commands/view.js @@ -572,8 +572,7 @@ t.test('workspaces', async t => { }) t.test('completion', async t => { - const { npm } = await loadMockNpm(t) - const view = await npm.cmd('view') + const { view } = await loadMockNpm(t, { command: 'view' }) const res = await view.completion({ conf: { argv: { remain: ['npm', 'view', 'green@1.0.0'] } }, }) @@ -581,8 +580,7 @@ t.test('completion', async t => { }) t.test('no package completion', async t => { - const { npm } = await loadMockNpm(t) - const view = await npm.cmd('view') + const { view } = await loadMockNpm(t, { command: 'view' }) const res = await view.completion({ conf: { argv: { remain: ['npm', 'view'] } } }) t.notOk(res, 'there is no package completion') t.end() diff --git a/test/lib/docs.js b/test/lib/docs.js index 275b379245539..da83977df6ee4 100644 --- a/test/lib/docs.js +++ b/test/lib/docs.js @@ -82,7 +82,7 @@ t.test('usage', async t => { let output = null if (!bareCommands.includes(cmd)) { const { Npm } = await loadMockNpm(t, { load: false }) - const { describeUsage } = Npm.derefCommand(cmd) + const { describeUsage } = Npm.cmd(cmd) output = describeUsage } diff --git a/test/lib/npm.js b/test/lib/npm.js index ad7b5c97b9630..57a8828031d3b 100644 --- a/test/lib/npm.js +++ b/test/lib/npm.js @@ -52,6 +52,7 @@ t.test('npm.load', async t => { otherDirs: { newCache: {}, }, + globals: { platform: 'posix' }, }) t.equal(npm.loaded, true) @@ -59,28 +60,21 @@ t.test('npm.load', async t => { t.equal(npm.config.get('force'), false) t.ok(npm.usage, 'has usage') - t.match(npm, { - flatOptions: {}, - }) t.match(logs.timing.filter(([p]) => p === 'npm:load'), [ ['npm:load', /Completed in [0-9.]+ms/], ]) - mockGlobals(t, { process: { platform: 'posix' } }) - t.equal(resolve(npm.cache), resolve(cache), 'cache is cache') - npm.cache = other.newCache - t.equal(npm.config.get('cache'), other.newCache, 'cache setter sets config') - t.equal(npm.cache, other.newCache, 'cache getter gets new config') + t.match(npm, { flatOptions: {} }) t.equal(npm.lockfileVersion, 2, 'lockfileVersion getter') + + t.equal(resolve(npm.cache), resolve(cache), 'cache is cache') t.equal(npm.prefix, npm.localPrefix, 'prefix is local prefix') t.not(npm.prefix, npm.globalPrefix, 'prefix is not global prefix') - npm.globalPrefix = npm.prefix - t.equal(npm.prefix, npm.globalPrefix, 'globalPrefix setter') - npm.localPrefix = dir + '/extra/prefix' - t.equal(npm.prefix, npm.localPrefix, 'prefix is local prefix after localPrefix setter') - t.not(npm.prefix, npm.globalPrefix, 'prefix is not global prefix after localPrefix setter') - npm.prefix = dir + '/some/prefix' + npm.config.set('cache', other.newCache) + t.equal(resolve(npm.cache), other.newCache, 'cache setter sets config') + + npm.config.set('prefix', dir + '/some/prefix') t.equal(npm.prefix, npm.localPrefix, 'prefix is local prefix after prefix setter') t.not(npm.prefix, npm.globalPrefix, 'prefix is not global prefix after prefix setter') t.equal(npm.bin, npm.localBin, 'bin is local bin after prefix setter') @@ -96,19 +90,21 @@ t.test('npm.load', async t => { t.equal(npm.dir, npm.globalDir, 'dir is global dir after setting global') t.not(npm.dir, npm.localDir, 'dir is not local dir after setting global') - npm.prefix = dir + '/new/global/prefix' + npm.config.set('prefix', dir + '/new/global/prefix') t.equal(npm.prefix, npm.globalPrefix, 'prefix is global prefix after prefix setter') t.not(npm.prefix, npm.localPrefix, 'prefix is not local prefix after prefix setter') t.equal(npm.bin, npm.globalBin, 'bin is global bin after prefix setter') t.not(npm.bin, npm.localBin, 'bin is not local bin after prefix setter') + }) + + await t.test('basic loading windows', async t => { + const { npm } = await loadMockNpm(t, { + prefixDir: { node_modules: {} }, + globals: { platform: 'win32' }, + }) - mockGlobals(t, { process: { platform: 'win32' } }) t.equal(npm.bin, npm.globalBin, 'bin is global bin in windows mode') t.equal(npm.dir, npm.globalDir, 'dir is global dir in windows mode') - - const tmp = npm.tmp - t.match(tmp, String, 'npm.tmp is a string') - t.equal(tmp, npm.tmp, 'getter only generates it once') }) await t.test('forceful loading', async t => { @@ -127,7 +123,7 @@ t.test('npm.load', async t => { await t.test('node is a symlink', async t => { const node = process.platform === 'win32' ? 'node.exe' : 'node' - const { npm, logs, outputs, prefix } = await loadMockNpm(t, { + const { Npm, npm, logs, outputs, prefix } = await loadMockNpm(t, { prefixDir: { bin: t.fixture('symlink', dirname(process.execPath)), }, @@ -168,8 +164,8 @@ t.test('npm.load', async t => { t.equal(npm.command, 'll', 'command set to first npm command') t.equal(npm.flatOptions.npmCommand, 'll', 'npmCommand flatOption set') - const ll = await npm.cmd('ll') - t.same(outputs, [[ll.usage]], 'print usage') + const ll = Npm.cmd('ll') + t.same(outputs, [[ll.describeUsage]], 'print usage') npm.config.set('usage', false) outputs.length = 0 @@ -554,13 +550,13 @@ t.test('output clears progress and console.logs the message', async t => { }) t.test('aliases and typos', async t => { - const { npm } = await loadMockNpm(t, { load: false }) - await t.rejects(npm.cmd('thisisnotacommand'), { code: 'EUNKNOWNCOMMAND' }) - await t.rejects(npm.cmd(''), { code: 'EUNKNOWNCOMMAND' }) - await t.rejects(npm.cmd('birthday'), { code: 'EUNKNOWNCOMMAND' }) - await t.resolves(npm.cmd('it'), { name: 'install-test' }) - await t.resolves(npm.cmd('installTe'), { name: 'install-test' }) - await t.resolves(npm.cmd('access'), { name: 'access' }) + const { Npm } = await loadMockNpm(t, { init: false }) + t.throws(() => Npm.cmd('thisisnotacommand'), { code: 'EUNKNOWNCOMMAND' }) + t.throws(() => Npm.cmd(''), { code: 'EUNKNOWNCOMMAND' }) + t.throws(() => Npm.cmd('birthday'), { code: 'EUNKNOWNCOMMAND' }) + t.match(Npm.cmd('it'), { name: 'install-test' }) + t.match(Npm.cmd('installTe'), { name: 'install-test' }) + t.match(Npm.cmd('access'), { name: 'access' }) }) t.test('explicit workspace rejection', async t => { diff --git a/test/lib/utils/exit-handler.js b/test/lib/utils/exit-handler.js index f698f61348d08..01f84ccf6df05 100644 --- a/test/lib/utils/exit-handler.js +++ b/test/lib/utils/exit-handler.js @@ -50,13 +50,14 @@ mockGlobals(t, { }), }, { replace: true }) -const mockExitHandler = async (t, { init, load, testdir, config, mocks, files } = {}) => { +const mockExitHandler = async (t, { init, load, testdir, config, mocks, files, command } = {}) => { const errors = [] const { npm, logMocks, ...rest } = await loadMockNpm(t, { init, load, testdir, + command, mocks: { '{ROOT}/package.json': { version: '1.0.0', @@ -593,9 +594,7 @@ t.test('exits uncleanly when only emitting exit event', async (t) => { }) t.test('do no fancy handling for shellouts', async t => { - const { exitHandler, npm, logs } = await mockExitHandler(t) - - await npm.cmd('exec') + const { exitHandler, logs } = await mockExitHandler(t, { command: 'exec' }) const loudNoises = () => logs.filter(([level]) => ['warn', 'error'].includes(level)) diff --git a/test/lib/utils/open-url-prompt.js b/test/lib/utils/open-url-prompt.js index 0d21f4a9f4953..d93af30f49b91 100644 --- a/test/lib/utils/open-url-prompt.js +++ b/test/lib/utils/open-url-prompt.js @@ -1,138 +1,111 @@ const t = require('tap') -const mockGlobals = require('@npmcli/mock-globals') const EventEmitter = require('events') const tmock = require('../../fixtures/tmock') - -const OUTPUT = [] -const output = (...args) => OUTPUT.push(args) -const npm = { - _config: { - json: false, - browser: true, - }, - config: { - get: k => npm._config[k], - set: (k, v) => { - npm._config[k] = v +const mockNpm = require('../../fixtures/mock-npm') + +const mockOpenUrlPrompt = async (t, { + questionShouldResolve = true, + openUrlPromptInterrupted = false, + openerResult = null, + isTTY = true, + emitter = null, + url: openUrl = 'https://www.npmjs.com', + ...config +}) => { + const mock = await mockNpm(t, { + globals: { + 'process.stdin.isTTY': isTTY, + 'process.stdout.isTTY': isTTY, }, - }, - output, -} - -let openerUrl = null -let openerOpts = null -let openerResult = null - -let questionShouldResolve = true -let openUrlPromptInterrupted = false + config, + }) -const readline = { - createInterface: () => ({ - question: (_q, cb) => { - if (questionShouldResolve === true) { - cb() - } + let openerUrl = null + let openerOpts = null + + const openUrlPrompt = tmock(t, '{LIB}/utils/open-url-prompt.js', { + '@npmcli/promise-spawn': { + open: async (url, options) => { + openerUrl = url + openerOpts = options + if (openerResult) { + throw openerResult + } + }, }, - close: () => {}, - on: (_signal, cb) => { - if (openUrlPromptInterrupted && _signal === 'SIGINT') { - cb() - } + readline: { + createInterface: () => ({ + question: (_q, cb) => { + if (questionShouldResolve === true) { + cb() + } + }, + close: () => {}, + on: (_signal, cb) => { + if (openUrlPromptInterrupted && _signal === 'SIGINT') { + cb() + } + }, + }), }, - }), -} + }) -const openUrlPrompt = tmock(t, '{LIB}/utils/open-url-prompt.js', { - '@npmcli/promise-spawn': { - open: async (url, options) => { - openerUrl = url - openerOpts = options - if (openerResult) { - throw openerResult - } - }, - }, - readline, -}) + let error + const args = [mock.npm, openUrl, 'npm home', 'prompt'] + if (emitter) { + mock.open = openUrlPrompt(...args, emitter) + } else { + await openUrlPrompt(...args).catch((er) => error = er) + } -mockGlobals(t, { - 'process.stdin.isTTY': true, - 'process.stdout.isTTY': true, -}) + return { + ...mock, + openerUrl, + openerOpts, + OUTPUT: mock.joinedOutput(), + emitter, + error, + } +} t.test('does not open a url in non-interactive environments', async t => { - t.teardown(() => { - openerUrl = null - openerOpts = null - OUTPUT.length = 0 - }) + const { openerUrl, openerOpts } = await mockOpenUrlPrompt(t, { isTTY: false }) - mockGlobals(t, { - 'process.stdin.isTTY': false, - 'process.stdout.isTTY': false, - }) - - await openUrlPrompt(npm, 'https://www.npmjs.com', 'npm home', 'prompt') t.equal(openerUrl, null, 'did not open') t.same(openerOpts, null, 'did not open') }) t.test('opens a url', async t => { - t.teardown(() => { - openerUrl = null - openerOpts = null - OUTPUT.length = 0 - npm._config.browser = true - }) + const { OUTPUT, openerUrl, openerOpts } = await mockOpenUrlPrompt(t, { browser: true }) - npm._config.browser = 'browser' - await openUrlPrompt(npm, 'https://www.npmjs.com', 'npm home', 'prompt') t.equal(openerUrl, 'https://www.npmjs.com', 'opened the given url') t.same(openerOpts, { command: 'browser' }, 'passed command as null (the default)') t.matchSnapshot(OUTPUT) }) t.test('prints json output', async t => { - t.teardown(() => { - openerUrl = null - openerOpts = null - OUTPUT.length = 0 - npm._config.json = false - }) + const { OUTPUT } = await mockOpenUrlPrompt(t, { json: true }) - npm._config.json = true - await openUrlPrompt(npm, 'https://www.npmjs.com', 'npm home', 'prompt') t.matchSnapshot(OUTPUT) }) t.test('returns error for non-https url', async t => { - t.teardown(() => { - openerUrl = null - openerOpts = null - OUTPUT.length = 0 + const { error, OUTPUT, openerUrl, openerOpts } = await mockOpenUrlPrompt(t, { + url: 'ftp://www.npmjs.com', }) - await t.rejects( - openUrlPrompt(npm, 'ftp://www.npmjs.com', 'npm home', 'prompt'), - /Invalid URL/, - 'got the correct error' - ) + + t.match(error, /Invalid URL/, 'got the correct error') t.equal(openerUrl, null, 'did not open') t.same(openerOpts, null, 'did not open') t.same(OUTPUT, [], 'printed no output') }) t.test('does not open url if canceled', async t => { - t.teardown(() => { - openerUrl = null - openerOpts = null - OUTPUT.length = 0 - questionShouldResolve = true - }) - - questionShouldResolve = false const emitter = new EventEmitter() - - const open = openUrlPrompt(npm, 'https://www.npmjs.com', 'npm home', 'prompt', emitter) + const { openerUrl, openerOpts, open } = await mockOpenUrlPrompt(t, { + questionShouldResolve: false, + emitter, + }) emitter.emit('abort') @@ -143,41 +116,21 @@ t.test('does not open url if canceled', async t => { }) t.test('returns error when opener errors', async t => { - t.teardown(() => { - openerUrl = null - openerOpts = null - openerResult = null - OUTPUT.length = 0 + const { error, openerUrl } = await mockOpenUrlPrompt(t, { + openerResult: new Error('Opener failed'), }) - openerResult = new Error('Opener failed') - - await t.rejects( - openUrlPrompt(npm, 'https://www.npmjs.com', 'npm home', 'prompt'), - /Opener failed/, - 'got the correct error' - ) + t.match(error, /Opener failed/, 'got the correct error') t.equal(openerUrl, 'https://www.npmjs.com', 'did not open') }) t.test('throws "canceled" error on SIGINT', async t => { - t.teardown(() => { - openerUrl = null - openerOpts = null - OUTPUT.length = 0 - questionShouldResolve = true - openUrlPromptInterrupted = false - }) - - questionShouldResolve = false - openUrlPromptInterrupted = true const emitter = new EventEmitter() + const { open } = await mockOpenUrlPrompt(t, { + questionShouldResolve: false, + openUrlPromptInterrupted: true, + emitter, + }) - const open = openUrlPrompt(npm, 'https://www.npmjs.com', 'npm home', 'prompt', emitter) - - try { - await open - } catch (err) { - t.equal(err.message, 'canceled') - } + await t.rejects(open, /canceled/, 'message is canceled') }) diff --git a/test/lib/utils/reify-finish.js b/test/lib/utils/reify-finish.js index ee112203a24bc..d558018a72193 100644 --- a/test/lib/utils/reify-finish.js +++ b/test/lib/utils/reify-finish.js @@ -1,81 +1,73 @@ const t = require('tap') +const fs = require('fs') +const { join } = require('path') const { cleanNewlines } = require('../../fixtures/clean-snapshot') const tmock = require('../../fixtures/tmock') +const mockNpm = require('../../fixtures/mock-npm') -const npm = { - config: { - data: { - get: () => builtinConfMock, +const mockReififyFinish = async (t, { actualTree = {}, ...config }) => { + const mock = await mockNpm(t, { + npm: ({ other }) => ({ + npmRoot: other, + }), + otherDirs: { + npmrc: `key=value`, }, - }, -} + config, + }) -const builtinConfMock = { - loadError: new Error('no builtin config'), - raw: { hasBuiltinConfig: true, x: 'y', nested: { foo: 'bar' } }, -} + const reifyFinish = tmock(t, '{LIB}/utils/reify-finish.js', { + '{LIB}/utils/reify-output.js': () => {}, + }) -const reifyOutput = () => {} + await reifyFinish(mock.npm, { + options: { global: config.global }, + actualTree: typeof actualTree === 'function' ? actualTree(mock) : actualTree, + }) -let expectWrite = false -const realFs = require('fs') -const fs = { - ...realFs, - promises: realFs.promises && { - ...realFs.promises, - writeFile: async (path, data) => { - if (!expectWrite) { - throw new Error('did not expect to write builtin config file') - } - return realFs.promises.writeFile(path, data) - }, - }, + return mock } -const reifyFinish = tmock(t, '{LIB}/utils/reify-finish.js', { - fs, - '{LIB}/utils/reify-output.js': reifyOutput, -}) - -t.test('should not write if not global', async t => { - expectWrite = false - await reifyFinish(npm, { - options: { global: false }, - actualTree: {}, +t.test('ok by default', async t => { + const mock = await mockReififyFinish(t, { + global: false, }) + t.strictSame(mock.npm.config.data.get('builtin').data, {}) }) t.test('should not write if no global npm module', async t => { - expectWrite = false - await reifyFinish(npm, { - options: { global: true }, + const mock = await mockReififyFinish(t, { + global: true, actualTree: { inventory: new Map(), }, }) + t.strictSame(mock.npm.config.data.get('builtin').data, {}) }) t.test('should not write if builtin conf had load error', async t => { - expectWrite = false - await reifyFinish(npm, { - options: { global: true }, + const mock = await mockReififyFinish(t, { + global: true, actualTree: { inventory: new Map([['node_modules/npm', {}]]), }, }) + t.strictSame(mock.npm.config.data.get('builtin').data, {}) }) t.test('should write if everything above passes', async t => { - expectWrite = true - delete builtinConfMock.loadError - const path = t.testdir() - await reifyFinish(npm, { - options: { global: true }, - actualTree: { - inventory: new Map([['node_modules/npm', { path }]]), - }, + const mock = await mockReififyFinish(t, { + global: true, + actualTree: ({ other }) => ({ + inventory: new Map([['node_modules/npm', { path: join(other, 'new-npm') }]]), + }), }) + + t.strictSame(mock.npm.config.data.get('builtin').data, {}) + // windowwwwwwssss!!!!! - const data = cleanNewlines(fs.readFileSync(`${path}/npmrc`, 'utf8')) - t.matchSnapshot(data, 'written config') + const oldFile = cleanNewlines(fs.readFileSync(join(mock.other, 'npmrc'), 'utf8')) + const newFile = cleanNewlines(fs.readFileSync(join(mock.other, 'new-npm/npmrc'), 'utf8')) + t.equal(oldFile, newFile) + t.matchSnapshot(newFile, 'written config') }) diff --git a/test/lib/utils/update-notifier.js b/test/lib/utils/update-notifier.js index 386541b84e8a7..cc7273666a3cc 100644 --- a/test/lib/utils/update-notifier.js +++ b/test/lib/utils/update-notifier.js @@ -79,8 +79,6 @@ const runUpdateNotifier = async (t, { }) const updateNotifier = tmock(t, '{LIB}/utils/update-notifier.js', mocks) - await mock[command].cmd() - const result = await updateNotifier(mock.npm) return { diff --git a/workspaces/config/lib/config-data.js b/workspaces/config/lib/config-data.js index 8103050cbce53..558dd6331c209 100644 --- a/workspaces/config/lib/config-data.js +++ b/workspaces/config/lib/config-data.js @@ -27,6 +27,7 @@ class ConfigData extends Map { #file = null #loaded = null #valid = true + #error = null constructor (type, { parent, data, envReplace }) { super() @@ -121,6 +122,7 @@ class ConfigData extends Map { if (error.code !== 'ENOENT') { log.verbose('config', `error loading ${this.where} config`, error) } + this.#error = error return } @@ -187,7 +189,7 @@ class ConfigData extends Map { log.warn('config', msg) } - async save () { + async save (newFile) { this.#assertLoaded() if (!this.file) { @@ -195,6 +197,17 @@ class ConfigData extends Map { `\`${this.where}\` from \`${this.#description}\``) } + if (this.#error) { + // Dont save a file that had an error while loading + return + } + + if (newFile) { + // allow saving a config file to a new location. used by reify-finish + // to preserve builtin config when installing global npm + this.#file = newFile + } + const { user } = this.#parent.constructor.Locations if (this.where === user) { // if email is nerfed, then we want to de-nerf it diff --git a/workspaces/config/lib/index.js b/workspaces/config/lib/index.js index 3dc46ac8e456f..a456ef7cfdcf2 100644 --- a/workspaces/config/lib/index.js +++ b/workspaces/config/lib/index.js @@ -148,6 +148,10 @@ class Config { return this.#configData.data } + get data () { + return this.#configData + } + get valid () { for (const conf of this.#configData.values()) { if (!conf.validate()) { From a0eb7e1bcbc00d790dec1282750d3be1a45d7b5b Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Thu, 12 Jan 2023 13:26:55 -0700 Subject: [PATCH 8/9] moar --- lib/cli/index.js | 2 +- lib/npm.js | 19 +- node_modules/nopt/lib/nopt-lib.js | 12 +- node_modules/nopt/lib/nopt.js | 4 + package-lock.json | 5 +- test/fixtures/mock-npm.js | 2 +- test/lib/npm.js | 36 +- workspaces/config/lib/config-data.js | 9 +- workspaces/config/lib/config-locations.js | 123 +- .../config/lib/definitions/definition.js | 182 +- .../config/lib/definitions/definitions.js | 1951 ++++++++ workspaces/config/lib/definitions/derived.js | 205 + workspaces/config/lib/definitions/index.js | 2242 +-------- .../config/lib/definitions/shorthands.js | 26 + .../config/lib/definitions/type-defs.js | 278 ++ workspaces/config/lib/definitions/values.js | 20 + workspaces/config/lib/index.js | 396 +- workspaces/config/lib/set-globals.js | 2 +- workspaces/config/lib/type-defs.js | 239 - workspaces/config/package.json | 1 + .../test/definitions/snapshots.js.test.cjs | 4033 +++++++++++++++++ .../tap-snapshots/test/index.js.test.cjs | 240 - .../test/type-description.js.test.cjs | 449 -- workspaces/config/test/config/definition.js | 218 - workspaces/config/test/config/definitions.js | 933 ---- workspaces/config/test/config/index.js | 63 - .../config/test/definitions/snapshots.js | 57 + workspaces/config/test/fixtures/cafile | 32 - workspaces/config/test/index.js | 1316 ------ workspaces/config/test/set-envs.js | 212 - 30 files changed, 7108 insertions(+), 6199 deletions(-) create mode 100644 workspaces/config/lib/definitions/definitions.js create mode 100644 workspaces/config/lib/definitions/derived.js create mode 100644 workspaces/config/lib/definitions/shorthands.js create mode 100644 workspaces/config/lib/definitions/type-defs.js create mode 100644 workspaces/config/lib/definitions/values.js delete mode 100644 workspaces/config/lib/type-defs.js create mode 100644 workspaces/config/tap-snapshots/test/definitions/snapshots.js.test.cjs delete mode 100644 workspaces/config/tap-snapshots/test/index.js.test.cjs delete mode 100644 workspaces/config/tap-snapshots/test/type-description.js.test.cjs delete mode 100644 workspaces/config/test/config/definition.js delete mode 100644 workspaces/config/test/config/definitions.js delete mode 100644 workspaces/config/test/config/index.js create mode 100644 workspaces/config/test/definitions/snapshots.js delete mode 100644 workspaces/config/test/fixtures/cafile delete mode 100644 workspaces/config/test/set-envs.js diff --git a/lib/cli/index.js b/lib/cli/index.js index 7d931cfb19993..57453f082e830 100644 --- a/lib/cli/index.js +++ b/lib/cli/index.js @@ -32,7 +32,7 @@ module.exports = async (process, validateEngines) => { // only used in tests but if this feature would be added in the future // it would need to be implemented something like this since the // directory would need to be available before configs are parsed - cwdRoot: null, + localPrefixRoot: null, })) // only log node and npm paths in argv initially since argv can contain diff --git a/lib/npm.js b/lib/npm.js index a06f124058af7..1f27ff6f0dc26 100644 --- a/lib/npm.js +++ b/lib/npm.js @@ -52,9 +52,9 @@ class Npm { }, }) - constructor ({ npmRoot, argv, cwdRoot }) { + constructor ({ npmRoot, argv, localPrefixRoot }) { this.#npmRoot = npmRoot - this.config = new Config({ npmRoot, argv, cwdRoot }) + this.config = new Config({ builtinRoot: npmRoot, argv, localPrefixRoot }) // config has parsed argv and now knows the command name // and the remaining args this.#argvCommand = this.config.command @@ -94,14 +94,15 @@ class Npm { // similar to how the CLI does by passing in an argv containing the command // and args. async exec (commandName = this.#argvCommand, args = this.#argvArgs) { - const Impl = Npm.cmd(commandName) - const command = new Impl(this) + const runNotifier = !this.#command + + const Command = Npm.cmd(commandName) + this.#command = new Command(this) + this.config.set('npm-command', this.command) // exec can be re-entered by an already running command but if it is the - // first time we want to kickoff the update notifier and set the command - if (!this.#command) { - this.#command = command - this.config.set('npm-command', this.command) + // first time we want to kickoff the update notifier + if (runNotifier) { // this is async but we dont await it, since its ok if it doesnt // finish before the command finishes running. it uses command and argv // so it must be initiated here, after the command name is set @@ -109,7 +110,7 @@ class Npm { updateNotifier(this).then((msg) => (this.updateNotification = msg)) } - return this.time(`command:${command.name}`, () => command.cmdExec(args)) + return this.time(`command:${this.command}`, () => this.#command.cmdExec(args)) } get loaded () { diff --git a/node_modules/nopt/lib/nopt-lib.js b/node_modules/nopt/lib/nopt-lib.js index 89d269fb43f1a..29583719bf394 100644 --- a/node_modules/nopt/lib/nopt-lib.js +++ b/node_modules/nopt/lib/nopt-lib.js @@ -2,7 +2,7 @@ var abbrev = require('abbrev') const debug = require('./debug') const defaultTypeDefs = require('./type-defs') -function nopt (args, { types, shorthands, typeDefs, invalidHandler }) { +function nopt (args, { types, shorthands, typeDefs, invalidHandler, typeDefault }) { debug(types, shorthands, args, typeDefs) var data = {} @@ -15,7 +15,7 @@ function nopt (args, { types, shorthands, typeDefs, invalidHandler }) { parse(args, data, argv.remain, { typeDefs, types, shorthands }) // now data is full - clean(data, { types, typeDefs, invalidHandler }) + clean(data, { types, typeDefs, invalidHandler, typeDefault }) data.argv = argv Object.defineProperty(data.argv, 'toString', { @@ -28,15 +28,13 @@ function nopt (args, { types, shorthands, typeDefs, invalidHandler }) { return data } -function clean (data, { types, typeDefs, invalidHandler }) { - const StringType = typeDefs.String.type +function clean (data, { types, typeDefs, invalidHandler, typeDefault }) { const NumberType = typeDefs.Number.type const ArrayType = typeDefs.Array.type const BooleanType = typeDefs.Boolean.type const DateType = typeDefs.Date.type var remove = {} - var typeDefault = [false, true, null, StringType, ArrayType] Object.keys(data).forEach(function (k) { if (k === 'argv') { @@ -82,6 +80,7 @@ function clean (data, { types, typeDefs, invalidHandler }) { } if (!Object.prototype.hasOwnProperty.call(types, k)) { + debug('xxxxxxxxxxxxxxxxxxxxx') return v } @@ -126,6 +125,8 @@ function clean (data, { types, typeDefs, invalidHandler }) { function validate (data, k, val, type, { typeDefs }) { const ArrayType = typeDefs.Array.type + + debug('type array', Array.isArray(type)) // arrays are lists of types. if (Array.isArray(type)) { for (let i = 0, l = type.length; i < l; i++) { @@ -176,6 +177,7 @@ function validate (data, k, val, type, { typeDefs }) { (type.name === t.type.name) : (type === t.type) )) { + debug('validating type %j', t) var d = {} ok = t.validate(d, k, val) !== false val = d[k] diff --git a/node_modules/nopt/lib/nopt.js b/node_modules/nopt/lib/nopt.js index 76de480cc3268..8cbad34191af6 100644 --- a/node_modules/nopt/lib/nopt.js +++ b/node_modules/nopt/lib/nopt.js @@ -12,12 +12,15 @@ exports.clean = clean exports.typeDefs = defaultTypeDefs exports.lib = lib +const typeDefault = [false, true, null, exports.typeDefs.String.Type, exports.typeDefs.Array.Type] + function nopt (types = {}, shorthands = {}, args = process.argv, slice = 2) { return lib.nopt(args.slice(slice), { types, shorthands: shorthands || {}, typeDefs: exports.typeDefs, invalidHandler: exports.invalidHandler, + typeDefault, }) } @@ -26,5 +29,6 @@ function clean (data, types, typeDefs = exports.typeDefs) { types, typeDefs, invalidHandler: exports.invalidHandler, + typeDefault, }) } diff --git a/package-lock.json b/package-lock.json index 8887bb97a2575..f0e317775cbb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -183,11 +183,9 @@ "name": "@npmcli/docs", "version": "1.0.0", "license": "ISC", - "dependencies": { - "@npmcli/config": "^6.1.0" - }, "devDependencies": { "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/config": "^6.1.0", "@npmcli/eslint-config": "^4.0.0", "@npmcli/template-oss": "4.11.0", "front-matter": "^4.0.2", @@ -14395,6 +14393,7 @@ "dependencies": { "@npmcli/arborist": "^6.1.5", "@npmcli/map-workspaces": "^3.0.0", + "@npmcli/mock-globals": "^1.0.0", "ci-info": "^3.7.1", "ini": "^3.0.0", "nopt": "file:../../../nopt/nopt-7.0.0.tgz", diff --git a/test/fixtures/mock-npm.js b/test/fixtures/mock-npm.js index e26de369c304e..cb8ebbf6af2c8 100644 --- a/test/fixtures/mock-npm.js +++ b/test/fixtures/mock-npm.js @@ -242,7 +242,7 @@ const setupMockNpm = async (t, { init, load, mocks: withDirs(mocks), - npm: { argv, npmRoot, cwdRoot: dir, ...withDirs(npmOpts) }, + npm: { argv, npmRoot, localPrefixRoot: dir, ...withDirs(npmOpts) }, }) t.teardown(() => { diff --git a/test/lib/npm.js b/test/lib/npm.js index 57a8828031d3b..689d659705b12 100644 --- a/test/lib/npm.js +++ b/test/lib/npm.js @@ -25,25 +25,9 @@ t.test('not yet loaded', async t => { t.test('npm.load', async t => { await t.test('load error', async t => { const { npm } = await loadMockNpm(t, { load: false }) - const loadError = new Error('load error') - npm.config.load = async () => { - throw loadError - } - await t.rejects( - () => npm.load(), - /load error/ - ) - t.equal(npm.loadErr, loadError) - npm.config.load = async () => { - throw new Error('different error') - } - await t.rejects( - () => npm.load(), - /load error/, - 'loading again returns the original error' - ) - t.equal(npm.loadErr, loadError) + await t.resolves(npm.load(), 'can load once') + await t.rejects(npm.load(), /not be loaded/, 'can only load once') }) await t.test('basic loading', async t => { @@ -68,18 +52,24 @@ t.test('npm.load', async t => { t.equal(npm.lockfileVersion, 2, 'lockfileVersion getter') t.equal(resolve(npm.cache), resolve(cache), 'cache is cache') + t.ok(npm.prefix, 'has prefix') + t.ok(npm.localPrefix, 'has local prefix') + t.ok(npm.globalPrefix, 'has global prefix') t.equal(npm.prefix, npm.localPrefix, 'prefix is local prefix') t.not(npm.prefix, npm.globalPrefix, 'prefix is not global prefix') - npm.config.set('cache', other.newCache) - t.equal(resolve(npm.cache), other.newCache, 'cache setter sets config') + npm.config.set('cache', join(other, 'newCache')) + t.equal(resolve(npm.cache), join(other, 'newCache'), 'cache setter sets config') - npm.config.set('prefix', dir + '/some/prefix') + console.log(npm.prefix) + npm.config.set('prefix', resolve(dir, 'some/prefix')) + console.log(npm.prefix) + t.equal(resolve(npm.prefix), resolve(dir, 'some/prefix')) t.equal(npm.prefix, npm.localPrefix, 'prefix is local prefix after prefix setter') - t.not(npm.prefix, npm.globalPrefix, 'prefix is not global prefix after prefix setter') t.equal(npm.bin, npm.localBin, 'bin is local bin after prefix setter') - t.not(npm.bin, npm.globalBin, 'bin is not global bin after prefix setter') t.equal(npm.dir, npm.localDir, 'dir is local dir after prefix setter') + t.not(npm.prefix, npm.globalPrefix, 'prefix is not global prefix after prefix setter') + t.not(npm.bin, npm.globalBin, 'bin is not global bin after prefix setter') t.not(npm.dir, npm.globalDir, 'dir is not global dir after prefix setter') npm.config.set('global', true) diff --git a/workspaces/config/lib/config-data.js b/workspaces/config/lib/config-data.js index 558dd6331c209..447400be6fda0 100644 --- a/workspaces/config/lib/config-data.js +++ b/workspaces/config/lib/config-data.js @@ -4,7 +4,7 @@ const ini = require('ini') const fs = require('fs/promises') const { dirname } = require('path') const nerfDart = require('./nerf-dart') -const { typeDefs } = require('./type-defs') +const { typeDefs } = require('./definitions/type-defs') const { definitions, shorthands, types } = require('./definitions') const SYMBOLS = { @@ -180,13 +180,16 @@ class ConfigData extends Map { } #invalidHandler (key, val) { - console.log(key, val, this.where) this.#valid = false const def = definitions[key] const msg = def ? `invalid item \`${key}\`, ${definitions[key].mustBe()} and got \`${val}\`` : `unknown item \`${key}\`` - log.warn('config', msg) + if (this.#type.throw) { + throw new Error(msg) + } else { + log.warn('config', msg) + } } async save (newFile) { diff --git a/workspaces/config/lib/config-locations.js b/workspaces/config/lib/config-locations.js index 20dacad604c32..13629f9eefe25 100644 --- a/workspaces/config/lib/config-locations.js +++ b/workspaces/config/lib/config-locations.js @@ -1,21 +1,22 @@ const ConfigData = require('./config-data') -const { camelCase } = require('./definitions/definition') +const { getFlatKey } = require('./definitions/definition') const { definitions, definitionKeys, derived, derivedKeys, valueKeys, + values, } = require('./definitions') -// TODO: flatten based on key +// TODO: flatten based on key match // if (/@.*:registry$/i.test(key) || /^\/\//.test(key)) { // flat[key] = val // } // this is in order from least -> most precedence const LocationsList = Object.entries({ - default: { description: `npm's default values`, allowDeprecated: true }, + default: { description: `npm's default values`, allowDeprecated: true, throw: true }, builtin: { description: `npm's builtin npmrc file` }, global: { description: 'global .npmrc file', validateAuth: true }, user: { description: 'user .npmrc file', validateAuth: true, mode: 0o600 }, @@ -30,6 +31,29 @@ const Locations = LocationsList.reduce((acc, [location]) => { return acc }, {}) +const hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k) + +const cacheDescriptor = ({ key, cache }, getValue) => ({ + configurable: false, + enumerable: true, + get: () => { + if (cache.has(key)) { + return cache.get(key) + } + const value = getValue() + cache.set(key, value) + return value + }, +}) + +const defineBaseAndFlat = (obj, key, descriptor) => { + Object.defineProperty(obj, key, descriptor) + const flatKey = getFlatKey(key) + if (key !== flatKey) { + Object.defineProperty(obj, flatKey, descriptor) + } +} + class ConfigLocations extends Map { static Locations = Locations @@ -57,7 +81,7 @@ class ConfigLocations extends Map { } for (const key of valueKeys) { - this.#createBaseDescriptor(key) + this.#createValueDescriptor(key) } for (const key of derivedKeys) { @@ -112,7 +136,7 @@ class ConfigLocations extends Map { } // cli -> defaults - * reverseValues (startWhere) { + * #reverseValues (startWhere) { const index = startWhere ? this.#revList.length - 1 - this.#indexes[startWhere] : 0 const locations = index ? this.#revList.slice(index) : this.#revList for (const where of locations) { @@ -121,7 +145,7 @@ class ConfigLocations extends Map { } find (where, key) { - for (const config of this.reverseValues(where)) { + for (const config of this.#reverseValues(where)) { if (config.has(key)) { return config.where } @@ -131,7 +155,17 @@ class ConfigLocations extends Map { getData (where, key) { if (where === null) { - for (const config of this.reverseValues()) { + const [found, value] = this.#getDerivedData(key) + if (found) { + return value + } + } + return this.#getBaseData(where, key) + } + + #getBaseData (where, key) { + if (where === null) { + for (const config of this.#reverseValues()) { if (config.has(key)) { return config.get(key) } @@ -141,9 +175,18 @@ class ConfigLocations extends Map { return this.get(where).get(key) } + #getDerivedData (k, data = this.#data) { + const key = getFlatKey(k) + const split = key.indexOf('.') + if (split !== -1) { + return this.#getDerivedData(key.slice(split + 1), data[key.slice(0, split)]) + } + return hasOwn(data, key) ? [true, data[key]] : [false] + } + hasData (where, key) { if (where === null) { - for (const config of this.reverseValues()) { + for (const config of this.#reverseValues()) { if (config.has(key)) { return true } @@ -165,48 +208,50 @@ class ConfigLocations extends Map { #mutateData (key) { this.#base.delete(key) + this.#derived.delete(key) const definition = definitions[key] - if (definition) { - for (const s of definition.derived) { - this.#derived.delete(s) - } + for (const s of definition?.derived || []) { + this.#derived.delete(s) } } // TODO: move nerfdart auth stuff into a nested object that // is only passed along to paths that end up calling npm-registry-fetch. - #createBaseDescriptor (k) { - const descriptor = { - configurable: true, - enumerable: true, - get: () => { - if (this.#base.has(k)) { - return this.#base.get(k) - } - const value = this.getData(null, k) - this.#base.set(k, value) - return value - }, - } - Object.defineProperty(this.#baseData, k, descriptor) - Object.defineProperty(this.#baseData, camelCase(k), descriptor) + #createBaseDescriptor (key, data = this.#baseData) { + defineBaseAndFlat(data, key, cacheDescriptor( + { key, cache: this.#base }, + () => this.#getBaseData(null, key) + )) } - #createDerivedDescriptor (k) { - const derive = derived[k] - Object.defineProperty(this.#data, camelCase(k), { - configurable: true, + #createValueDescriptor (key) { + Object.defineProperty(this.#data, getFlatKey(key), { + configurable: false, enumerable: true, - get: () => { - if (this.#derived.has(k)) { - return this.#derived.get(k) - } - const value = derive.set(this.#baseData, this.#config) - this.#derived.set(k, value) - return value - }, + value: values[key], }) } + + #createDerivedDescriptor (key, data = this.#data) { + const split = key.indexOf('.') + if (split !== -1) { + const [parentKey, childKey] = [key.slice(0, split), key.slice(split + 1)] + if (!hasOwn(data, parentKey)) { + defineBaseAndFlat(data, parentKey, { + configurable: false, + enumerable: true, + value: {}, + }) + } + return this.#createBaseDescriptor(childKey, data[parentKey]) + } + + const derive = derived[key] + Object.defineProperty(data, derive.flatKey, cacheDescriptor( + { key, cache: this.#derived }, + () => derive.get(this.#baseData, this.#config) + )) + } } module.exports = ConfigLocations diff --git a/workspaces/config/lib/definitions/definition.js b/workspaces/config/lib/definitions/definition.js index a3799664cbc2a..63469ffa01439 100644 --- a/workspaces/config/lib/definitions/definition.js +++ b/workspaces/config/lib/definitions/definition.js @@ -6,50 +6,57 @@ // say "these are for registry access", "these are for // version resolution" etc. -const { Types, getType } = require('../type-defs') +const { Types, getType } = require('./type-defs') const hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k) -const camelCase = (k) => k.replace(/-(ssl|[a-z])/g, (...a) => a[1].toUpperCase()) +// special affordance for ssl -> SSL +const getFlatKey = (k) => k.replace(/-(ssl|[a-z])/g, (...a) => a[1].toUpperCase()) class Derived { - #set = null + #get = null + #flatKey = null #sources = null - get set () { - return this.#set + get get () { + return this.#get } get sources () { return [...this.#sources.values()] } - constructor (key, { key: defKey, value, set, defSources, nested, sources } = {}) { + get flatKey () { + return this.#flatKey + } + + constructor (key, { key: defKey, value, get, defSources, nested, sources } = {}) { + this.#flatKey = getFlatKey(key) + if (defKey) { - const camelKey = camelCase(defKey) - this.#set = (d) => d[camelKey] + const defFlatKey = getFlatKey(defKey) + this.#get = (d) => d[defFlatKey] if (sources) { throw new Error('Derived configs based on a key cannot have other sources') } } else if (value !== undefined) { - this.#set = () => value + this.#get = () => value if (sources) { throw new Error('Derived configs based on a value cannot have other sources') } - } else if (typeof set === 'function') { - this.#set = set + } else if (typeof get === 'function') { + this.#get = get } - if (!this.#set) { - throw new Error(`Invalid value for derived key ${key} set: ${set}`) + if (!this.#get) { + throw new Error(`Invalid value for derived key ${key} get: ${get}`) } if (nested) { - const originalFn = this.#set - const camelKey = camelCase(key) - this.#set = (...args) => originalFn(...args)[camelKey] + const originalFn = this.#get + this.#get = (...args) => originalFn(...args)[this.#flatKey] } - this.#sources = new Set([...(sources || []), ...defSources]) + this.#sources = new Set([key, ...(sources || []), ...defSources]) } } @@ -78,10 +85,24 @@ class Definition { this.#derived.add(key) } else if (typeof def.flatten === 'string') { this.#derived.add(def.flatten) - } else if (Array.isArray(def.flatten)) { + } else if ( + Array.isArray(def.flatten) && + def.flatten.every(f => f === true || typeof f === 'string') + ) { for (const f of def.flatten) { this.#derived.add(f) } + } else if (def.flatten) { + throw new Error('flatten must be true, a string or an array of those values') + } + + if (!Array.isArray(this.#def.type)) { + this.#def.type = [this.#def.type] + } + + // always add null to types if its the default + if (this.#def.default === null && !this.#def.type.includes(null)) { + this.#def.type.unshift(null) } // needs a key @@ -91,7 +112,7 @@ class Definition { // needs required keys for (const req of Definition.required) { - if (typeof req === 'string' && !this.#hasOwn(req)) { + if (typeof req === 'string' && !hasOwn(this.#def, req)) { throw new Error(`config \`${this.#key}\` lacks required key: \`${req}\``) } } @@ -104,12 +125,8 @@ class Definition { } } - #hasOwn (k) { - return hasOwn(this.#def, k) - } - get default () { - return this.#def.default + return describeValue(this.#def.default) } get deprecated () { @@ -126,7 +143,11 @@ class Definition { } get isBoolean () { - return this.#typeDefs.some(t => t?.isBoolean) + return this.#typeDefs.some(t => t?.isBoolean || typeof t === 'boolean') + } + + get hasNonBoolean () { + return this.#typeDefs.some(t => !(t?.isBoolean || typeof t === 'boolean')) } get type () { @@ -137,22 +158,6 @@ class Definition { return [...this.#derived.values()] } - get #types () { - return [].concat(this.#def.type) - } - - get #typeMultiple () { - return this.#types.includes(Types.Array) - } - - get #typeDefs () { - return this.#types.map((t) => getType(t) ?? t) - } - - get #defaultDescription () { - return this.#def.defaultDescription ?? describeValue(this.#def.default) - } - addDerived (...keys) { for (const k of keys) { this.#derived.add(k) @@ -163,10 +168,18 @@ class Definition { return this.#derived.has(k) } + get #typeMultiple () { + return this.type.includes(Types.Array) + } + + get #typeDefs () { + return this.type.map((t) => getType(t) ?? t) + } + // a textual description of this config, suitable for help output describe () { const sections = [ - ['Default', this.#defaultDescription], + ['Default', this.#def.defaultDescription ?? this.default], ['Type', this.#describeTypes()], this.deprecated ? ['DEPRECATED', this.deprecated] : null, '', @@ -186,74 +199,69 @@ class Definition { mustBe () { const allowMultiple = this.#typeMultiple - const types = this.#types.includes(Types.Url) ? [Types.Url] - // no actual configs matching this, but path types SHOULD be handled + const types = this.type.includes(Types.URL) ? [Types.URL] + // no actual configs matching this, but path types SHOULD be handled // this way, like URLs, for the same reason - : /* istanbul ignore next */ this.#types.includes(Types.Path) ? [Types.Path] - : this.#types + : /* istanbul ignore next */ this.type.includes(Types.Path) ? [Types.Path] + : this.type - const mustBe = types.filter(t => t !== Types.Array).map((t) => { + const mustBe = types.filter(t => t !== Types.Array).flatMap((t) => { const type = getType(t) - return type?.description ?? type?.typeDescription ?? type + return type + ? type.values ?? type.description ?? type.typeDescription + : describeValue(t) }) - const singleValue = mustBe.length === 1 + const singleValue = mustBe.length === 1 const oneOf = singleValue && allowMultiple ? 'one or more' : !singleValue && allowMultiple ? 'one or more of:' : !singleValue ? 'one of:' : '' - return `Must be ${oneOf} ${mustBe.map(describeValue).join(', ')}`.replace(/\s+/g, ' ') + return `Must be ${oneOf} ${mustBe.join(', ')}`.replace(/\s+/g, ' ') } describeUsage () { const usage = this.short.map(s => `-${s}`) - if (this.isBoolean && this.default !== false) { - usage.push(`--no-${this.#key}`) + if (this.isBoolean) { + if (this.default === true) { + usage.push(`--no-${this.#key}`) + } else if (this.default === false) { + usage.push(`--${this.#key}`) + } else { + usage.push(`--no-${this.#key}`, `--${this.#key}`) + } + } else { + usage.push(`--${this.#key}`) } - usage.push(`--${this.#key}`) - - let description = [] - if (!this.isBoolean) { - // null type means optional and doesn't currently affect usage output since - // all non-optional params have defaults so we render everything as optional - const valueTypes = this.#typeDefs.filter(t => t !== null && t.type !== Types.Array) - - if (valueTypes.some(t => typeof t !== 'string' && typeof t !== 'number')) { - // Generic values, use hint - description = this.#def.hint ? [].concat(this.#def.hint) : this.#typeDefs.map(t => t?.hint) + let descriptions = [] + if (this.hasNonBoolean) { + // only non booleans get hints + if (this.#def.hint) { + // if the definition itself has a hint, always use that + descriptions = [].concat(this.#def.hint) } else { - // Specific values, use specifics given - description = valueTypes + // otherwise use the types specific values, or the hint, or the value itself + descriptions = this.#typeDefs + // null type means optional and doesn't currently affect usage output since + // all non-optional params have defaults so we render everything as optional + .filter(t => t !== null && t.type !== Types.Array) + .flatMap(t => t?.hint ?? (t.type ? this.#key : t)) } } - const d = description.filter(Boolean).join('|') - const usageDesc = `${usage.join('|')} ${d ? `<${d}>` : ''}`.trim() + const desc = descriptions.filter(Boolean).join('|') + const usageDesc = `${usage.join('|')} ${desc ? `<${desc}>` : ''}`.trim() return this.#typeMultiple ? `${usageDesc} [${usageDesc} ...]` : usageDesc } #describeTypes () { - let descriptions - - const type = getType(this.#def.type) - if (type) { - descriptions = [].concat(type.typeDescription) - } else { - const types = this.#typeDefs.filter(t => t?.type !== Types.Array) - descriptions = types.flatMap(t => { - if (t?.typeDescription) { - return [].concat(t.typeDescription) - } - return { value: t } - }) - } - - descriptions = descriptions - .map(v => hasOwn(v, 'value') ? JSON.stringify(v.value) : v) + const descriptions = this.#typeDefs + .filter(t => t?.type !== Types.Array) + .flatMap(t => t?.typeDescription ?? t?.values ?? JSON.stringify(t)) // [a] => "a" // [a, b] => "a or b" @@ -269,7 +277,9 @@ class Definition { } // if it's a string, quote it. otherwise, just cast to string. -const describeValue = val => (typeof val === 'string' ? JSON.stringify(val) : String(val)) +const describeValue = val => Array.isArray(val) + ? JSON.stringify(val.map(describeValue)) + : typeof val === 'string' ? JSON.stringify(val) : String(val) const unindent = s => { // get the first \n followed by a bunch of spaces, and pluck off @@ -310,4 +320,4 @@ const wrapAll = s => { }).join('\n\n') } -module.exports = { Definition, Derived, camelCase } +module.exports = { Definition, Derived, getFlatKey } diff --git a/workspaces/config/lib/definitions/definitions.js b/workspaces/config/lib/definitions/definitions.js new file mode 100644 index 0000000000000..0b20fc4dd7df9 --- /dev/null +++ b/workspaces/config/lib/definitions/definitions.js @@ -0,0 +1,1951 @@ +const ciInfo = require('ci-info') +const { tmpdir } = require('os') +const { Types } = require('./type-defs') + +const { + EDITOR, + VISUAL, + SYSTEMROOT, + ComSpec = 'cmd', + SHELL = 'sh', + LC_ALL, + LC_CTYPE, + LANG, + LOCALAPPDATA, + NODE_ENV, + NO_COLOR, +} = process.env + +const isWindows = process.platform === 'win32' + +const Editor = EDITOR || VISUAL || (isWindows ? `${SYSTEMROOT}\\notepad.exe` : 'vi') +const Shell = isWindows ? ComSpec : SHELL +const Unicode = /UTF-?8$/i.test(LC_ALL || LC_CTYPE || LANG) +// use LOCALAPPDATA on Windows, if set https://github.com/npm/cli/pull/899 +const CacheRoot = (isWindows && LOCALAPPDATA) || '~' +const Cache = `${CacheRoot}/${isWindows ? 'npm-cache' : '.npm'}` + +const define = (key, v) => module.exports[key] = v + +// Define all config keys we know about + +define('_auth', { + default: null, + type: Types.String, + description: ` + A basic-auth string to use when authenticating against the npm registry. + This will ONLY be used to authenticate against the npm registry. For other + registries you will need to scope it like "//other-registry.tld/:_auth" + + Warning: This should generally not be set via a command-line option. It + is safer to use a registry-provided authentication bearer token stored in + the ~/.npmrc file by running \`npm login\`. + `, + flatten: true, +}) + +define('access', { + default: null, + type: ['restricted', 'public'], + defaultDescription: ` + 'public' for new packages, existing packages it will not change the current level + `, + description: ` + If do not want your scoped package to be publicly viewable (and + installable) set \`--access=restricted\`. + + Unscoped packages can not be set to \`restricted\`. + + Note: This defaults to not changing the current access level for existing + packages. Specifying a value of \`restricted\` or \`public\` during + publish will change the access for an existing package the same way that + \`npm access set status\` would. + `, + flatten: true, +}) + +define('all', { + default: false, + type: Types.Boolean, + short: 'a', + description: ` + When running \`npm outdated\` and \`npm ls\`, setting \`--all\` will show + all outdated or installed packages, rather than only those directly + depended upon by the current project. + `, + flatten: true, +}) + +define('allow-same-version', { + default: false, + type: Types.Boolean, + description: ` + Prevents throwing an error when \`npm version\` is used to set the new + version to the same value as the current version. + `, + flatten: true, +}) + +define('also', { + default: null, + type: ['dev', 'development'], + description: ` + When set to \`dev\` or \`development\`, this is an alias for + \`--include=dev\`. + `, + deprecated: 'Please use --include=dev instead.', +}) + +define('audit', { + default: true, + type: Types.Boolean, + description: ` + When "true" submit audit reports alongside the current npm command to the + default registry and all registries configured for scopes. See the + documentation for [\`npm audit\`](/commands/npm-audit) for details on what + is submitted. + `, + flatten: true, +}) + +define('audit-level', { + default: null, + type: ['info', 'low', 'moderate', 'high', 'critical', 'none'], + description: ` + The minimum level of vulnerability for \`npm audit\` to exit with + a non-zero exit code. + `, + flatten: true, +}) + +define('auth-type', { + default: 'web', + type: ['legacy', 'web'], + description: ` + What authentication strategy to use with \`login\`. + `, + flatten: true, +}) + +define('before', { + default: null, + type: Types.Date, + description: ` + If passed to \`npm install\`, will rebuild the npm tree such that only + versions that were available **on or before** the \`--before\` time get + installed. If there's no versions available for the current set of + direct dependencies, the command will error. + + If the requested version is a \`dist-tag\` and the given tag does not + pass the \`--before\` filter, the most recent version less than or equal + to that tag will be used. For example, \`foo@latest\` might install + \`foo@1.2\` even though \`latest\` is \`2.0\`. + `, + flatten: true, +}) + +define('bin-links', { + default: true, + type: Types.Boolean, + description: ` + Tells npm to create symlinks (or \`.cmd\` shims on Windows) for package + executables. + + Set to false to have it not do this. This can be used to work around the + fact that some file systems don't support symlinks, even on ostensibly + Unix systems. + `, + flatten: true, +}) + +define('browser', { + default: null, + type: Types.BooleanOrString, + defaultDescription: ` + OS X: \`"open"\`, Windows: \`"start"\`, Others: \`"xdg-open"\` + `, + description: ` + The browser that is called by npm commands to open websites. + + Set to \`false\` to suppress browser behavior and instead print urls to + terminal. + + Set to \`true\` to use default system URL opener. + `, + flatten: true, +}) + +define('ca', { + default: null, + type: [Types.String, Types.Array], + description: ` + The Certificate Authority signing certificate that is trusted for SSL + connections to the registry. Values should be in PEM format (Windows + calls it "Base-64 encoded X.509 (.CER)") with newlines replaced by the + string "\\n". For example: + + \`\`\`ini + ca="-----BEGIN CERTIFICATE-----\\nXXXX\\nXXXX\\n-----END CERTIFICATE-----" + \`\`\` + + Set to \`null\` to only allow "known" registrars, or to a specific CA + cert to trust only that specific signing authority. + + Multiple CAs can be trusted by specifying an array of certificates: + + \`\`\`ini + ca[]="..." + ca[]="..." + \`\`\` + + See also the \`strict-ssl\` config. + `, + flatten: true, +}) + +define('cache', { + default: Cache, + defaultDescription: ` + Windows: \`%LocalAppData%\\npm-cache\`, Posix: \`~/.npm\` + `, + type: Types.Path, + description: ` + The location of npm's cache directory. + `, +}) + +define('cache-max', { + default: Infinity, + type: Types.Number, + description: ` + \`--cache-max=0\` is an alias for \`--prefer-online\` + `, + deprecated: ` + This option has been deprecated in favor of \`--prefer-online\` + `, +}) + +define('cache-min', { + default: 0, + type: Types.Number, + description: ` + \`--cache-min=9999 (or bigger)\` is an alias for \`--prefer-offline\`. + `, + deprecated: ` + This option has been deprecated in favor of \`--prefer-offline\`. + `, +}) + +define('cafile', { + default: null, + type: Types.Path, + description: ` + A path to a file containing one or multiple Certificate Authority signing + certificates. Similar to the \`ca\` setting, but allows for multiple + CA's, as well as for the CA information to be stored in a file on disk. + `, +}) + +define('call', { + default: '', + type: Types.String, + short: 'c', + description: ` + Optional companion option for \`npm exec\`, \`npx\` that allows for + specifying a custom command to be run along with the installed packages. + + \`\`\`bash + npm exec --package yo --package generator-node --call "yo node" + \`\`\` + `, + flatten: true, +}) + +define('cert', { + default: null, + type: Types.String, + description: ` + A client certificate to pass when accessing the registry. Values should + be in PEM format (Windows calls it "Base-64 encoded X.509 (.CER)") with + newlines replaced by the string "\\n". For example: + + \`\`\`ini + cert="-----BEGIN CERTIFICATE-----\\nXXXX\\nXXXX\\n-----END CERTIFICATE-----" + \`\`\` + + It is _not_ the path to a certificate file, though you can set a registry-scoped + "certfile" path like "//other-registry.tld/:certfile=/path/to/cert.pem". + `, + deprecated: ` + \`key\` and \`cert\` are no longer used for most registry operations. + Use registry scoped \`keyfile\` and \`certfile\` instead. + Example: + //other-registry.tld/:keyfile=/path/to/key.pem + //other-registry.tld/:certfile=/path/to/cert.crt + `, + flatten: true, +}) + +define('ci-name', { + default: ciInfo.name ? ciInfo.name.toLowerCase().split(' ').join('-') : null, + defaultDescription: ` + The name of the current CI system, or \`null\` when not on a known CI + platform. + `, + type: [null, Types.String], + description: ` + The name of a continuous integration system. If not set explicitly, npm + will detect the current CI environment using the + [\`ci-info\`](http://npm.im/ci-info) module. + `, + flatten: true, +}) + +define('cidr', { + default: null, + type: [Types.String, Types.Array], + description: ` + This is a list of CIDR address to be used when configuring limited access + tokens with the \`npm token create\` command. + `, + flatten: true, +}) + +define('color', { + default: !NO_COLOR || NO_COLOR === '0', + defaultDescription: ` + true unless the NO_COLOR environ is set to something other than '0' + `, + type: ['always', Types.Boolean], + description: ` + If false, never shows colors. If \`"always"\` then always shows colors. + If true, then only prints color codes for tty file descriptors. + `, +}) + +define('commit-hooks', { + default: true, + type: Types.Boolean, + description: ` + Run git commit hooks when using the \`npm version\` command. + `, + flatten: true, +}) + +define('depth', { + default: null, + type: Types.Number, + defaultDescription: ` + \`Infinity\` if \`--all\` is set, otherwise \`1\` + `, + description: ` + The depth to go when recursing packages for \`npm ls\`. + + If not set, \`npm ls\` will show only the immediate dependencies of the + root project. If \`--all\` is set, then npm will show all dependencies + by default. + `, + flatten: true, +}) + +define('description', { + default: true, + type: Types.Boolean, + description: ` + Show the description in \`npm search\` + `, +}) + +define('dev', { + default: false, + type: Types.Boolean, + description: ` + Alias for \`--include=dev\`. + `, + deprecated: 'Please use --include=dev instead.', +}) + +define('diff', { + default: [], + type: [Types.Spec, Types.Array], + description: ` + Define arguments to compare in \`npm diff\`. + `, + flatten: true, +}) + +define('diff-ignore-all-space', { + default: false, + type: Types.Boolean, + description: ` + Ignore whitespace when comparing lines in \`npm diff\`. + `, + flatten: true, +}) + +define('diff-name-only', { + default: false, + type: Types.Boolean, + description: ` + Prints only filenames when using \`npm diff\`. + `, + flatten: true, +}) + +define('diff-no-prefix', { + default: false, + type: Types.Boolean, + description: ` + Do not show any source or destination prefix in \`npm diff\` output. + + Note: this causes \`npm diff\` to ignore the \`--diff-src-prefix\` and + \`--diff-dst-prefix\` configs. + `, + flatten: true, +}) + +define('diff-dst-prefix', { + default: 'b/', + type: Types.String, + description: ` + Destination prefix to be used in \`npm diff\` output. + `, + flatten: true, +}) + +define('diff-src-prefix', { + default: 'a/', + type: Types.String, + description: ` + Source prefix to be used in \`npm diff\` output. + `, + flatten: true, +}) + +define('diff-text', { + default: false, + type: Types.Boolean, + description: ` + Treat all files as text in \`npm diff\`. + `, + flatten: true, +}) + +define('diff-unified', { + default: 3, + type: Types.Number, + description: ` + The number of lines of context to print in \`npm diff\`. + `, + flatten: true, +}) + +define('dry-run', { + default: false, + type: Types.Boolean, + description: ` + Indicates that you don't want npm to make any changes and that it should + only report what it would have done. This can be passed into any of the + commands that modify your local installation, eg, \`install\`, + \`update\`, \`dedupe\`, \`uninstall\`, as well as \`pack\` and + \`publish\`. + + Note: This is NOT honored by other network related commands, eg + \`dist-tags\`, \`owner\`, etc. + `, + flatten: true, +}) + +define('editor', { + default: Editor, + defaultDescription: ` + The EDITOR or VISUAL environment variables, or '%SYSTEMROOT%\\notepad.exe' on Windows, + or 'vi' on Unix systems + `, + type: Types.String, + description: ` + The command to run for \`npm edit\` and \`npm config edit\`. + `, + flatten: true, +}) + +define('engine-strict', { + default: false, + type: Types.Boolean, + description: ` + If set to true, then npm will stubbornly refuse to install (or even + consider installing) any package that claims to not be compatible with + the current Node.js version. + + This can be overridden by setting the \`--force\` flag. + `, + flatten: true, +}) + +define('fetch-retries', { + default: 2, + type: Types.Number, + description: ` + The "retries" config for the \`retry\` module to use when fetching + packages from the registry. + + npm will retry idempotent read requests to the registry in the case + of network failures or 5xx HTTP errors. + `, + flatten: 'retry.retries', +}) + +define('fetch-retry-factor', { + default: 10, + type: Types.Number, + description: ` + The "factor" config for the \`retry\` module to use when fetching + packages. + `, + flatten: 'retry.factor', +}) + +define('fetch-retry-maxtimeout', { + default: 60000, + defaultDescription: '60000 (1 minute)', + type: Types.Number, + description: ` + The "maxTimeout" config for the \`retry\` module to use when fetching + packages. + `, + flatten: 'retry.max-timeout', +}) + +define('fetch-retry-mintimeout', { + default: 10000, + defaultDescription: '10000 (10 seconds)', + type: Types.Number, + description: ` + The "minTimeout" config for the \`retry\` module to use when fetching + packages. + `, + flatten: 'retry.min-timeout', +}) + +define('fetch-timeout', { + default: 5 * 60 * 1000, + defaultDescription: `${5 * 60 * 1000} (5 minutes)`, + type: Types.Number, + description: ` + The maximum amount of time to wait for HTTP requests to complete. + `, + flatten: 'timeout', +}) + +define('force', { + default: false, + type: Types.Boolean, + short: 'f', + description: ` + Removes various protections against unfortunate side effects, common + mistakes, unnecessary performance degradation, and malicious input. + + * Allow clobbering non-npm files in global installs. + * Allow the \`npm version\` command to work on an unclean git repository. + * Allow deleting the cache folder with \`npm cache clean\`. + * Allow installing packages that have an \`engines\` declaration + requiring a different version of npm. + * Allow installing packages that have an \`engines\` declaration + requiring a different version of \`node\`, even if \`--engine-strict\` + is enabled. + * Allow \`npm audit fix\` to install modules outside your stated + dependency range (including SemVer-major changes). + * Allow unpublishing all versions of a published package. + * Allow conflicting peerDependencies to be installed in the root project. + * Implicitly set \`--yes\` during \`npm init\`. + * Allow clobbering existing values in \`npm pkg\` + * Allow unpublishing of entire packages (not just a single version). + + If you don't have a clear idea of what you want to do, it is strongly + recommended that you do not use this option! + `, + flatten: true, +}) + +define('foreground-scripts', { + default: false, + type: Types.Boolean, + description: ` + Run all build scripts (ie, \`preinstall\`, \`install\`, and + \`postinstall\`) scripts for installed packages in the foreground + process, sharing standard input, output, and error with the main npm + process. + + Note that this will generally make installs run slower, and be much + noisier, but can be useful for debugging. + `, + flatten: true, +}) + +define('format-package-lock', { + default: true, + type: Types.Boolean, + description: ` + Format \`package-lock.json\` or \`npm-shrinkwrap.json\` as a human + readable file. + `, + flatten: true, +}) + +define('fund', { + default: true, + type: Types.Boolean, + description: ` + When "true" displays the message at the end of each \`npm install\` + acknowledging the number of dependencies looking for funding. + See [\`npm fund\`](/commands/npm-fund) for details. + `, + flatten: true, +}) + +define('git', { + default: 'git', + type: Types.String, + description: ` + The command to use for git commands. If git is installed on the + computer, but is not in the \`PATH\`, then set this to the full path to + the git binary. + `, + flatten: true, +}) + +define('git-tag-version', { + default: true, + type: Types.Boolean, + description: ` + Tag the commit when using the \`npm version\` command. Setting this to + false results in no commit being made at all. + `, + flatten: true, +}) + +define('global', { + default: false, + type: Types.Boolean, + short: 'g', + description: ` + Operates in "global" mode, so that packages are installed into the + \`prefix\` folder instead of the current working directory. See + [folders](/configuring-npm/folders) for more on the differences in + behavior. + + * packages are installed into the \`{prefix}/lib/node_modules\` folder, + instead of the current working directory. + * bin files are linked to \`{prefix}/bin\` + * man pages are linked to \`{prefix}/share/man\` + `, +}) + +define('globalconfig', { + default: null, + type: Types.Path, + defaultDescription: ` + The global --prefix setting plus 'etc/npmrc'. For example, + '/usr/local/etc/npmrc' + `, + description: ` + The config file to read for global config options. + `, + flatten: true, +}) + +define('global-style', { + default: false, + type: Types.Boolean, + description: ` + Only install direct dependencies in the top level \`node_modules\`, + but hoist on deeper dependendencies. + Sets \`--install-strategy=shallow\`. + `, + deprecated: ` + This option has been deprecated in favor of \`--install-strategy=shallow\` + `, +}) + +define('heading', { + default: 'npm', + type: Types.String, + description: ` + The string that starts all the debugging log output. + `, + flatten: true, +}) + +define('https-proxy', { + default: null, + type: Types.URL, + description: ` + A proxy to use for outgoing https requests. If the \`HTTPS_PROXY\` or + \`https_proxy\` or \`HTTP_PROXY\` or \`http_proxy\` environment variables + are set, proxy settings will be honored by the underlying + \`make-fetch-happen\` library. + `, + flatten: true, +}) + +define('if-present', { + default: false, + type: Types.Boolean, + envExport: false, + description: ` + If true, npm will not exit with an error code when \`run-script\` is + invoked for a script that isn't defined in the \`scripts\` section of + \`package.json\`. This option can be used when it's desirable to + optionally run a script when it's present and fail if the script fails. + This is useful, for example, when running scripts that may only apply for + some builds in an otherwise generic CI setup. + `, + flatten: true, +}) + +define('ignore-scripts', { + default: false, + type: Types.Boolean, + description: ` + If true, npm does not run scripts specified in package.json files. + + Note that commands explicitly intended to run a particular script, such + as \`npm start\`, \`npm stop\`, \`npm restart\`, \`npm test\`, and \`npm + run-script\` will still run their intended script if \`ignore-scripts\` is + set, but they will *not* run any pre- or post-scripts. + `, + flatten: true, +}) + +define('include', { + default: [], + type: [Types.Array, 'prod', 'dev', 'optional', 'peer'], + description: ` + Option that allows for defining which types of dependencies to install. + + This is the inverse of \`--omit=\`. + + Dependency types specified in \`--include\` will not be omitted, + regardless of the order in which omit/include are specified on the + command-line. + `, +}) + +define('include-staged', { + default: false, + type: Types.Boolean, + description: ` + Allow installing "staged" published packages, as defined by [npm RFC PR + #92](https://github.com/npm/rfcs/pull/92). + + This is experimental, and not implemented by the npm public registry. + `, + flatten: true, +}) + +define('include-workspace-root', { + default: false, + type: Types.Boolean, + short: 'iwr', + envExport: false, + description: ` + Include the workspace root when workspaces are enabled for a command. + + When false, specifying individual workspaces via the \`workspace\` config, + or all workspaces via the \`workspaces\` flag, will cause npm to operate only + on the specified workspaces, and not on the root project. + `, + flatten: true, +}) + +define('init-author-email', { + default: '', + type: Types.String, + description: ` + The value \`npm init\` should use by default for the package author's + email. + `, +}) + +define('init-author-name', { + default: '', + type: Types.String, + description: ` + The value \`npm init\` should use by default for the package author's name. + `, +}) + +define('init-author-url', { + default: '', + type: ['', Types.URL], + description: ` + The value \`npm init\` should use by default for the package author's homepage. + `, +}) + +define('init-license', { + default: 'ISC', + type: Types.String, + description: ` + The value \`npm init\` should use by default for the package license. + `, +}) + +define('init-module', { + default: '~/.npm-init.js', + type: Types.Path, + description: ` + A module that will be loaded by the \`npm init\` command. See the + documentation for the + [init-package-json](https://github.com/npm/init-package-json) module for + more information, or [npm init](/commands/npm-init). + `, +}) + +define('init-version', { + default: '1.0.0', + type: Types.Semver, + description: ` + The value that \`npm init\` should use by default for the package + version number, if not already set in package.json. + `, +}) + +// these "aliases" are historically supported in .npmrc files, unfortunately +// They should be removed in a future npm version. +define('init.author.email', { + default: '', + type: Types.String, + deprecated: ` + Use \`--init-author-email\` instead.`, + description: ` + Alias for \`--init-author-email\` + `, +}) + +define('init.author.name', { + default: '', + type: Types.String, + deprecated: ` + Use \`--init-author-name\` instead. + `, + description: ` + Alias for \`--init-author-name\` + `, +}) + +define('init.author.url', { + default: '', + type: ['', Types.URL], + deprecated: ` + Use \`--init-author-url\` instead. + `, + description: ` + Alias for \`--init-author-url\` + `, +}) + +define('init.license', { + default: 'ISC', + type: Types.String, + deprecated: ` + Use \`--init-license\` instead. + `, + description: ` + Alias for \`--init-license\` + `, +}) + +define('init.module', { + default: '~/.npm-init.js', + type: Types.Path, + deprecated: ` + Use \`--init-module\` instead. + `, + description: ` + Alias for \`--init-module\` + `, +}) + +define('init.version', { + default: '1.0.0', + type: Types.Semver, + deprecated: ` + Use \`--init-version\` instead. + `, + description: ` + Alias for \`--init-version\` + `, +}) + +define('install-links', { + default: true, + type: Types.Boolean, + description: ` + When set file: protocol dependencies will be packed and installed as + regular dependencies instead of creating a symlink. This option has + no effect on workspaces. + `, + flatten: true, +}) + +define('install-strategy', { + default: 'hoisted', + type: ['hoisted', 'nested', 'shallow'], + description: ` + Sets the strategy for installing packages in node_modules. + hoisted (default): Install non-duplicated in top-level, and duplicated as + necessary within directory structure. + nested: (formerly --legacy-bundling) install in place, no hoisting. + shallow (formerly --global-style) only install direct deps at top-level. + linked: (coming soon) install in node_modules/.store, link in place, + unhoisted. + `, + flatten: true, +}) + +define('json', { + default: false, + type: Types.Boolean, + description: ` + Whether or not to output JSON data, rather than the normal output. + + * In \`npm pkg set\` it enables parsing set values with JSON.parse() + before saving them to your \`package.json\`. + + Not supported by all npm commands. + `, + flatten: true, +}) + +define('key', { + default: null, + type: Types.String, + description: ` + A client key to pass when accessing the registry. Values should be in + PEM format with newlines replaced by the string "\\n". For example: + + \`\`\`ini + key="-----BEGIN PRIVATE KEY-----\\nXXXX\\nXXXX\\n-----END PRIVATE KEY-----" + \`\`\` + + It is _not_ the path to a key file, though you can set a registry-scoped + "keyfile" path like "//other-registry.tld/:keyfile=/path/to/key.pem". + `, + deprecated: ` + \`key\` and \`cert\` are no longer used for most registry operations. + Use registry scoped \`keyfile\` and \`certfile\` instead. + Example: + //other-registry.tld/:keyfile=/path/to/key.pem + //other-registry.tld/:certfile=/path/to/cert.crt + `, + flatten: true, +}) + +define('legacy-bundling', { + default: false, + type: Types.Boolean, + description: ` + Instead of hoisting package installs in \`node_modules\`, install packages + in the same manner that they are depended on. This may cause very deep + directory structures and duplicate package installs as there is no + de-duplicating. + Sets \`--install-strategy=nested\`. + `, + deprecated: ` + This option has been deprecated in favor of \`--install-strategy=nested\` + `, +}) + +define('legacy-peer-deps', { + default: false, + type: Types.Boolean, + description: ` + Causes npm to completely ignore \`peerDependencies\` when building a + package tree, as in npm versions 3 through 6. + + If a package cannot be installed because of overly strict + \`peerDependencies\` that collide, it provides a way to move forward + resolving the situation. + + This differs from \`--omit=peer\`, in that \`--omit=peer\` will avoid + unpacking \`peerDependencies\` on disk, but will still design a tree such + that \`peerDependencies\` _could_ be unpacked in a correct place. + + Use of \`legacy-peer-deps\` is not recommended, as it will not enforce + the \`peerDependencies\` contract that meta-dependencies may rely on. + `, + flatten: true, +}) + +define('link', { + default: false, + type: Types.Boolean, + description: ` + Used with \`npm ls\`, limiting output to only those packages that are + linked. + `, +}) + +define('local-address', { + default: null, + type: Types.IpAddress, + description: ` + The IP address of the local interface to use when making connections to + the npm registry. Must be IPv4 in versions of Node prior to 0.12. + `, + flatten: true, +}) + +define('location', { + default: 'user', + short: 'L', + type: [ + 'global', + 'user', + 'project', + ], + defaultDescription: ` + "user" unless \`--global\` is passed, which will also set this value to "global" + `, + description: ` + When passed to \`npm config\` this refers to which config file to use. + + When set to "global" mode, packages are installed into the \`prefix\` folder + instead of the current working directory. See + [folders](/configuring-npm/folders) for more on the differences in behavior. + + * packages are installed into the \`{prefix}/lib/node_modules\` folder, + instead of the current working directory. + * bin files are linked to \`{prefix}/bin\` + * man pages are linked to \`{prefix}/share/man\` + `, +}) + +define('lockfile-version', { + default: null, + type: Types.Values(1, 2, 3), + defaultDescription: ` + Version 3 if no lockfile, auto-converting v1 lockfiles to v3, otherwise + maintain current lockfile version. + `, + description: ` + Set the lockfile format version to be used in package-lock.json and + npm-shrinkwrap-json files. Possible options are: + + 1: The lockfile version used by npm versions 5 and 6. Lacks some data that + is used during the install, resulting in slower and possibly less + deterministic installs. Prevents lockfile churn when interoperating with + older npm versions. + + 2: The default lockfile version used by npm version 7 and 8. Includes both + the version 1 lockfile data and version 3 lockfile data, for maximum + determinism and interoperability, at the expense of more bytes on disk. + + 3: Only the new lockfile information introduced in npm version 7. Smaller + on disk than lockfile version 2, but not interoperable with older npm + versions. Ideal if all users are on npm version 7 and higher. + `, + flatten: true, +}) + +define('loglevel', { + default: 'notice', + type: [ + 'silent', + 'error', + 'warn', + 'notice', + 'http', + 'info', + 'verbose', + 'silly', + ], + description: ` + What level of logs to report. All logs are written to a debug log, + with the path to that file printed if the execution of a command fails. + + Any logs of a higher level than the setting are shown. The default is + "notice". + + See also the \`foreground-scripts\` config. + `, +}) + +define('logs-dir', { + default: null, + type: Types.Path, + defaultDescription: ` + A directory named \`_logs\` inside the cache + `, + description: ` + The location of npm's log directory. See [\`npm + logging\`](/using-npm/logging) for more information. + `, +}) + +define('logs-max', { + default: 10, + type: Types.Number, + description: ` + The maximum number of log files to store. + + If set to 0, no log files will be written for the current run. + `, +}) + +define('long', { + default: false, + type: Types.Boolean, + short: 'l', + description: ` + Show extended information in \`ls\`, \`search\`, and \`help-search\`. + `, +}) + +define('maxsockets', { + default: 15, + type: Types.Number, + description: ` + The maximum number of connections to use per origin (protocol/host/port + combination). + `, + flatten: 'max-sockets', +}) + +define('message', { + default: '%s', + type: Types.String, + short: 'm', + description: ` + Commit message which is used by \`npm version\` when creating version commit. + + Any "%s" in the message will be replaced with the version number. + `, + flatten: true, +}) + +define('node-options', { + default: null, + type: Types.String, + description: ` + Options to pass through to Node.js via the \`NODE_OPTIONS\` environment + variable. This does not impact how npm itself is executed but it does + impact how lifecycle scripts are called. + `, +}) + +define('noproxy', { + default: '', + defaultDescription: ` + The value of the NO_PROXY environment variable + `, + type: [Types.String, Types.CSV, Types.Array], + description: ` + Domain extensions that should bypass any proxies. + + Also accepts a comma-delimited string. + `, + flatten: 'no-proxy', +}) + +define('offline', { + default: false, + type: Types.Boolean, + description: ` + Force offline mode: no network requests will be done during install. To allow + the CLI to fill in missing cache data, see \`--prefer-offline\`. + `, + flatten: true, +}) + +define('omit', { + default: NODE_ENV === 'production' ? ['dev'] : [], + defaultDescription: ` + 'dev' if the \`NODE_ENV\` environment variable is set to 'production', + otherwise empty. + `, + type: [Types.Array, 'prod', 'dev', 'optional', 'peer'], + description: ` + Dependency types to omit from the installation tree on disk. + + Note that these dependencies _are_ still resolved and added to the + \`package-lock.json\` or \`npm-shrinkwrap.json\` file. They are just + not physically installed on disk. + + If a package type appears in both the \`--include\` and \`--omit\` + lists, then it will be included. + + If the resulting omit list includes \`'dev'\`, then the \`NODE_ENV\` + environment variable will be set to \`'production'\` for all lifecycle + scripts. + `, +}) + +define('omit-lockfile-registry-resolved', { + default: false, + type: Types.Boolean, + description: ` + This option causes npm to create lock files without a \`resolved\` key for + registry dependencies. Subsequent installs will need to resolve tarball + endpoints with the configured registry, likely resulting in a longer install + time. + `, + flatten: true, +}) + +define('only', { + default: null, + type: ['prod', 'production'], + deprecated: ` + Use \`--omit=dev\` to omit dev dependencies from the install. + `, + description: ` + When set to \`prod\` or \`production\`, this is an alias for + \`--omit=dev\`. + `, +}) + +define('optional', { + default: null, + type: Types.Boolean, + deprecated: ` + Use \`--omit=optional\` to exclude optional dependencies, or + \`--include=optional\` to include them. + + Default value does install optional deps unless otherwise omitted. + `, + description: ` + Alias for --include=optional or --omit=optional + `, +}) + +define('otp', { + default: null, + type: Types.String, + description: ` + This is a one-time password from a two-factor authenticator. It's needed + when publishing or changing package permissions with \`npm access\`. + + If not set, and a registry response fails with a challenge for a one-time + password, npm will prompt on the command line for one. + `, + flatten: true, +}) + +define('package', { + default: [], + type: [Types.Spec, Types.Array], + description: ` + The package or packages to install for [\`npm exec\`](/commands/npm-exec) + `, + flatten: true, +}) + +define('package-lock', { + default: true, + type: Types.Boolean, + description: ` + If set to false, then ignore \`package-lock.json\` files when installing. + This will also prevent _writing_ \`package-lock.json\` if \`save\` is + true. + + This configuration does not affect \`npm ci\`. + `, +}) + +define('package-lock-only', { + default: false, + type: Types.Boolean, + description: ` + If set to true, the current operation will only use the \`package-lock.json\`, + ignoring \`node_modules\`. + + For \`update\` this means only the \`package-lock.json\` will be updated, + instead of checking \`node_modules\` and downloading dependencies. + + For \`list\` this means the output will be based on the tree described by the + \`package-lock.json\`, rather than the contents of \`node_modules\`. + `, +}) + +define('pack-destination', { + default: '.', + type: Types.String, + description: ` + Directory in which \`npm pack\` will save tarballs. + `, + flatten: true, +}) + +define('parseable', { + default: false, + type: Types.Boolean, + short: 'p', + description: ` + Output parseable results from commands that write to standard output. For + \`npm search\`, this will be tab-separated table format. + `, + flatten: true, +}) + +define('prefer-offline', { + default: false, + type: Types.Boolean, + description: ` + If true, staleness checks for cached data will be bypassed, but missing + data will be requested from the server. To force full offline mode, use + \`--offline\`. + `, + flatten: true, +}) + +define('prefer-online', { + default: false, + type: Types.Boolean, + description: ` + If true, staleness checks for cached data will be forced, making the CLI + look for updates immediately even for fresh package data. + `, + flatten: true, +}) + +define('prefix', { + default: null, + type: Types.Path, + short: 'C', + defaultDescription: ` + In global mode, the folder where the node executable is installed. + Otherwise, the nearest parent folder containing either a package.json + file or a node_modules folder. + `, + description: ` + The location to install global items. If set on the command line, then + it forces non-global commands to run in the specified folder. + `, +}) + +define('preid', { + default: '', + hint: 'prerelease-id', + type: Types.String, + description: ` + The "prerelease identifier" to use as a prefix for the "prerelease" part + of a semver. Like the \`rc\` in \`1.2.0-rc.8\`. + `, + flatten: true, +}) + +define('production', { + default: null, + type: Types.Boolean, + deprecated: 'Use `--omit=dev` instead.', + description: 'Alias for `--omit=dev`', +}) + +define('progress', { + default: !ciInfo.isCI, + defaultDescription: ` + \`true\` unless running in a known CI system + `, + type: Types.Boolean, + description: ` + When set to \`true\`, npm will display a progress bar during time + intensive operations, if \`process.stderr\` is a TTY. + + Set to \`false\` to suppress the progress bar. + `, + flatten: true, +}) + +define('proxy', { + default: null, + type: [false, Types.URL], // allow proxy to be disabled explicitly + description: ` + A proxy to use for outgoing http requests. If the \`HTTP_PROXY\` or + \`http_proxy\` environment variables are set, proxy settings will be + honored by the underlying \`request\` library. + `, + flatten: true, +}) + +define('read-only', { + default: false, + type: Types.Boolean, + description: ` + This is used to mark a token as unable to publish when configuring + limited access tokens with the \`npm token create\` command. + `, + flatten: true, +}) + +define('rebuild-bundle', { + default: true, + type: Types.Boolean, + description: ` + Rebuild bundled dependencies after installation. + `, + flatten: true, +}) + +define('registry', { + default: 'https://registry.npmjs.org/', + type: Types.URL, + description: ` + The base URL of the npm registry. + `, + flatten: true, +}) + +define('replace-registry-host', { + default: 'npmjs', + type: ['npmjs', 'never', 'always', Types.String], + description: ` + Defines behavior for replacing the registry host in a lockfile with the + configured registry. + + The default behavior is to replace package dist URLs from the default + registry (https://registry.npmjs.org) to the configured registry. If set to + "never", then use the registry value. If set to "always", then replace the + registry host with the configured host every time. + + You may also specify a bare hostname (e.g., "registry.npmjs.org"). + `, + flatten: true, +}) + +define('save', { + default: true, + defaultDescription: ` + \`true\` unless when using \`npm update\` where it defaults to \`false\` + `, + usage: '--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle', + type: Types.Boolean, + short: 'S', + description: ` + Save installed packages to a \`package.json\` file as dependencies. + + When used with the \`npm rm\` command, removes the dependency from + \`package.json\`. + + Will also prevent writing to \`package-lock.json\` if set to \`false\`. + `, + flatten: true, +}) + +define('save-bundle', { + default: false, + type: Types.Boolean, + short: 'B', + description: ` + If a package would be saved at install time by the use of \`--save\`, + \`--save-dev\`, or \`--save-optional\`, then also put it in the + \`bundleDependencies\` list. + + Ignored if \`--save-peer\` is set, since peerDependencies cannot be bundled. + `, + flatten: true, +}) + +// XXX: We should really deprecate all these `--save-blah` switches +// in favor of a single `--save-type` option. The unfortunate shortcut +// we took for `--save-peer --save-optional` being `--save-type=peerOptional` +// makes this tricky, and likely a breaking change. + +define('save-dev', { + default: false, + type: Types.Boolean, + short: 'D', + description: ` + Save installed packages to a package.json file as \`devDependencies\`. + `, +}) + +define('save-exact', { + default: false, + type: Types.Boolean, + short: 'E', + description: ` + Dependencies saved to package.json will be configured with an exact + version rather than using npm's default semver range operator. + `, +}) + +define('save-optional', { + default: false, + type: Types.Boolean, + short: 'O', + description: ` + Save installed packages to a package.json file as + \`optionalDependencies\`. + `, +}) + +define('save-peer', { + default: false, + type: Types.Boolean, + description: ` + Save installed packages to a package.json file as \`peerDependencies\` + `, +}) + +define('save-prefix', { + default: '^', + type: Types.String, + description: ` + Configure how versions of packages installed to a package.json file via + \`--save\` or \`--save-dev\` get prefixed. + + For example if a package has version \`1.2.3\`, by default its version is + set to \`^1.2.3\` which allows minor upgrades for that package, but after + \`npm config set save-prefix='~'\` it would be set to \`~1.2.3\` which + only allows patch upgrades. + `, +}) + +define('save-prod', { + default: false, + type: Types.Boolean, + short: 'P', + description: ` + Save installed packages into \`dependencies\` specifically. This is + useful if a package already exists in \`devDependencies\` or + \`optionalDependencies\`, but you want to move it to be a non-optional + production dependency. + + This is the default behavior if \`--save\` is true, and neither + \`--save-dev\` or \`--save-optional\` are true. + `, +}) + +define('scope', { + default: '', + defaultDescription: ` + the scope of the current project, if any, or "" + `, + type: Types.Scope, + description: ` + Associate an operation with a scope for a scoped registry. + + Useful when logging in to or out of a private registry: + + \`\`\` + # log in, linking the scope to the custom registry + npm login --scope=@mycorp --registry=https://registry.mycorp.com + + # log out, removing the link and the auth token + npm logout --scope=@mycorp + \`\`\` + + This will cause \`@mycorp\` to be mapped to the registry for future + installation of packages specified according to the pattern + \`@mycorp/package\`. + + This will also cause \`npm init\` to create a scoped package. + + \`\`\` + # accept all defaults, and create a package named "@foo/whatever", + # instead of just named "whatever" + npm init --scope=@foo --yes + \`\`\` + `, + flatten: true, +}) + +define('script-shell', { + default: null, + type: Types.String, + defaultDescription: ` + '/bin/sh' on POSIX systems, 'cmd.exe' on Windows + `, + description: ` + The shell to use for scripts run with the \`npm exec\`, + \`npm run\` and \`npm init \` commands. + `, + flatten: true, +}) + +define('searchexclude', { + default: '', + type: Types.String, + description: ` + Space-separated options that limit the results from search. + `, + flatten: 'search.exclude', +}) + +define('searchlimit', { + default: 20, + type: Types.Number, + description: ` + Number of items to limit search results to. Will not apply at all to + legacy searches. + `, + flatten: 'search.limit', +}) + +define('searchopts', { + default: '', + type: Types.Querystring, + description: ` + Space-separated options that are always passed to search. + `, + flatten: 'search.opts', +}) + +define('searchstaleness', { + default: 15 * 60, + type: Types.Number, + description: ` + The age of the cache, in seconds, before another registry request is made + if using legacy search endpoint. + `, + flatten: 'search.staleness', +}) + +define('shell', { + default: Shell, + defaultDescription: ` + SHELL environment variable, or "bash" on Posix, or "cmd.exe" on Windows + `, + type: Types.String, + description: ` + The shell to run for the \`npm explore\` command. + `, + flatten: true, +}) + +define('shrinkwrap', { + default: true, + type: Types.Boolean, + deprecated: ` + Use the --package-lock setting instead. + `, + description: ` + Alias for --package-lock + `, + // TODO: is this ok? + flatten: 'package-lock', +}) + +define('sign-git-commit', { + default: false, + type: Types.Boolean, + description: ` + If set to true, then the \`npm version\` command will commit the new + package version using \`-S\` to add a signature. + + Note that git requires you to have set up GPG keys in your git configs + for this to work properly. + `, + flatten: true, +}) + +define('sign-git-tag', { + default: false, + type: Types.Boolean, + description: ` + If set to true, then the \`npm version\` command will tag the version + using \`-s\` to add a signature. + + Note that git requires you to have set up GPG keys in your git configs + for this to work properly. + `, + flatten: true, +}) + +define('strict-peer-deps', { + default: false, + type: Types.Boolean, + description: ` + If set to \`true\`, and \`--legacy-peer-deps\` is not set, then _any_ + conflicting \`peerDependencies\` will be treated as an install failure, + even if npm could reasonably guess the appropriate resolution based on + non-peer dependency relationships. + + By default, conflicting \`peerDependencies\` deep in the dependency graph + will be resolved using the nearest non-peer dependency specification, + even if doing so will result in some packages receiving a peer dependency + outside the range set in their package's \`peerDependencies\` object. + + When such and override is performed, a warning is printed, explaining the + conflict and the packages involved. If \`--strict-peer-deps\` is set, + then this warning is treated as a failure. + `, + flatten: true, +}) + +define('strict-ssl', { + default: true, + type: Types.Boolean, + description: ` + Whether or not to do SSL key validation when making requests to the + registry via https. + + See also the \`ca\` config. + `, + flatten: true, +}) + +define('tag', { + default: 'latest', + type: Types.String, + description: ` + If you ask npm to install a package and don't tell it a specific version, + then it will install the specified tag. + + Also the tag that is added to the package@version specified by the \`npm + tag\` command, if no explicit tag is given. + + When used by the \`npm diff\` command, this is the tag used to fetch the + tarball that will be compared with the local files by default. + `, + flatten: 'default-tag', +}) + +define('tag-version-prefix', { + default: 'v', + type: Types.String, + description: ` + If set, alters the prefix used when tagging a new version when performing + a version increment using \`npm version\`. To remove the prefix + altogether, set it to the empty string: \`""\`. + + Because other tools may rely on the convention that npm version tags look + like \`v1.0.0\`, _only use this property if it is absolutely necessary_. + In particular, use care when overriding this setting for public packages. + `, + flatten: true, +}) + +define('timing', { + default: false, + type: Types.Boolean, + description: ` + If true, writes timing information to a process specific json file in + the cache or \`logs-dir\`. The file name ends with \`-timing.json\`. + + You can quickly view it with this [json](https://npm.im/json) command + line: \`cat ~/.npm/_logs/*-timing.json | npm exec -- json -g\`. + + Timing information will also be reported in the terminal. To suppress this + while still writing the timing file, use \`--silent\`. + `, +}) + +define('tmp', { + default: tmpdir(), + defaultDescription: ` + The value returned by the Node.js \`os.tmpdir()\` method + + `, + type: Types.Path, + deprecated: ` + This setting is no longer used. npm stores temporary files in a special + location in the cache, and they are managed by + [\`cacache\`](http://npm.im/cacache). + `, + description: ` + Historically, the location where temporary files were stored. No longer + relevant. + `, +}) + +define('umask', { + default: 0, + type: Types.Umask, + description: ` + The "umask" value to use when setting the file creation mode on files and + folders. + + Folders and executables are given a mode which is \`0o777\` masked + against this value. Other files are given a mode which is \`0o666\` + masked against this value. + + Note that the underlying system will _also_ apply its own umask value to + files and folders that are created, and npm does not circumvent this, but + rather adds the \`--umask\` config to it. + + Thus, the effective default umask value on most POSIX systems is 0o22, + meaning that folders and executables are created with a mode of 0o755 and + other files are created with a mode of 0o644. + `, + flatten: true, +}) + +define('unicode', { + default: Unicode, + defaultDescription: ` + false on windows, true on mac/unix systems with a unicode locale, as + defined by the \`LC_ALL\`, \`LC_CTYPE\`, or \`LANG\` environment variables. + `, + type: Types.Boolean, + description: ` + When set to true, npm uses unicode characters in the tree output. When + false, it uses ascii characters instead of unicode glyphs. + `, + flatten: true, +}) + +define('update-notifier', { + default: true, + type: Types.Boolean, + description: ` + Set to false to suppress the update notification when using an older + version of npm than the latest. + `, +}) + +define('usage', { + default: false, + type: Types.Boolean, + short: ['?', 'H', 'h'], + description: ` + Show short usage output about the command specified. + `, +}) + +define('user-agent', { + default: 'npm/{npm-version} ' + + 'node/{node-version} ' + + '{platform} ' + + '{arch} ' + + 'workspaces/{workspaces} ' + + '{ci}', + type: Types.String, + description: ` + Sets the User-Agent request header. The following fields are replaced + with their actual counterparts: + + * \`{npm-version}\` - The npm version in use + * \`{node-version}\` - The Node.js version in use + * \`{platform}\` - The value of \`process.platform\` + * \`{arch}\` - The value of \`process.arch\` + * \`{workspaces}\` - Set to \`true\` if the \`workspaces\` or \`workspace\` + options are set. + * \`{ci}\` - The value of the \`ci-name\` config, if set, prefixed with + \`ci/\`, or an empty string if \`ci-name\` is empty. + `, + flatten: true, +}) + +define('userconfig', { + default: '~/.npmrc', + type: Types.Path, + description: ` + The location of user-level configuration settings. + + This may be overridden by the \`npm_config_userconfig\` environment + variable or the \`--userconfig\` command line option, but may _not_ + be overridden by settings in the \`globalconfig\` file. + `, +}) + +define('version', { + default: false, + type: Types.Boolean, + short: 'v', + description: ` + If true, output the npm version and exit successfully. + + Only relevant when specified explicitly on the command line. + `, +}) + +define('versions', { + default: false, + type: Types.Boolean, + description: ` + If true, output the npm version as well as node's \`process.versions\` + map and the version in the current working directory's \`package.json\` + file if one exists, and exit successfully. + + Only relevant when specified explicitly on the command line. + `, +}) + +define('viewer', { + default: isWindows ? 'browser' : 'man', + defaultDescription: ` + "man" on Posix, "browser" on Windows + `, + type: Types.String, + description: ` + The program to use to view help content. + + Set to \`"browser"\` to view html help content in the default web browser. + `, +}) + +define('which', { + default: null, + type: Types.PositiveInteger, + description: ` + If there are multiple funding sources, which 1-indexed source URL to open. + `, +}) + +define('workspace', { + default: [], + type: [Types.String, Types.Path, Types.Array], + hint: ['workspace-name', 'workspace-path'], + short: 'w', + envExport: false, + description: ` + Enable running a command in the context of the configured workspaces of the + current project while filtering by running only the workspaces defined by + this configuration option. + + Valid values for the \`workspace\` config are either: + + * Workspace names + * Path to a workspace directory + * Path to a parent workspace directory (will result in selecting all + workspaces within that folder) + + When set for the \`npm init\` command, this may be set to the folder of + a workspace which does not yet exist, to create the folder and set it + up as a brand new workspace within the project. + `, +}) + +define('workspaces', { + default: null, + type: Types.Boolean, + short: 'ws', + envExport: false, + description: ` + Set to true to run the command in the context of **all** configured + workspaces. + + Explicitly setting this to false will cause commands like \`install\` to + ignore workspaces altogether. + When not set explicitly: + + - Commands that operate on the \`node_modules\` tree (install, update, + etc.) will link workspaces into the \`node_modules\` folder. + - Commands that do other things (test, exec, publish, etc.) will operate + on the root project, _unless_ one or more workspaces are specified in + the \`workspace\` config. + `, +}) + +define('workspaces-update', { + default: true, + type: Types.Boolean, + description: ` + If set to true, the npm cli will run an update after operations that may + possibly change the workspaces installed to the \`node_modules\` folder. + `, + flatten: true, +}) + +define('yes', { + default: null, + type: Types.Boolean, + short: 'y', + description: ` + Automatically answer "yes" to any prompts that npm might print on + the command line. + `, +}) diff --git a/workspaces/config/lib/definitions/derived.js b/workspaces/config/lib/definitions/derived.js new file mode 100644 index 0000000000000..2dd45e9611158 --- /dev/null +++ b/workspaces/config/lib/definitions/derived.js @@ -0,0 +1,205 @@ +const { resolve, join } = require('path') +const { readFileSync } = require('fs') + +const maybeReadFile = file => { + try { + return readFileSync(file, 'utf8') + } catch (er) { + if (er.code !== 'ENOENT') { + throw er + } + return null + } +} + +// we export a Map because a derived key can be an array of keys that we +// normalize when we create all the relationships in index.js +module.exports = new Map() +const derive = (key, ...values) => module.exports.set(key, values) + +// derived values can read directly from config if necessary +derive('npm-bin', (_, config) => config.npmExecPath) +derive('node-bin', (_, config) => config.execPath) + +derive(['omit', 'include'], ({ omit, include, dev, production, optional, also, only }) => { + const derived = { omit: [...omit], include: [...include] } + + if (/^prod(uction)?$/.test(only) || production) { + derived.omit.push('dev') + } else if (production === false) { + derived.include.push('dev') + } + + if (/^dev/.test(also)) { + derived.include.push('dev') + } + + if (dev) { + derived.include.push('dev') + } + + if (optional === false) { + derived.omit.push('optional') + } else if (optional === true) { + derived.include.push('optional') + } + + derived.omit = [...new Set(derived.omit)].filter(type => !derived.include.includes(type)) + derived.include = [...new Set(derived.include)] + + return derived +}, ['dev', 'production', 'optional', 'also', 'only']) + +const deriveGlobal = ({ global, location }) => { + const isGlobal = global || location === 'global' + return isGlobal ? { global: true, location: 'global' } : { global, location } +} +derive(['global', 'location'], deriveGlobal) + +derive(['prefix', 'globalconfig', 'global-prefix'], ({ prefix, globalconfig }, config) => { + const defaultPrefix = prefix ?? config.defaultGlobalPrefix + // if the prefix is set on cli, env, or userconfig, then we need to + // default the globalconfig file to that location, instead of the default + // global prefix. It's weird that `npm get globalconfig --prefix=/foo` + // returns `/foo/etc/npmrc`, but better to not change it at this point. + return { + prefix: defaultPrefix, + globalPrefix: defaultPrefix, + globalconfig: globalconfig ?? resolve(defaultPrefix, 'etc/npmrc'), + } +}) + +derive('local-prefix', ({ prefix, workspaces, global, location }, { defaultLocalPrefix, cwd }) => { + if (prefix != null) { + return prefix + } + + if (defaultLocalPrefix.root && (workspaces === false || deriveGlobal({ global, location }))) { + return defaultLocalPrefix.root ?? cwd + } + + return defaultLocalPrefix.workspace ?? defaultLocalPrefix.root ?? cwd +}, ['prefix', 'workspaces', 'global', 'location']) + +derive(['cache', 'npx-cache', 'logs-dir'], ({ cache, logsDir }) => { + return { + cache: join(cache, '_cacache'), + npxCache: join(cache, '_npx'), + logsDir: logsDir || join(cache, '_logs'), + } +}) + +derive('prefer-online', ({ cacheMax, preferOnline }) => { + return cacheMax <= 0 ? true : preferOnline +}, ['cache-max']) + +derive('prefer-offline', ({ cacheMin, preferOffline }) => { + return cacheMin >= 9999 ? true : preferOffline +}, ['cache-min']) + +derive('ca', ({ cafile }) => { + const raw = cafile ? maybeReadFile(cafile) : null + if (!raw) { + return + } + const delim = '-----END CERTIFICATE-----' + return raw.replace(/\r\n/g, '\n') + .split(delim) + .filter(s => s.trim()) + .map(s => s.trimLeft() + delim) +}, ['cafile']) + +derive('color', ({ color }) => { + return !color ? false : color === 'always' ? true : !!process.stdout.isTTY +}) + +derive('log-color', ({ color }) => { + return !color ? false : color === 'always' ? true : !!process.stderr.isTTY +}, ['color']) + +derive('search.limit', ({ searchlimit }) => { + return searchlimit +}, ['searchlimit']) + +derive('search.description', ({ description }) => { + return description +}, ['description']) + +derive('search.exclude', ({ searchexclude }) => { + return searchexclude.toLowerCase() +}, ['searchexclude']) + +derive('search.opts', ({ searchopts }) => { + return searchopts +}, ['searchopts']) + +derive('progress', ({ progress }) => { + return !progress ? false : !!process.stderr.isTTY && process.env.TERM !== 'dumb' +}) + +derive('save-bundle', ({ saveBundle, savePeer }) => { + // XXX update arborist to just ignore it if resulting saveType is peer + // otherwise this won't have the expected effect: + // + // npm config set save-peer true + // npm i foo --save-bundle --save-prod <-- should bundle + return saveBundle && !savePeer +}, ['save-peer']) + +derive('install-strategy', ({ globalStyle, legacyBundling, installStrategy }) => { + return globalStyle ? 'shallow' : legacyBundling ? 'nested' : installStrategy +}, ['global-style', 'legacy-bundling']) + +derive('save-prefix', ({ savePrefix, saveExact }) => { + return saveExact ? '' : savePrefix +}, ['save-exact']) + +derive('save-type', ({ saveDev, saveOptional, savePeer, saveProd }) => { + if (savePeer && saveOptional) { + return 'peerOptional' + } + if (savePeer) { + return 'peer' + } + if (saveOptional) { + return 'optional' + } + if (saveDev) { + return 'dev' + } + if (saveProd) { + return 'prod' + } +}, ['save-dev', 'save-optional', 'save-peer', 'save-prod']) + +// projectScope is kept for compatibility with npm-registry-fetch +derive('project-scope', ({ scope }) => { + return scope +}, ['scope']) + +derive('user-agent', ({ userAgent, ciName, workspaces, workspace, npmVersion }) => { + const ws = !!(workspaces || workspace?.length) + return userAgent.replace(/\{node-version\}/gi, process.version) + .replace(/\{npm-version\}/gi, npmVersion) + .replace(/\{platform\}/gi, process.platform) + .replace(/\{arch\}/gi, process.arch) + .replace(/\{workspaces\}/gi, ws) + .replace(/\{ci\}/gi, ciName ? `ci/${ciName}` : '') + .trim() +}, ['ci-name', 'workspaces', 'workspace', 'npm-version']) + +derive('silent', ({ loglevel }) => { + return loglevel === 'silent' +}, ['loglevel']) + +derive(['workspaces-enabled'], ({ workspaces }) => { + return workspaces !== false +}, ['workspaces']) + +derive(['package-lock', 'package-lock-only'], ({ packageLock, packageLockOnly }) => { + const lock = !!(packageLock || packageLockOnly) + return { + packageLock: lock, + packageLockOnly: lock, + } +}) diff --git a/workspaces/config/lib/definitions/index.js b/workspaces/config/lib/definitions/index.js index c575b89127746..1b2321dbff50a 100644 --- a/workspaces/config/lib/definitions/index.js +++ b/workspaces/config/lib/definitions/index.js @@ -1,44 +1,9 @@ -const ciInfo = require('ci-info') -const { tmpdir } = require('os') -const { join, resolve } = require('path') -const fs = require('fs') -const Arborist = require('@npmcli/arborist') -const { Types } = require('../type-defs') const { Definition, Derived } = require('./definition') -const { version } = require('../../../../package.json') - -const { - EDITOR, - VISUAL, - SYSTEMROOT, - ComSpec = 'cmd', - SHELL = 'sh', - LC_ALL, - LC_CTYPE, - LANG, - LOCALAPPDATA, - NODE_ENV, - NO_COLOR, -} = process.env -const isWindows = process.platform === 'win32' - -const Editor = EDITOR || VISUAL || (isWindows ? `${SYSTEMROOT}\\notepad.exe` : 'vi') -const Shell = isWindows ? ComSpec : SHELL -const Unicode = /UTF-?8$/i.test(LC_ALL || LC_CTYPE || LANG) -// use LOCALAPPDATA on Windows, if set https://github.com/npm/cli/pull/899 -const CacheRoot = (isWindows && LOCALAPPDATA) || '~' -const Cache = `${CacheRoot}/${isWindows ? 'npm-cache' : '.npm'}` - -const maybeReadFile = file => { - try { - return fs.readFileSync(file, 'utf8') - } catch (er) { - if (er.code !== 'ENOENT') { - throw er - } - return null - } -} +const definitions = require('./definitions') +const derivations = require('./derived') +const values = require('./values') +const shorthands = require('./shorthands') +const typeDefs = require('./type-defs') module.exports = { // definition instances and their keys @@ -49,58 +14,28 @@ module.exports = { // in object form defaults: {}, types: {}, - // derived instances and their keys - derived: {}, - derivedKeys: [], // values values: {}, valueKeys: [], - // aliases where they get expanded into a completely different thing - // these are NOT supported in the environment or npmrc files, only - // expanded on the CLI. - // TODO: when we switch off of nopt, use an arg parser that supports - // more reasonable aliasing and short opts right in the definitions set. - shorthands: { - 'enjoy-by': ['--before'], - d: ['--loglevel', 'info'], - dd: ['--loglevel', 'verbose'], - ddd: ['--loglevel', 'silly'], - quiet: ['--loglevel', 'warn'], - q: ['--loglevel', 'warn'], - s: ['--loglevel', 'silent'], - silent: ['--loglevel', 'silent'], - verbose: ['--loglevel', 'verbose'], - desc: ['--description'], - help: ['--usage'], - local: ['--no-global'], - n: ['--no-yes'], - no: ['--no-yes'], - porcelain: ['--parseable'], - readonly: ['--read-only'], - reg: ['--registry'], - iwr: ['--include-workspace-root'], - }, - shortKeys: [], -} - -const finish = () => { - for (const definitionKey of module.exports.definitionKeys) { - const definition = module.exports.definitions[definitionKey] - for (const derivedKey of definition.derived) { - if (!module.exports.derived[derivedKey]) { - derive(derivedKey, { key: definitionKey }) - } - } - } - - for (const value of Object.values(module.exports)) { - Object.freeze(value) - } + // shorthands + shorthands: shorthands, + shortKeys: Object.keys(shorthands), + // derived instances and their keys + derived: {}, + derivedKeys: [], + ...typeDefs, } const define = (key, data) => { const def = new Definition(key, data) + for (const derivedKey of def.derived) { + if (derivations[derivedKey]) { + throw new Error(`Config item ${key} cannot flatten to an existing derived config ` + + `${derivedKey}. Instead the derived config should list ${key} in its sources.`) + } + } + module.exports.definitions[key] = def module.exports.definitionKeys.push(key) @@ -113,19 +48,15 @@ const define = (key, data) => { } } -const derive = (keys, set, sources) => { - // All definitions need to be created before creating derived values - Object.freeze(module.exports.definitions) - Object.freeze(module.exports.values) - +const derive = (keys, get, sources) => { const keysArr = [].concat(keys) const defSources = keysArr.filter((k) => module.exports.definitions[k]) const opts = { - nested: Array.isArray(keys), + nested: Array.isArray(keys) && keys.length > 1, sources, defSources, - ...(typeof set === 'object' ? set : { set }), + ...(typeof get === 'object' ? get : { get }), } for (const key of keysArr) { @@ -136,7 +67,7 @@ const derive = (keys, set, sources) => { for (const source of derived.sources) { const definition = module.exports.definitions[source] - if (!definition && !module.exports.values[source]) { + if (!definition && !module.exports.values[source] && source !== key) { throw new Error(`Derived key ${key} depends on missing definition: ${source}`) } else if (definition) { definition.addDerived(key) @@ -150,2114 +81,41 @@ const value = (key, v) => { module.exports.valueKeys.push(key) } -// Define all config keys we know about - -define('_auth', { - default: null, - type: [null, Types.String], - description: ` - A basic-auth string to use when authenticating against the npm registry. - This will ONLY be used to authenticate against the npm registry. For other - registries you will need to scope it like "//other-registry.tld/:_auth" - - Warning: This should generally not be set via a command-line option. It - is safer to use a registry-provided authentication bearer token stored in - the ~/.npmrc file by running \`npm login\`. - `, - flatten: true, -}) - -define('access', { - default: null, - defaultDescription: ` - 'public' for new packages, existing packages it will not change the current level - `, - type: [null, 'restricted', 'public'], - description: ` - If do not want your scoped package to be publicly viewable (and - installable) set \`--access=restricted\`. - - Unscoped packages can not be set to \`restricted\`. - - Note: This defaults to not changing the current access level for existing - packages. Specifying a value of \`restricted\` or \`public\` during - publish will change the access for an existing package the same way that - \`npm access set status\` would. - `, - flatten: true, -}) - -define('all', { - default: false, - type: Types.Boolean, - short: 'a', - description: ` - When running \`npm outdated\` and \`npm ls\`, setting \`--all\` will show - all outdated or installed packages, rather than only those directly - depended upon by the current project. - `, - flatten: true, -}) - -define('allow-same-version', { - default: false, - type: Types.Boolean, - description: ` - Prevents throwing an error when \`npm version\` is used to set the new - version to the same value as the current version. - `, - flatten: true, -}) - -define('also', { - default: null, - type: [null, 'dev', 'development'], - description: ` - When set to \`dev\` or \`development\`, this is an alias for - \`--include=dev\`. - `, - deprecated: 'Please use --include=dev instead.', -}) - -define('audit', { - default: true, - type: Types.Boolean, - description: ` - When "true" submit audit reports alongside the current npm command to the - default registry and all registries configured for scopes. See the - documentation for [\`npm audit\`](/commands/npm-audit) for details on what - is submitted. - `, - flatten: true, -}) - -define('audit-level', { - default: null, - type: [null, 'info', 'low', 'moderate', 'high', 'critical', 'none'], - description: ` - The minimum level of vulnerability for \`npm audit\` to exit with - a non-zero exit code. - `, - flatten: true, -}) - -define('auth-type', { - default: 'web', - type: ['legacy', 'web'], - description: ` - What authentication strategy to use with \`login\`. - `, - flatten: true, -}) - -define('before', { - default: null, - type: [null, Types.Date], - description: ` - If passed to \`npm install\`, will rebuild the npm tree such that only - versions that were available **on or before** the \`--before\` time get - installed. If there's no versions available for the current set of - direct dependencies, the command will error. - - If the requested version is a \`dist-tag\` and the given tag does not - pass the \`--before\` filter, the most recent version less than or equal - to that tag will be used. For example, \`foo@latest\` might install - \`foo@1.2\` even though \`latest\` is \`2.0\`. - `, - flatten: true, -}) - -define('bin-links', { - default: true, - type: Types.Boolean, - description: ` - Tells npm to create symlinks (or \`.cmd\` shims on Windows) for package - executables. - - Set to false to have it not do this. This can be used to work around the - fact that some file systems don't support symlinks, even on ostensibly - Unix systems. - `, - flatten: true, -}) - -define('browser', { - default: null, - defaultDescription: ` - OS X: \`"open"\`, Windows: \`"start"\`, Others: \`"xdg-open"\` - `, - type: Types.BooleanOrString, - description: ` - The browser that is called by npm commands to open websites. - - Set to \`false\` to suppress browser behavior and instead print urls to - terminal. - - Set to \`true\` to use default system URL opener. - `, - flatten: true, -}) - -define('ca', { - default: null, - type: [null, Types.String, Types.Array], - description: ` - The Certificate Authority signing certificate that is trusted for SSL - connections to the registry. Values should be in PEM format (Windows - calls it "Base-64 encoded X.509 (.CER)") with newlines replaced by the - string "\\n". For example: - - \`\`\`ini - ca="-----BEGIN CERTIFICATE-----\\nXXXX\\nXXXX\\n-----END CERTIFICATE-----" - \`\`\` - - Set to \`null\` to only allow "known" registrars, or to a specific CA - cert to trust only that specific signing authority. - - Multiple CAs can be trusted by specifying an array of certificates: - - \`\`\`ini - ca[]="..." - ca[]="..." - \`\`\` - - See also the \`strict-ssl\` config. - `, - flatten: true, -}) - -define('cache', { - default: Cache, - defaultDescription: ` - Windows: \`%LocalAppData%\\npm-cache\`, Posix: \`~/.npm\` - `, - type: Types.Path, - description: ` - The location of npm's cache directory. - `, -}) - -define('cache-max', { - default: Infinity, - type: Types.Number, - description: ` - \`--cache-max=0\` is an alias for \`--prefer-online\` - `, - deprecated: ` - This option has been deprecated in favor of \`--prefer-online\` - `, -}) - -define('cache-min', { - default: 0, - type: Types.Number, - description: ` - \`--cache-min=9999 (or bigger)\` is an alias for \`--prefer-offline\`. - `, - deprecated: ` - This option has been deprecated in favor of \`--prefer-offline\`. - `, -}) - -define('cafile', { - default: null, - type: [null, Types.Path], - description: ` - A path to a file containing one or multiple Certificate Authority signing - certificates. Similar to the \`ca\` setting, but allows for multiple - CA's, as well as for the CA information to be stored in a file on disk. - `, -}) - -define('call', { - default: '', - type: Types.String, - short: 'c', - description: ` - Optional companion option for \`npm exec\`, \`npx\` that allows for - specifying a custom command to be run along with the installed packages. - - \`\`\`bash - npm exec --package yo --package generator-node --call "yo node" - \`\`\` - `, - flatten: true, -}) - -define('cert', { - default: null, - type: [null, Types.String], - description: ` - A client certificate to pass when accessing the registry. Values should - be in PEM format (Windows calls it "Base-64 encoded X.509 (.CER)") with - newlines replaced by the string "\\n". For example: - - \`\`\`ini - cert="-----BEGIN CERTIFICATE-----\\nXXXX\\nXXXX\\n-----END CERTIFICATE-----" - \`\`\` - - It is _not_ the Types.Path to a certificate file, though you can set a registry-scoped - "certfile" Types.Path like "//other-registry.tld/:certfile=/path/to/cert.pem". - `, - deprecated: ` - \`key\` and \`cert\` are no longer used for most registry operations. - Use registry scoped \`keyfile\` and \`certfile\` instead. - Example: - //other-registry.tld/:keyfile=/path/to/key.pem - //other-registry.tld/:certfile=/path/to/cert.crt - `, - flatten: true, -}) - -define('ci-name', { - default: ciInfo.name ? ciInfo.name.toLowerCase().split(' ').join('-') : null, - defaultDescription: ` - The name of the current CI system, or \`null\` when not on a known CI - platform. - `, - type: [null, Types.String], - description: ` - The name of a continuous integration system. If not set explicitly, npm - will detect the current CI environment using the - [\`ci-info\`](http://npm.im/ci-info) module. - `, - flatten: true, -}) - -define('cidr', { - default: null, - type: [null, Types.String, Types.Array], - description: ` - This is a list of CIDR address to be used when configuring limited access - tokens with the \`npm token create\` command. - `, - flatten: true, -}) - -define('color', { - default: !NO_COLOR || NO_COLOR === '0', - defaultDescription: ` - true unless the NO_COLOR environ is set to something other than '0' - `, - type: ['always', Types.Boolean], - description: ` - If false, never shows colors. If \`"always"\` then always shows colors. - If true, then only prints color codes for tty file descriptors. - `, -}) - -define('commit-hooks', { - default: true, - type: Types.Boolean, - description: ` - Run git commit hooks when using the \`npm version\` command. - `, - flatten: true, -}) - -define('depth', { - default: null, - defaultDescription: ` - \`Infinity\` if \`--all\` is set, otherwise \`1\` - `, - type: [null, Types.Number], - description: ` - The depth to go when recursing packages for \`npm ls\`. - - If not set, \`npm ls\` will show only the immediate dependencies of the - root project. If \`--all\` is set, then npm will show all dependencies - by default. - `, - flatten: true, -}) - -define('description', { - default: true, - type: Types.Boolean, - description: ` - Show the description in \`npm search\` - `, -}) - -define('dev', { - default: false, - type: Types.Boolean, - description: ` - Alias for \`--include=dev\`. - `, - deprecated: 'Please use --include=dev instead.', -}) - -define('diff', { - default: [], - type: [Types.Spec, Types.Array], - description: ` - Define arguments to compare in \`npm diff\`. - `, - flatten: true, -}) - -define('diff-ignore-all-space', { - default: false, - type: Types.Boolean, - description: ` - Ignore whitespace when comparing lines in \`npm diff\`. - `, - flatten: true, -}) - -define('diff-name-only', { - default: false, - type: Types.Boolean, - description: ` - Prints only filenames when using \`npm diff\`. - `, - flatten: true, -}) - -define('diff-no-prefix', { - default: false, - type: Types.Boolean, - description: ` - Do not show any source or destination prefix in \`npm diff\` output. - - Note: this causes \`npm diff\` to ignore the \`--diff-src-prefix\` and - \`--diff-dst-prefix\` configs. - `, - flatten: true, -}) - -define('diff-dst-prefix', { - default: 'b/', - type: Types.String, - description: ` - Destination prefix to be used in \`npm diff\` output. - `, - flatten: true, -}) - -define('diff-src-prefix', { - default: 'a/', - type: Types.String, - description: ` - Source prefix to be used in \`npm diff\` output. - `, - flatten: true, -}) - -define('diff-text', { - default: false, - type: Types.Boolean, - description: ` - Treat all files as text in \`npm diff\`. - `, - flatten: true, -}) - -define('diff-unified', { - default: 3, - type: Types.Number, - description: ` - The number of lines of context to print in \`npm diff\`. - `, - flatten: true, -}) - -define('dry-run', { - default: false, - type: Types.Boolean, - description: ` - Indicates that you don't want npm to make any changes and that it should - only report what it would have done. This can be passed into any of the - commands that modify your local installation, eg, \`install\`, - \`update\`, \`dedupe\`, \`uninstall\`, as well as \`pack\` and - \`publish\`. - - Note: This is NOT honored by other network related commands, eg - \`dist-tags\`, \`owner\`, etc. - `, - flatten: true, -}) - -define('editor', { - default: Editor, - defaultDescription: ` - The EDITOR or VISUAL environment variables, or '%SYSTEMROOT%\\notepad.exe' on Windows, - or 'vi' on Unix systems - `, - type: Types.String, - description: ` - The command to run for \`npm edit\` and \`npm config edit\`. - `, - flatten: true, -}) - -define('engine-strict', { - default: false, - type: Types.Boolean, - description: ` - If set to true, then npm will stubbornly refuse to install (or even - consider installing) any package that claims to not be compatible with - the current Node.js version. - - This can be overridden by setting the \`--force\` flag. - `, - flatten: true, -}) - -define('fetch-retries', { - default: 2, - type: Types.Number, - description: ` - The "retries" config for the \`retry\` module to use when fetching - packages from the registry. - - npm will retry idempotent read requests to the registry in the case - of network failures or 5xx HTTP errors. - `, - flatten: 'retry.retries', -}) - -define('fetch-retry-factor', { - default: 10, - type: Types.Number, - description: ` - The "factor" config for the \`retry\` module to use when fetching - packages. - `, - flatten: 'retry.factor', -}) - -define('fetch-retry-maxtimeout', { - default: 60000, - defaultDescription: '60000 (1 minute)', - type: Types.Number, - description: ` - The "maxTimeout" config for the \`retry\` module to use when fetching - packages. - `, - flatten: 'retry.max-timeout', -}) - -define('fetch-retry-mintimeout', { - default: 10000, - defaultDescription: '10000 (10 seconds)', - type: Types.Number, - description: ` - The "minTimeout" config for the \`retry\` module to use when fetching - packages. - `, - flatten: 'retry.min-timeout', -}) - -define('fetch-timeout', { - default: 5 * 60 * 1000, - defaultDescription: `${5 * 60 * 1000} (5 minutes)`, - type: Types.Number, - description: ` - The maximum amount of time to wait for HTTP requests to complete. - `, - flatten: 'timeout', -}) - -define('force', { - default: false, - type: Types.Boolean, - short: 'f', - description: ` - Removes various protections against unfortunate side effects, common - mistakes, unnecessary performance degradation, and malicious input. - - * Allow clobbering non-npm files in global installs. - * Allow the \`npm version\` command to work on an unclean git repository. - * Allow deleting the cache folder with \`npm cache clean\`. - * Allow installing packages that have an \`engines\` declaration - requiring a different version of npm. - * Allow installing packages that have an \`engines\` declaration - requiring a different version of \`node\`, even if \`--engine-strict\` - is enabled. - * Allow \`npm audit fix\` to install modules outside your stated - dependency range (including SemVer-major changes). - * Allow unpublishing all versions of a published package. - * Allow conflicting peerDependencies to be installed in the root project. - * Implicitly set \`--yes\` during \`npm init\`. - * Allow clobbering existing values in \`npm pkg\` - * Allow unpublishing of entire packages (not just a single version). - - If you don't have a clear idea of what you want to do, it is strongly - recommended that you do not use this option! - `, - flatten: true, -}) - -define('foreground-scripts', { - default: false, - type: Types.Boolean, - description: ` - Run all build scripts (ie, \`preinstall\`, \`install\`, and - \`postinstall\`) scripts for installed packages in the foreground - process, sharing standard input, output, and error with the main npm - process. - - Note that this will generally make installs run slower, and be much - noisier, but can be useful for debugging. - `, - flatten: true, -}) - -define('format-package-lock', { - default: true, - type: Types.Boolean, - description: ` - Format \`package-lock.json\` or \`npm-shrinkwrap.json\` as a human - readable file. - `, - flatten: true, -}) - -define('fund', { - default: true, - type: Types.Boolean, - description: ` - When "true" displays the message at the end of each \`npm install\` - acknowledging the number of dependencies looking for funding. - See [\`npm fund\`](/commands/npm-fund) for details. - `, - flatten: true, -}) - -define('git', { - default: 'git', - type: Types.String, - description: ` - The command to use for git commands. If git is installed on the - computer, but is not in the \`PATH\`, then set this to the full path to - the git binary. - `, - flatten: true, -}) - -define('git-tag-version', { - default: true, - type: Types.Boolean, - description: ` - Tag the commit when using the \`npm version\` command. Setting this to - false results in no commit being made at all. - `, - flatten: true, -}) - -define('global', { - default: false, - type: Types.Boolean, - short: 'g', - description: ` - Operates in "global" mode, so that packages are installed into the - \`prefix\` folder instead of the current working directory. See - [folders](/configuring-npm/folders) for more on the differences in - behavior. - - * packages are installed into the \`{prefix}/lib/node_modules\` folder, - instead of the current working directory. - * bin files are linked to \`{prefix}/bin\` - * man pages are linked to \`{prefix}/share/man\` - `, -}) - -define('globalconfig', { - type: [null, Types.Path], - default: null, - defaultDescription: ` - The global --prefix setting plus 'etc/npmrc'. For example, - '/usr/local/etc/npmrc' - `, - description: ` - The config file to read for global config options. - `, - flatten: true, -}) - -define('global-style', { - default: false, - type: Types.Boolean, - description: ` - Only install direct dependencies in the top level \`node_modules\`, - but hoist on deeper dependendencies. - Sets \`--install-strategy=shallow\`. - `, - deprecated: ` - This option has been deprecated in favor of \`--install-strategy=shallow\` - `, -}) - -define('heading', { - default: 'npm', - type: Types.String, - description: ` - The string that starts all the debugging log output. - `, - flatten: true, -}) - -define('https-proxy', { - default: null, - type: [null, Types.URL], - description: ` - A proxy to use for outgoing https requests. If the \`HTTPS_PROXY\` or - \`https_proxy\` or \`HTTP_PROXY\` or \`http_proxy\` environment variables - are set, proxy settings will be honored by the underlying - \`make-fetch-happen\` library. - `, - flatten: true, -}) - -define('if-present', { - default: false, - type: Types.Boolean, - envExport: false, - description: ` - If true, npm will not exit with an error code when \`run-script\` is - invoked for a script that isn't defined in the \`scripts\` section of - \`package.json\`. This option can be used when it's desirable to - optionally run a script when it's present and fail if the script fails. - This is useful, for example, when running scripts that may only apply for - some builds in an otherwise generic CI setup. - `, - flatten: true, -}) - -define('ignore-scripts', { - default: false, - type: Types.Boolean, - description: ` - If true, npm does not run scripts specified in package.json files. - - Note that commands explicitly intended to run a particular script, such - as \`npm start\`, \`npm stop\`, \`npm restart\`, \`npm test\`, and \`npm - run-script\` will still run their intended script if \`ignore-scripts\` is - set, but they will *not* run any pre- or post-scripts. - `, - flatten: true, -}) - -define('include', { - default: [], - type: [Types.Array, 'prod', 'dev', 'optional', 'peer'], - description: ` - Option that allows for defining which types of dependencies to install. - - This is the inverse of \`--omit=\`. - - Dependency types specified in \`--include\` will not be omitted, - regardless of the order in which omit/include are specified on the - command-line. - `, -}) - -define('include-staged', { - default: false, - type: Types.Boolean, - description: ` - Allow installing "staged" published packages, as defined by [npm RFC PR - #92](https://github.com/npm/rfcs/pull/92). - - This is experimental, and not implemented by the npm public registry. - `, - flatten: true, -}) - -define('include-workspace-root', { - default: false, - type: Types.Boolean, - envExport: false, - description: ` - Include the workspace root when workspaces are enabled for a command. - - When false, specifying individual workspaces via the \`workspace\` config, - or all workspaces via the \`workspaces\` flag, will cause npm to operate only - on the specified workspaces, and not on the root project. - `, - flatten: true, -}) - -define('init-author-email', { - default: '', - type: Types.String, - description: ` - The value \`npm init\` should use by default for the package author's - email. - `, -}) - -define('init-author-name', { - default: '', - type: Types.String, - description: ` - The value \`npm init\` should use by default for the package author's name. - `, -}) - -define('init-author-url', { - default: '', - type: ['', Types.URL], - description: ` - The value \`npm init\` should use by default for the package author's homepage. - `, -}) - -define('init-license', { - default: 'ISC', - type: Types.String, - description: ` - The value \`npm init\` should use by default for the package license. - `, -}) - -define('init-module', { - default: '~/.npm-init.js', - type: Types.Path, - description: ` - A module that will be loaded by the \`npm init\` command. See the - documentation for the - [init-package-json](https://github.com/npm/init-package-json) module for - more information, or [npm init](/commands/npm-init). - `, -}) - -define('init-version', { - default: '1.0.0', - type: Types.Semver, - description: ` - The value that \`npm init\` should use by default for the package - version number, if not already set in package.json. - `, -}) - -// these "aliases" are historically supported in .npmrc files, unfortunately -// They should be removed in a future npm version. -define('init.author.email', { - default: '', - type: Types.String, - deprecated: ` - Use \`--init-author-email\` instead.`, - description: ` - Alias for \`--init-author-email\` - `, -}) - -define('init.author.name', { - default: '', - type: Types.String, - deprecated: ` - Use \`--init-author-name\` instead. - `, - description: ` - Alias for \`--init-author-name\` - `, -}) - -define('init.author.url', { - default: '', - type: ['', Types.URL], - deprecated: ` - Use \`--init-author-url\` instead. - `, - description: ` - Alias for \`--init-author-url\` - `, -}) - -define('init.license', { - default: 'ISC', - type: Types.String, - deprecated: ` - Use \`--init-license\` instead. - `, - description: ` - Alias for \`--init-license\` - `, -}) - -define('init.module', { - default: '~/.npm-init.js', - type: Types.Path, - deprecated: ` - Use \`--init-module\` instead. - `, - description: ` - Alias for \`--init-module\` - `, -}) - -define('init.version', { - default: '1.0.0', - type: Types.Semver, - deprecated: ` - Use \`--init-version\` instead. - `, - description: ` - Alias for \`--init-version\` - `, -}) - -define('install-links', { - default: true, - type: Types.Boolean, - description: ` - When set file: protocol dependencies will be packed and installed as - regular dependencies instead of creating a symlink. This option has - no effect on workspaces. - `, - flatten: true, -}) - -define('install-strategy', { - default: 'hoisted', - type: ['hoisted', 'nested', 'shallow'], - description: ` - Sets the strategy for installing packages in node_modules. - hoisted (default): Install non-duplicated in top-level, and duplicated as - necessary within directory structure. - nested: (formerly --legacy-bundling) install in place, no hoisting. - shallow (formerly --global-style) only install direct deps at top-level. - linked: (coming soon) install in node_modules/.store, link in place, - unhoisted. - `, - flatten: true, -}) - -define('json', { - default: false, - type: Types.Boolean, - description: ` - Whether or not to output JSON data, rather than the normal output. - - * In \`npm pkg set\` it enables parsing set values with JSON.parse() - before saving them to your \`package.json\`. - - Not supported by all npm commands. - `, - flatten: true, -}) - -define('key', { - default: null, - type: [null, Types.String], - description: ` - A client key to pass when accessing the registry. Values should be in - PEM format with newlines replaced by the string "\\n". For example: - - \`\`\`ini - key="-----BEGIN PRIVATE KEY-----\\nXXXX\\nXXXX\\n-----END PRIVATE KEY-----" - \`\`\` - - It is _not_ the path to a key file, though you can set a registry-scoped - "keyfile" path like "//other-registry.tld/:keyfile=/path/to/key.pem". - `, - deprecated: ` - \`key\` and \`cert\` are no longer used for most registry operations. - Use registry scoped \`keyfile\` and \`certfile\` instead. - Example: - //other-registry.tld/:keyfile=/path/to/key.pem - //other-registry.tld/:certfile=/path/to/cert.crt - `, - flatten: true, -}) - -define('legacy-bundling', { - default: false, - type: Types.Boolean, - description: ` - Instead of hoisting package installs in \`node_modules\`, install packages - in the same manner that they are depended on. This may cause very deep - directory structures and duplicate package installs as there is no - de-duplicating. - Sets \`--install-strategy=nested\`. - `, - deprecated: ` - This option has been deprecated in favor of \`--install-strategy=nested\` - `, -}) - -define('legacy-peer-deps', { - default: false, - type: Types.Boolean, - description: ` - Causes npm to completely ignore \`peerDependencies\` when building a - package tree, as in npm versions 3 through 6. - - If a package cannot be installed because of overly strict - \`peerDependencies\` that collide, it provides a way to move forward - resolving the situation. - - This differs from \`--omit=peer\`, in that \`--omit=peer\` will avoid - unpacking \`peerDependencies\` on disk, but will still design a tree such - that \`peerDependencies\` _could_ be unpacked in a correct place. - - Use of \`legacy-peer-deps\` is not recommended, as it will not enforce - the \`peerDependencies\` contract that meta-dependencies may rely on. - `, - flatten: true, -}) - -define('link', { - default: false, - type: Types.Boolean, - description: ` - Used with \`npm ls\`, limiting output to only those packages that are - linked. - `, -}) - -define('local-address', { - default: null, - type: Types.IpAddress, - description: ` - The IP address of the local interface to use when making connections to - the npm registry. Must be IPv4 in versions of Node prior to 0.12. - `, - flatten: true, -}) - -define('location', { - default: 'user', - short: 'L', - type: [ - 'global', - 'user', - 'project', - ], - defaultDescription: ` - "user" unless \`--global\` is passed, which will also set this value to "global" - `, - description: ` - When passed to \`npm config\` this refers to which config file to use. - - When set to "global" mode, packages are installed into the \`prefix\` folder - instead of the current working directory. See - [folders](/configuring-npm/folders) for more on the differences in behavior. - - * packages are installed into the \`{prefix}/lib/node_modules\` folder, - instead of the current working directory. - * bin files are linked to \`{prefix}/bin\` - * man pages are linked to \`{prefix}/share/man\` - `, -}) - -define('lockfile-version', { - default: null, - type: [null, 1, 2, 3], - defaultDescription: ` - Version 3 if no lockfile, auto-converting v1 lockfiles to v3, otherwise - maintain current lockfile version. - `, - description: ` - Set the lockfile format version to be used in package-lock.json and - npm-shrinkwrap-json files. Possible options are: - - 1: The lockfile version used by npm versions 5 and 6. Lacks some data that - is used during the install, resulting in slower and possibly less - deterministic installs. Prevents lockfile churn when interoperating with - older npm versions. - - 2: The default lockfile version used by npm version 7 and 8. Includes both - the version 1 lockfile data and version 3 lockfile data, for maximum - determinism and interoperability, at the expense of more bytes on disk. - - 3: Only the new lockfile information introduced in npm version 7. Smaller - on disk than lockfile version 2, but not interoperable with older npm - versions. Ideal if all users are on npm version 7 and higher. - `, - flatten: true, -}) - -define('loglevel', { - default: 'notice', - type: [ - 'silent', - 'error', - 'warn', - 'notice', - 'http', - 'info', - 'verbose', - 'silly', - ], - description: ` - What level of logs to report. All logs are written to a debug log, - with the path to that file printed if the execution of a command fails. - - Any logs of a higher level than the setting are shown. The default is - "notice". - - See also the \`foreground-scripts\` config. - `, -}) - -define('logs-dir', { - default: null, - type: [null, Types.Path], - defaultDescription: ` - A directory named \`_logs\` inside the cache - `, - description: ` - The location of npm's log directory. See [\`npm - logging\`](/using-npm/logging) for more information. - `, -}) - -define('logs-max', { - default: 10, - type: Types.Number, - description: ` - The maximum number of log files to store. - - If set to 0, no log files will be written for the current run. - `, -}) - -define('long', { - default: false, - type: Types.Boolean, - short: 'l', - description: ` - Show extended information in \`ls\`, \`search\`, and \`help-search\`. - `, -}) - -define('maxsockets', { - default: 15, - type: Types.Number, - description: ` - The maximum number of connections to use per origin (protocol/host/port - combination). - `, - flatten: 'max-sockets', -}) - -define('message', { - default: '%s', - type: Types.String, - short: 'm', - description: ` - Commit message which is used by \`npm version\` when creating version commit. - - Any "%s" in the message will be replaced with the version number. - `, - flatten: true, -}) - -define('node-options', { - default: null, - type: [null, Types.String], - description: ` - Options to pass through to Node.js via the \`NODE_OPTIONS\` environment - variable. This does not impact how npm itself is executed but it does - impact how lifecycle scripts are called. - `, -}) - -define('noproxy', { - default: '', - defaultDescription: ` - The value of the NO_PROXY environment variable - `, - type: [Types.String, Types.CSV, Types.Array], - description: ` - Domain extensions that should bypass any proxies. - - Also accepts a comma-delimited string. - `, - flatten: 'no-proxy', -}) - -define('offline', { - default: false, - type: Types.Boolean, - description: ` - Force offline mode: no network requests will be done during install. To allow - the CLI to fill in missing cache data, see \`--prefer-offline\`. - `, - flatten: true, -}) - -define('omit', { - default: NODE_ENV === 'production' ? ['dev'] : [], - defaultDescription: ` - 'dev' if the \`NODE_ENV\` environment variable is set to 'production', - otherwise empty. - `, - type: [Types.Array, 'prod', 'dev', 'optional', 'peer'], - description: ` - Dependency types to omit from the installation tree on disk. - - Note that these dependencies _are_ still resolved and added to the - \`package-lock.json\` or \`npm-shrinkwrap.json\` file. They are just - not physically installed on disk. - - If a package type appears in both the \`--include\` and \`--omit\` - lists, then it will be included. - - If the resulting omit list includes \`'dev'\`, then the \`NODE_ENV\` - environment variable will be set to \`'production'\` for all lifecycle - scripts. - `, -}) - -define('omit-lockfile-registry-resolved', { - default: false, - type: Types.Boolean, - description: ` - This option causes npm to create lock files without a \`resolved\` key for - registry dependencies. Subsequent installs will need to resolve tarball - endpoints with the configured registry, likely resulting in a longer install - time. - `, - flatten: true, -}) - -define('only', { - default: null, - type: [null, 'prod', 'production'], - deprecated: ` - Use \`--omit=dev\` to omit dev dependencies from the install. - `, - description: ` - When set to \`prod\` or \`production\`, this is an alias for - \`--omit=dev\`. - `, -}) - -define('optional', { - default: null, - type: [null, Types.Boolean], - deprecated: ` - Use \`--omit=optional\` to exclude optional dependencies, or - \`--include=optional\` to include them. - - Default value does install optional deps unless otherwise omitted. - `, - description: ` - Alias for --include=optional or --omit=optional - `, -}) - -define('otp', { - default: null, - type: [null, Types.String], - description: ` - This is a one-time password from a two-factor authenticator. It's needed - when publishing or changing package permissions with \`npm access\`. - - If not set, and a registry response fails with a challenge for a one-time - password, npm will prompt on the command line for one. - `, - flatten: true, -}) - -define('package', { - default: [], - type: [Types.Spec, Types.Array], - description: ` - The package or packages to install for [\`npm exec\`](/commands/npm-exec) - `, - flatten: true, -}) - -define('package-lock', { - default: true, - type: Types.Boolean, - description: ` - If set to false, then ignore \`package-lock.json\` files when installing. - This will also prevent _writing_ \`package-lock.json\` if \`save\` is - true. - - This configuration does not affect \`npm ci\`. - `, -}) - -define('package-lock-only', { - default: false, - type: Types.Boolean, - description: ` - If set to true, the current operation will only use the \`package-lock.json\`, - ignoring \`node_modules\`. - - For \`update\` this means only the \`package-lock.json\` will be updated, - instead of checking \`node_modules\` and downloading dependencies. - - For \`list\` this means the output will be based on the tree described by the - \`package-lock.json\`, rather than the contents of \`node_modules\`. - `, -}) - -define('pack-destination', { - default: '.', - type: Types.String, - description: ` - Directory in which \`npm pack\` will save tarballs. - `, - flatten: true, -}) - -define('parseable', { - default: false, - type: Types.Boolean, - short: 'p', - description: ` - Output parseable results from commands that write to standard output. For - \`npm search\`, this will be tab-separated table format. - `, - flatten: true, -}) - -define('prefer-offline', { - default: false, - type: Types.Boolean, - description: ` - If true, staleness checks for cached data will be bypassed, but missing - data will be requested from the server. To force full offline mode, use - \`--offline\`. - `, - flatten: true, -}) - -define('prefer-online', { - default: false, - type: Types.Boolean, - description: ` - If true, staleness checks for cached data will be forced, making the CLI - look for updates immediately even for fresh package data. - `, - flatten: true, -}) - -define('prefix', { - type: [null, Types.Path], - short: 'C', - default: null, - defaultDescription: ` - In global mode, the folder where the node executable is installed. - Otherwise, the nearest parent folder containing either a package.json - file or a node_modules folder. - `, - description: ` - The location to install global items. If set on the command line, then - it forces non-global commands to run in the specified folder. - `, -}) - -define('preid', { - default: '', - hint: 'prerelease-id', - type: Types.String, - description: ` - The "prerelease identifier" to use as a prefix for the "prerelease" part - of a semver. Like the \`rc\` in \`1.2.0-rc.8\`. - `, - flatten: true, -}) - -define('production', { - default: null, - type: [null, Types.Boolean], - deprecated: 'Use `--omit=dev` instead.', - description: 'Alias for `--omit=dev`', -}) - -define('progress', { - default: !ciInfo.isCI, - defaultDescription: ` - \`true\` unless running in a known CI system - `, - type: Types.Boolean, - description: ` - When set to \`true\`, npm will display a progress bar during time - intensive operations, if \`process.stderr\` is a TTY. - - Set to \`false\` to suppress the progress bar. - `, - flatten: true, -}) - -define('proxy', { - default: null, - type: [null, false, Types.URL], // allow proxy to be disabled explicitly - description: ` - A proxy to use for outgoing http requests. If the \`HTTP_PROXY\` or - \`http_proxy\` environment variables are set, proxy settings will be - honored by the underlying \`request\` library. - `, - flatten: true, -}) - -define('read-only', { - default: false, - type: Types.Boolean, - description: ` - This is used to mark a token as unable to publish when configuring - limited access tokens with the \`npm token create\` command. - `, - flatten: true, -}) - -define('rebuild-bundle', { - default: true, - type: Types.Boolean, - description: ` - Rebuild bundled dependencies after installation. - `, - flatten: true, -}) - -define('registry', { - default: 'https://registry.npmjs.org/', - type: Types.URL, - description: ` - The base URL of the npm registry. - `, - flatten: true, -}) - -define('replace-registry-host', { - default: 'npmjs', - type: ['npmjs', 'never', 'always', Types.String], - description: ` - Defines behavior for replacing the registry host in a lockfile with the - configured registry. - - The default behavior is to replace package dist URLs from the default - registry (https://registry.npmjs.org) to the configured registry. If set to - "never", then use the registry value. If set to "always", then replace the - registry host with the configured host every time. - - You may also specify a bare hostname (e.g., "registry.npmjs.org"). - `, - flatten: true, -}) - -define('save', { - default: true, - defaultDescription: ` - \`true\` unless when using \`npm update\` where it defaults to \`false\` - `, - usage: '--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle', - type: Types.Boolean, - short: 'S', - description: ` - Save installed packages to a \`package.json\` file as dependencies. - - When used with the \`npm rm\` command, removes the dependency from - \`package.json\`. - - Will also prevent writing to \`package-lock.json\` if set to \`false\`. - `, - flatten: true, -}) - -define('save-bundle', { - default: false, - type: Types.Boolean, - short: 'B', - description: ` - If a package would be saved at install time by the use of \`--save\`, - \`--save-dev\`, or \`--save-optional\`, then also put it in the - \`bundleDependencies\` list. - - Ignored if \`--save-peer\` is set, since peerDependencies cannot be bundled. - `, - flatten: true, -}) - -// XXX: We should really deprecate all these `--save-blah` switches -// in favor of a single `--save-type` option. The unfortunate shortcut -// we took for `--save-peer --save-optional` being `--save-type=peerOptional` -// makes this tricky, and likely a breaking change. - -define('save-dev', { - default: false, - type: Types.Boolean, - short: 'D', - description: ` - Save installed packages to a package.json file as \`devDependencies\`. - `, -}) - -define('save-exact', { - default: false, - type: Types.Boolean, - short: 'E', - description: ` - Dependencies saved to package.json will be configured with an exact - version rather than using npm's default semver range operator. - `, -}) - -define('save-optional', { - default: false, - type: Types.Boolean, - short: 'O', - description: ` - Save installed packages to a package.json file as - \`optionalDependencies\`. - `, -}) - -define('save-peer', { - default: false, - type: Types.Boolean, - description: ` - Save installed packages to a package.json file as \`peerDependencies\` - `, -}) - -define('save-prefix', { - default: '^', - type: Types.String, - description: ` - Configure how versions of packages installed to a package.json file via - \`--save\` or \`--save-dev\` get prefixed. - - For example if a package has version \`1.2.3\`, by default its version is - set to \`^1.2.3\` which allows minor upgrades for that package, but after - \`npm config set save-prefix='~'\` it would be set to \`~1.2.3\` which - only allows patch upgrades. - `, -}) - -define('save-prod', { - default: false, - type: Types.Boolean, - short: 'P', - description: ` - Save installed packages into \`dependencies\` specifically. This is - useful if a package already exists in \`devDependencies\` or - \`optionalDependencies\`, but you want to move it to be a non-optional - production dependency. - - This is the default behavior if \`--save\` is true, and neither - \`--save-dev\` or \`--save-optional\` are true. - `, -}) - -define('scope', { - default: '', - defaultDescription: ` - the scope of the current project, if any, or "" - `, - type: Types.Scope, - description: ` - Associate an operation with a scope for a scoped registry. - - Useful when logging in to or out of a private registry: - - \`\`\` - # log in, linking the scope to the custom registry - npm login --scope=@mycorp --registry=https://registry.mycorp.com - - # log out, removing the link and the auth token - npm logout --scope=@mycorp - \`\`\` - - This will cause \`@mycorp\` to be mapped to the registry for future - installation of packages specified according to the pattern - \`@mycorp/package\`. - - This will also cause \`npm init\` to create a scoped package. - - \`\`\` - # accept all defaults, and create a package named "@foo/whatever", - # instead of just named "whatever" - npm init --scope=@foo --yes - \`\`\` - `, - flatten: true, -}) - -define('script-shell', { - default: null, - defaultDescription: ` - '/bin/sh' on POSIX systems, 'cmd.exe' on Windows - `, - type: [null, Types.String], - description: ` - The shell to use for scripts run with the \`npm exec\`, - \`npm run\` and \`npm init \` commands. - `, - flatten: true, -}) - -define('searchexclude', { - default: '', - type: Types.String, - description: ` - Space-separated options that limit the results from search. - `, - flatten: 'search.exclude', -}) - -define('searchlimit', { - default: 20, - type: Types.Number, - description: ` - Number of items to limit search results to. Will not apply at all to - legacy searches. - `, - flatten: 'search.limit', -}) - -define('searchopts', { - default: '', - type: Types.Querystring, - description: ` - Space-separated options that are always passed to search. - `, - flatten: 'search.opts', -}) - -define('searchstaleness', { - default: 15 * 60, - type: Types.Number, - description: ` - The age of the cache, in seconds, before another registry request is made - if using legacy search endpoint. - `, - flatten: 'search.staleness', -}) - -define('shell', { - default: Shell, - defaultDescription: ` - SHELL environment variable, or "bash" on Posix, or "cmd.exe" on Windows - `, - type: Types.String, - description: ` - The shell to run for the \`npm explore\` command. - `, - flatten: true, -}) - -define('shrinkwrap', { - default: true, - type: Types.Boolean, - deprecated: ` - Use the --package-lock setting instead. - `, - description: ` - Alias for --package-lock - `, - // TODO: is this ok? - flatten: 'package-lock', -}) - -define('sign-git-commit', { - default: false, - type: Types.Boolean, - description: ` - If set to true, then the \`npm version\` command will commit the new - package version using \`-S\` to add a signature. - - Note that git requires you to have set up GPG keys in your git configs - for this to work properly. - `, - flatten: true, -}) - -define('sign-git-tag', { - default: false, - type: Types.Boolean, - description: ` - If set to true, then the \`npm version\` command will tag the version - using \`-s\` to add a signature. - - Note that git requires you to have set up GPG keys in your git configs - for this to work properly. - `, - flatten: true, -}) - -define('strict-peer-deps', { - default: false, - type: Types.Boolean, - description: ` - If set to \`true\`, and \`--legacy-peer-deps\` is not set, then _any_ - conflicting \`peerDependencies\` will be treated as an install failure, - even if npm could reasonably guess the appropriate resolution based on - non-peer dependency relationships. - - By default, conflicting \`peerDependencies\` deep in the dependency graph - will be resolved using the nearest non-peer dependency specification, - even if doing so will result in some packages receiving a peer dependency - outside the range set in their package's \`peerDependencies\` object. - - When such and override is performed, a warning is printed, explaining the - conflict and the packages involved. If \`--strict-peer-deps\` is set, - then this warning is treated as a failure. - `, - flatten: true, -}) - -define('strict-ssl', { - default: true, - type: Types.Boolean, - description: ` - Whether or not to do SSL key validation when making requests to the - registry via https. - - See also the \`ca\` config. - `, - flatten: true, -}) - -define('tag', { - default: 'latest', - type: Types.String, - description: ` - If you ask npm to install a package and don't tell it a specific version, - then it will install the specified tag. - - Also the tag that is added to the package@version specified by the \`npm - tag\` command, if no explicit tag is given. - - When used by the \`npm diff\` command, this is the tag used to fetch the - tarball that will be compared with the local files by default. - `, - flatten: 'default-tag', -}) - -define('tag-version-prefix', { - default: 'v', - type: Types.String, - description: ` - If set, alters the prefix used when tagging a new version when performing - a version increment using \`npm version\`. To remove the prefix - altogether, set it to the empty string: \`""\`. - - Because other tools may rely on the convention that npm version tags look - like \`v1.0.0\`, _only use this property if it is absolutely necessary_. - In particular, use care when overriding this setting for public packages. - `, - flatten: true, -}) - -define('timing', { - default: false, - type: Types.Boolean, - description: ` - If true, writes timing information to a process specific json file in - the cache or \`logs-dir\`. The file name ends with \`-timing.json\`. - - You can quickly view it with this [json](https://npm.im/json) command - line: \`cat ~/.npm/_logs/*-timing.json | npm exec -- json -g\`. - - Timing information will also be reported in the terminal. To suppress this - while still writing the timing file, use \`--silent\`. - `, -}) - -define('tmp', { - default: tmpdir(), - defaultDescription: ` - The value returned by the Node.js \`os.tmpdir()\` method - - `, - type: Types.Path, - deprecated: ` - This setting is no longer used. npm stores temporary files in a special - location in the cache, and they are managed by - [\`cacache\`](http://npm.im/cacache). - `, - description: ` - Historically, the location where temporary files were stored. No longer - relevant. - `, -}) - -define('umask', { - default: 0, - type: Types.Umask, - description: ` - The "umask" value to use when setting the file creation mode on files and - folders. - - Folders and executables are given a mode which is \`0o777\` masked - against this value. Other files are given a mode which is \`0o666\` - masked against this value. - - Note that the underlying system will _also_ apply its own umask value to - files and folders that are created, and npm does not circumvent this, but - rather adds the \`--umask\` config to it. - - Thus, the effective default umask value on most POSIX systems is 0o22, - meaning that folders and executables are created with a mode of 0o755 and - other files are created with a mode of 0o644. - `, - flatten: true, -}) - -define('unicode', { - default: Unicode, - defaultDescription: ` - false on windows, true on mac/unix systems with a unicode locale, as - defined by the \`LC_ALL\`, \`LC_CTYPE\`, or \`LANG\` environment variables. - `, - type: Types.Boolean, - description: ` - When set to true, npm uses unicode characters in the tree output. When - false, it uses ascii characters instead of unicode glyphs. - `, - flatten: true, -}) - -define('update-notifier', { - default: true, - type: Types.Boolean, - description: ` - Set to false to suppress the update notification when using an older - version of npm than the latest. - `, -}) - -define('usage', { - default: false, - type: Types.Boolean, - short: ['?', 'H', 'h'], - description: ` - Show short usage output about the command specified. - `, -}) - -define('user-agent', { - default: 'npm/{npm-version} ' + - 'node/{node-version} ' + - '{platform} ' + - '{arch} ' + - 'workspaces/{workspaces} ' + - '{ci}', - type: Types.String, - description: ` - Sets the User-Agent request header. The following fields are replaced - with their actual counterparts: - - * \`{npm-version}\` - The npm version in use - * \`{node-version}\` - The Node.js version in use - * \`{platform}\` - The value of \`process.platform\` - * \`{arch}\` - The value of \`process.arch\` - * \`{workspaces}\` - Set to \`true\` if the \`workspaces\` or \`workspace\` - options are set. - * \`{ci}\` - The value of the \`ci-name\` config, if set, prefixed with - \`ci/\`, or an empty string if \`ci-name\` is empty. - `, - flatten: true, -}) - -define('userconfig', { - default: '~/.npmrc', - type: Types.Path, - description: ` - The location of user-level configuration settings. - - This may be overridden by the \`npm_config_userconfig\` environment - variable or the \`--userconfig\` command line option, but may _not_ - be overridden by settings in the \`globalconfig\` file. - `, -}) - -define('version', { - default: false, - type: Types.Boolean, - short: 'v', - description: ` - If true, output the npm version and exit successfully. - - Only relevant when specified explicitly on the command line. - `, -}) - -define('versions', { - default: false, - type: Types.Boolean, - description: ` - If true, output the npm version as well as node's \`process.versions\` - map and the version in the current working directory's \`package.json\` - file if one exists, and exit successfully. - - Only relevant when specified explicitly on the command line. - `, -}) - -define('viewer', { - default: isWindows ? 'browser' : 'man', - defaultDescription: ` - "man" on Posix, "browser" on Windows - `, - type: Types.String, - description: ` - The program to use to view help content. - - Set to \`"browser"\` to view html help content in the default web browser. - `, -}) - -define('which', { - default: null, - type: [null, Types.PositiveInteger], - description: ` - If there are multiple funding sources, which 1-indexed source URL to open. - `, -}) - -define('workspace', { - default: [], - type: [Types.String, Types.Path, Types.Array], - hint: 'workspace-name|workspace-path', - short: 'w', - envExport: false, - description: ` - Enable running a command in the context of the configured workspaces of the - current project while filtering by running only the workspaces defined by - this configuration option. - - Valid values for the \`workspace\` config are either: - - * Workspace names - * Path to a workspace directory - * Path to a parent workspace directory (will result in selecting all - workspaces within that folder) - - When set for the \`npm init\` command, this may be set to the folder of - a workspace which does not yet exist, to create the folder and set it - up as a brand new workspace within the project. - `, -}) - -define('workspaces', { - default: null, - type: [null, Types.Boolean], - short: 'ws', - envExport: false, - description: ` - Set to true to run the command in the context of **all** configured - workspaces. - - Explicitly setting this to false will cause commands like \`install\` to - ignore workspaces altogether. - When not set explicitly: - - - Commands that operate on the \`node_modules\` tree (install, update, - etc.) will link workspaces into the \`node_modules\` folder. - - Commands that do other things (test, exec, publish, etc.) will operate - on the root project, _unless_ one or more workspaces are specified in - the \`workspace\` config. - `, -}) - -define('workspaces-update', { - default: true, - type: Types.Boolean, - description: ` - If set to true, the npm cli will run an update after operations that may - possibly change the workspaces installed to the \`node_modules\` folder. - `, - flatten: true, -}) - -define('yes', { - default: null, - type: [null, Types.Boolean], - short: 'y', - description: ` - Automatically answer "yes" to any prompts that npm might print on - the command line. - `, -}) - -// These are default values that cannot be overridden at any other level so they -// are defined here instead of definitions since we do not want to document them -// but they should still be applied to flat options, and derived configs can depend -// on them unlike other derived configs. -value('npm-command', '') -value('npm-version', version) - -// the Arborist constructor is used almost everywhere we call pacote, it's -// easiest to attach it to flatOptions so it goes everywhere without having -// to touch every call -value('Arborist', Arborist) - -// XXX should this be sha512? is it even relevant? -value('hash-algorithm', 'sha1') - -// derived values can read directly from config if necessary -derive('npm-bin', (_, config) => config.npmExecPath) -derive('node-bin', (_, config) => config.execPath) - -derive(['omit', 'include'], ({ omit, include, dev, production, optional, also, only }) => { - const derived = { omit: [...omit], include: [...include] } - - if (/^prod(uction)?$/.test(only) || production) { - derived.omit.push('dev') - } else if (production === false) { - derived.include.push('dev') - } - - if (/^dev/.test(also)) { - derived.include.push('dev') - } - - if (dev) { - derived.include.push('dev') +const main = () => { + for (const [key, def] of Object.entries(definitions)) { + define(key, def) } - if (optional === false) { - derived.omit.push('optional') - } else if (optional === true) { - derived.include.push('optional') - } - - derived.omit = [...new Set(derived.omit)].filter(type => !derived.include.includes(type)) - derived.include = [...new Set(derived.include)] - - return derived -}, ['dev', 'production', 'optional', 'also', 'only']) - -derive(['global', 'location'], ({ global, location }) => { - const isGlobal = global || location === 'global' - return isGlobal ? { global: true, location: 'global' } : { global, location } -}) - -derive(['prefix', 'globalconfig'], ({ prefix, globalconfig }, config) => { - const defaultPrefix = prefix ?? config.globalPrefix - // if the prefix is set on cli, env, or userconfig, then we need to - // default the globalconfig file to that location, instead of the default - // global prefix. It's weird that `npm get globalconfig --prefix=/foo` - // returns `/foo/etc/npmrc`, but better to not change it at this point. - return { - prefix: defaultPrefix, - globalconfig: globalconfig ?? resolve(defaultPrefix, 'etc/npmrc'), - } -}) + // Everything needs to be added before derived values are created + Object.freeze(module.exports.definitions) + Object.freeze(module.exports.definitionKeys) + Object.freeze(module.exports.defaults) + Object.freeze(module.exports.types) + Object.freeze(module.exports.shorthands) + Object.freeze(module.exports.shortKeys) -derive(['cache', 'npx-cache', 'logs-dir'], ({ cache, logsDir }) => { - return { - cache: join(cache, '_cacache'), - npxCache: join(cache, '_npx'), - logsDir: logsDir || join(cache, '_logs'), + for (const [key, v] of Object.entries(values)) { + value(key, v) } -}) - -derive('prefer-online', ({ cacheMax, preferOnline }) => { - return cacheMax <= 0 ? true : preferOnline -}, ['cache-max']) -derive('prefer-offline', ({ cacheMin, preferOffline }) => { - return cacheMin >= 9999 ? true : preferOffline -}, ['cache-min']) + Object.freeze(module.exports.values) + Object.freeze(module.exports.valueKeys) -derive('ca', ({ cafile }) => { - const raw = cafile ? maybeReadFile(cafile) : null - if (!raw) { - return + for (const [key, derived] of derivations.entries()) { + derive(key, ...derived) } - const delim = '-----END CERTIFICATE-----' - return raw.replace(/\r\n/g, '\n') - .split(delim) - .filter(s => s.trim()) - .map(s => s.trimLeft() + delim) -}, ['cafile']) - -derive('color', ({ color }) => { - return !color ? false : color === 'always' ? true : !!process.stdout.isTTY -}) - -derive('log-color', ({ color }) => { - return !color ? false : color === 'always' ? true : !!process.stderr.isTTY -}, ['color']) - -derive('search.limit', ({ searchlimit }) => { - return searchlimit -}, ['searchlimit']) - -derive('search.description', ({ description }) => { - return description -}, ['description']) - -derive('search.exclude', ({ searchexclude }) => { - return searchexclude.toLowerCase() -}, ['searchexclude']) -derive('search.opts', ({ searchopts }) => { - return searchopts -}, ['searchopts']) - -derive('progress', ({ progress }) => { - return !progress ? false : !!process.stderr.isTTY && process.env.TERM !== 'dumb' -}) - -derive('save-bundle', ({ saveBundle, savePeer }) => { - // XXX update arborist to just ignore it if resulting saveType is peer - // otherwise this won't have the expected effect: - // - // npm config set save-peer true - // npm i foo --save-bundle --save-prod <-- should bundle - return saveBundle && !savePeer -}, ['save-peer']) - -derive('install-strategy', ({ globalStyle, legacyBundling, installStrategy }) => { - return globalStyle ? 'shallow' : legacyBundling ? 'nested' : installStrategy -}, ['global-style', 'legacy-bundling']) - -derive('save-prefix', ({ savePrefix, saveExact }) => { - return saveExact ? '' : savePrefix -}, ['save-exact']) - -derive('save-type', ({ saveDev, saveOptional, savePeer, saveProd }) => { - if (savePeer && saveOptional) { - return 'peerOptional' - } - if (savePeer) { - return 'peer' - } - if (saveOptional) { - return 'optional' - } - if (saveDev) { - return 'dev' - } - if (saveProd) { - return 'prod' + for (const key of module.exports.definitionKeys) { + const definition = module.exports.definitions[key] + for (const derivedKey of definition.derived) { + if (!module.exports.derived[derivedKey]) { + derive(derivedKey, { key }) + } + } } -}, ['save-dev', 'save-optional', 'save-peer', 'save-prod']) - -// projectScope is kept for compatibility with npm-registry-fetch -derive('project-scope', ({ scope }) => { - return scope -}, ['scope']) - -derive('user-agent', ({ userAgent, ciName, workspaces, workspace, npmVersion }) => { - const ws = !!(workspaces || workspace?.length) - return userAgent.replace(/\{node-version\}/gi, process.version) - .replace(/\{npm-version\}/gi, npmVersion) - .replace(/\{platform\}/gi, process.platform) - .replace(/\{arch\}/gi, process.arch) - .replace(/\{workspaces\}/gi, ws) - .replace(/\{ci\}/gi, ciName ? `ci/${ciName}` : '') - .trim() -}, ['ci-name', 'workspaces', 'workspace', 'npm-version']) -derive('silent', ({ loglevel }) => { - return loglevel === 'silent' -}, ['loglevel']) - -derive(['workspaces-enabled'], ({ workspaces }) => { - return workspaces !== false -}, ['workspaces']) - -derive(['package-lock', 'package-lock-only'], ({ packageLock, packageLockOnly }) => { - const lock = !!(packageLock || packageLockOnly) - return { - packageLock: lock, - packageLockOnly: lock, - } -}) + Object.freeze(module.exports.derived) + Object.freeze(module.exports.derivedKeys) +} -finish() +main() diff --git a/workspaces/config/lib/definitions/shorthands.js b/workspaces/config/lib/definitions/shorthands.js new file mode 100644 index 0000000000000..3439028a4e79a --- /dev/null +++ b/workspaces/config/lib/definitions/shorthands.js @@ -0,0 +1,26 @@ + +// aliases where they get expanded into a completely different thing +// these are NOT supported in the environment or npmrc files, only +// expanded on the CLI. +// TODO: when we switch off of nopt, use an arg parser that supports +// more reasonable aliasing and short opts right in the definitions set. + +module.exports = { + 'enjoy-by': ['--before'], + d: ['--loglevel', 'info'], + dd: ['--loglevel', 'verbose'], + ddd: ['--loglevel', 'silly'], + quiet: ['--loglevel', 'warn'], + q: ['--loglevel', 'warn'], + s: ['--loglevel', 'silent'], + silent: ['--loglevel', 'silent'], + verbose: ['--loglevel', 'verbose'], + desc: ['--description'], + help: ['--usage'], + local: ['--no-global'], + n: ['--no-yes'], + no: ['--no-yes'], + porcelain: ['--parseable'], + readonly: ['--read-only'], + reg: ['--registry'], +} diff --git a/workspaces/config/lib/definitions/type-defs.js b/workspaces/config/lib/definitions/type-defs.js new file mode 100644 index 0000000000000..57864fb177173 --- /dev/null +++ b/workspaces/config/lib/definitions/type-defs.js @@ -0,0 +1,278 @@ +const { typeDefs: noptDefs } = require('nopt').lib +const semver = require('semver') +const querystring = require('querystring') +const { resolve } = require('path') +const { networkInterfaces } = require('os') + +const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1) +const noptValidators = Object.entries(noptDefs).reduce((acc, [k, v]) => { + acc[capitalize(k)] = v.validate + return acc +}, {}) + +const typeSymbols = {} +const typeDefs = {} +const byType = new Map() +const Types = {} + +const getType = (k) => byType.get(k) +const updateType = (k, data) => Object.assign(getType(k), data) + +const type = (key, { type: t, ...typeOpts } = {}) => { + const typeKey = t ?? key + const typeSymbol = typeSymbols[typeKey] ?? Symbol(typeKey) + typeSymbols[typeKey] = typeSymbol + + const typeDef = { + type: typeSymbol, + validate: noptValidators[key], + ...typeOpts, + } + typeDefs[key] = typeDef + byType.set(typeSymbol, typeDefs[key]) + Types[key] = typeSymbol + return typeSymbol +} + +const valuesType = (values) => { + const allNumeric = values.every(v => typeof v === 'number') + return { + type: JSON.stringify(values), + values, + validate: (data, k, val) => { + if (allNumeric) { + const numVal = noptValidators.Number(data, k, val) + if (values.includes(numVal)) { + data[k] = numVal + return + } + return false + } + + if (values.includes(val)) { + data[k] = val + } + + return false + }, + } +} + +const validateUmask = (data, k, val) => { + if (typeof val === 'string') { + if (/^0o?[0-7]+$/.test(val)) { + data[k] = parseInt(val.replace(/^0o?/, ''), 8) + } else if (/^[1-9][0-9]*$/.test(val)) { + data[k] = parseInt(val, 10) + } + return false + } + + if (typeof val !== 'number') { + return false + } + + val = Math.floor(val) + + if (val < 0 || val > 511) { + return false + } + + data[k] = val +} + +// Override nopt path validaton to use the HOME and PLATFORM +// values set by @npmcli/config constructor +const validatePath = (data, k, val) => { + if (typeof val !== 'string') { + return false + } + + const isWin = typeDefs.Path.PLATFORM === 'win32' + const homePattern = isWin ? /^~(\/|\\)/ : /^~\// + const home = typeDefs.Path.HOME + + if (home && val.match(homePattern)) { + data[k] = resolve(home, val.slice(2)) + } else { + data[k] = resolve(val) + } +} + +function validatePositiveNumber (data, k, val) { + if (isNaN(val)) { + return false + } + val = +val + if (val < 1) { + return false + } + data[k] = val +} + +const validateQs = (data, k, val) => { + data[k] = querystring.parse(val.replace(/\s+/g, '&')) +} + +const validateCsv = (data, k, val) => { + data[k] = val.split(',') +} + +const validateScope = (data, k, val) => { + data[k] = !/^@/.test(val) ? `@${val}` : val +} + +const getLocalIps = () => { + try { + return Object.values(networkInterfaces()).flatMap(i => i.map(ii => ii.address)) + } catch { + return [] + } +} + +const isStrictBool = (val) => { + if (typeof val === 'boolean' || val === 'true' || val === 'false') { + return true + } +} + +// `description` gets shown during a runtime validation warning +// `typeDescription` gets displayed in the docs for the `Type:` +type('String', { + typeDescription: 'String', + description: 'a string', +}) + +type('Boolean', { + typeDescription: 'Boolean', + description: 'a boolean value (true or false)', + isBoolean: true, +}) + +type('Number', { + typeDescription: 'Number', + description: 'a numeric value', + hint: 'number', +}) + +type('PositiveInteger', { + validate: validatePositiveNumber, + typeDescription: 'Positive integer', + description: 'an integer greater than or equal to 1', + hint: '1|2|3|n', +}) + +type('Date', { + typeDescription: 'Date', + description: 'a valid Date string', +}) + +type('URL', { + typeDescription: 'URL', + description: 'a full url with "http://"', +}) + +type('Querystring', { + validate: validateQs, + typeDescription: 'Querystring', + description: 'a space-delimited querystring', + hint: 'key=val key2=val2', +}) + +type('CSV', { + validate: validateCsv, + typeDescription: 'Comma-delimited string', + description: 'a comma-delimited string', +}) + +type('Scope', { + validate: validateScope, + typeDescription: 'Scope', + description: 'an npm scope with or without the leading @', + hint: '@scope', +}) + +type('Spec', { + // a spec has always been just a string but we could validate futher + // here and give better validation messages for certain formats + validate: noptValidators.String, + typeDescription: 'Spec', + description: 'an npm package spec', + hint: 'package-spec', +}) + +type('Path', { + validate: validatePath, + typeDescription: 'Path', + description: 'a valid filesystem path', +}) + +type('Semver', { + validate: (data, k, val) => { + const valid = semver.valid(val) + if (!valid) { + return false + } + data[k] = valid + }, + typeDescription: 'SemVer string', + description: 'a full valid SemVer string', +}) + +type('Umask', { + validate: validateUmask, + typeDescription: 'Octal numeric string in range 0000..0777 (0..511)', + description: 'an octal number in range 0o000..0o777 (0..511)', +}) + +type('BooleanOrString', { + validate: (data, k, val) => { + if (isStrictBool(val)) { + return noptValidators.Boolean(data, k, val) + } + return noptValidators.String(data, k, val) + }, + typeDescription: ['Boolean', 'String'], + description: ['a boolean value (true or false)', 'a string'], + isBoolean: true, +}) + +type('BooleanOrNumber', { + validate: (data, k, val) => { + if (isStrictBool(val)) { + return noptValidators.Boolean(data, k, val) + } + return noptValidators.Number(data, k, val) + }, + typeDescription: ['Boolean', 'Number'], + description: ['a boolean value (true or false)', 'a numeric value'], + isBoolean: true, +}) + +type('IpAddress', { + ...valuesType(getLocalIps()), + type: 'IpAddress', + typeDescription: 'IP Address', +}) + +// the array type has no validation or description its presence in a +// definition signals that more than one value of the other types are allowed +type('Array') + +// This type is used to set a config value as explicitly not allowed since nopt +// will always parse unknown keys to the raw value passed in. It's not used +// explicitly in the defintions, but can be used at certain config locations to +// not allow certain keys there. +type('NotAllowed', { validate: () => false }) + +Types.Values = (...values) => { + const t = valuesType(values) + return type(t.type, t) +} + +module.exports = { + typeDefs, + Types, + getType, + updateType, +} diff --git a/workspaces/config/lib/definitions/values.js b/workspaces/config/lib/definitions/values.js new file mode 100644 index 0000000000000..305f7916dee8f --- /dev/null +++ b/workspaces/config/lib/definitions/values.js @@ -0,0 +1,20 @@ +const Arborist = require('@npmcli/arborist') +const { version } = require('../../../../package.json') + +// These are default values that cannot be overridden at any other level so they +// are defined here instead of definitions since we do not want to document them +// but they should still be applied to flat options, and derived configs can depend +// on them unlike other derived configs. + +const value = (key, v) => module.exports[key] = v + +value('npm-command', '') +value('npm-version', version) + +// the Arborist constructor is used almost everywhere we call pacote, it's +// easiest to attach it to flatOptions so it goes everywhere without having +// to touch every call +value('Arborist', Arborist) + +// XXX should this be sha512? is it even relevant? +value('hash-algorithm', 'sha1') diff --git a/workspaces/config/lib/index.js b/workspaces/config/lib/index.js index a456ef7cfdcf2..d68e4bbf5c502 100644 --- a/workspaces/config/lib/index.js +++ b/workspaces/config/lib/index.js @@ -7,16 +7,26 @@ const log = require('proc-log') const { resolve, dirname, join } = require('path') const { homedir } = require('os') const fs = require('fs/promises') -const TypeDefs = require('./type-defs.js') const SetGlobal = require('./set-globals.js') const { ErrInvalidAuth } = require('./errors') -const Credentials = require('./credentials.js') const ConfigTypes = require('./config-locations') const Definitions = require('./definitions') -const { isNerfed } = require('./nerf-dart.js') +const nerfDart = require('./nerf-dart.js') const replaceInfo = require('./replace-info') const Locations = ConfigTypes.Locations +// These are the configs that we can nerf-dart. Not all of them currently even +// *have* config definitions so we have to explicitly validate them here +const NerfDarts = [ + '_auth', + '_authToken', + 'username', + '_password', + 'email', + 'certfile', + 'keyfile', +] + const fileExists = (...p) => fs.stat(resolve(...p)) .then((st) => st.isFile()) .catch(() => false) @@ -29,7 +39,7 @@ class Config { static Locations = Locations static EnvKeys = [...SetGlobal.EnvKeys.values()] static ProcessKeys = [...SetGlobal.ProcessKeys.values()] - static NerfDarts = Credentials.nerfDarts + static NerfDarts = NerfDarts static replaceInfo = replaceInfo static configKeys = Definitions.definitionKeys static definitions = Definitions.definitions @@ -41,9 +51,9 @@ class Config { #configData = null // required options in constructor - #npmRoot = null + #builtinRoot = null #argv = null - #cwdRoot = null + #localPrefixRoot = null // options just to override in tests, mostly #process = null @@ -60,8 +70,8 @@ class Config { #title = null // set when we load configs - #globalPrefix = null - #localPrefix = null + #defaultGlobalPrefix = null + #defaultLocalPrefix = null #localPackage = null #loaded = false #home = null @@ -70,12 +80,11 @@ class Config { #setEnv = null #setNpmEnv = null #setProc = null - #credentials = null constructor ({ - npmRoot, + builtinRoot, argv, - cwdRoot, + localPrefixRoot, // pass in process to set everything, but also allow // overriding specific parts of process that are used @@ -86,7 +95,7 @@ class Config { execPath = _process.execPath, cwd = _process.cwd(), }) { - this.#npmRoot = npmRoot + this.#builtinRoot = builtinRoot this.#process = _process this.#env = env @@ -95,24 +104,32 @@ class Config { this.#execPath = execPath this.#npmExecPath = require.main?.filename this.#cwd = cwd - this.#cwdRoot = cwdRoot + this.#localPrefixRoot = localPrefixRoot this.#home = this.#env.HOME || homedir() - TypeDefs.typeDefs.Path.HOME = this.#home - TypeDefs.typeDefs.Path.PLATFORM = this.#platform + // this allows the Path type definition to do replacements + // using the detected home and platform + Definitions.updateType(Definitions.Types.Path, { + HOME: this.#home, + PLATFORM: this.#platform, + }) this.#configData = new ConfigTypes({ envReplace: (k) => SetGlobal.replaceEnv(this.#env, k), config: this, }) - this.#credentials = new Credentials(this) - this.#setProc = (...args) => SetGlobal.setProcess(this.#process, ...args) this.#setEnv = (...args) => SetGlobal.setEnv(this.#env, ...args) this.#setNpmEnv = (...args) => SetGlobal.npm.setEnv(this.#env, ...args) - this.#init() + // load env first because it has no dependencies + this.#loadEnv() + + // then load the cli options since those have no dependencies but can have env + // vars replaced in them. this gives us the command name and any remaining args + // which will be passed to npm.exec(). + this.#loadCli() } // ============================================= @@ -124,12 +141,24 @@ class Config { return this.#loaded } + get cwd () { + return this.#cwd + } + get globalPrefix () { - return this.#globalPrefix + return this.flat.globalPrefix + } + + get defaultGlobalPrefix () { + return this.#defaultGlobalPrefix } get localPrefix () { - return this.#localPrefix + return this.flat.localPrefix + } + + get defaultLocalPrefix () { + return this.#defaultLocalPrefix } get localPackage () { @@ -162,7 +191,11 @@ class Config { } get credentials () { - return this.#credentials + return { + setByURI: (uri) => this.#setByURI(uri), + getByURI: (uri) => this.#getByURI(uri), + clearByURI: (uri) => this.#clearByURI(uri), + } } get command () { @@ -255,13 +288,25 @@ class Config { // Config type loaders // // ============================================= - #init () { - // load env first because it has no dependencies - this.#loadEnv() + #loadEnv () { + const data = Object.entries(this.#env).reduce((acc, [key, val]) => { + if (!SetGlobal.npm.testKey(key) || !val) { + return acc + } + const configKey = key.slice(SetGlobal.npm.envPrefix.length) + if (nerfDart.isNerfed(configKey)) { + // don't normalize nerf-darted keys + acc[configKey] = val + } else { + // don't replace _ at the start of the key + acc[configKey.replace(/(?!^)_/g, '-').toLowerCase()] = val + } + return acc + }, {}) + this.#loadObject(Locations.env, data) + } - // then load the cli options since those have no dependencies but can have env - // vars replaced in them. this gives us the command name and any remaining args - // which will be passed to npm.exec(). + #loadCli () { // NOTE: this is where command specific config could go since we now have a parsed // command name, the remaining args, and config values from the CLI and can rewrite // them or parse the remaining config files with this information. @@ -290,7 +335,7 @@ class Config { // to keep those from being leaked. this.#title = `npm ${replaceInfo(remain).join(' ')}`.trim() this.#setProc('title', this.#title) - log.verbose('title', this.#title) + log.verbose('config', 'title', this.#title) // The cooked argv is also logged separately for debugging purposes. It is // cleaned as a best effort by replacing known secrets like basic auth @@ -298,14 +343,14 @@ class Config { // safer the config should create a sanitized version of the argv as it // has the full context of what each option contains. this.#clean = replaceInfo(cooked) - log.verbose('argv', this.#clean.map(JSON.stringify).join(' ')) + log.verbose('config', 'argv', this.#clean.map(JSON.stringify).join(' ')) // Options are prefixed by a hyphen-minus (-, \u2d). // Other dash-type chars look similar but are invalid. const nonDashArgs = remain.filter(a => /^[\u2010-\u2015\u2212\uFE58\uFE63\uFF0D]/.test(a)) if (nonDashArgs.length) { log.error( - 'arg', + 'config', 'Argument starts with non-ascii dash, this is probably invalid:', nonDashArgs.join(', ') ) @@ -319,7 +364,7 @@ class Config { async load () { this.#assertLoaded(false) - return this.#time('load', () => this.#load()) + await this.#time('load', () => this.#load()) } async #load () { @@ -333,49 +378,22 @@ class Config { await this.#time(`load:${Locations.user}`, () => this.#loadUser()) // last but not least, global config file await this.#time(`load:${Locations.global}`, () => this.#loadGlobal()) - - // set proper globalPrefix now that everything is loaded - // needs to be set before setEnvs to use it - // this is a derived value that has been defaulted to the previous value - // of global prefix determined in loadDefaults - this.#globalPrefix = this.#configData.data.prefix + // now that everything is loaded we can set our env vars this.#time('load:setEnvs', () => this.#setEnvs()) this.#loaded = true } async #loadDefaults () { - await this.#time('whichnode', async () => { - const node = await which(this.#argv[0]).catch(() => {}) - if (node?.toUpperCase() !== this.#execPath.toUpperCase()) { - log.verbose('node symlink', node) - this.#execPath = node - SetGlobal.setProcess(this.#process, 'execPath', node) - } - }) - - if (this.#env.PREFIX) { - this.#globalPrefix = this.#env.PREFIX - } else if (this.#platform === 'win32') { - // c:\node\node.exe --> prefix=c:\node\ - this.#globalPrefix = dirname(this.#execPath) - } else { - // /usr/local/bin/node --> prefix=/usr/local - this.#globalPrefix = dirname(dirname(this.#execPath)) - // destdir only is respected on Unix - if (this.#env.DESTDIR) { - this.#globalPrefix = join(this.#env.DESTDIR, this.#globalPrefix) - } - } - + await this.#findGlobalPrefix() this.#loadObject(Locations.default, Definitions.defaults) } async #loadBuiltin () { - await this.#loadFile(resolve(this.#npmRoot, 'npmrc'), Locations.builtin) + await this.#loadFile(resolve(this.#builtinRoot, 'npmrc'), Locations.builtin) } async #loadGlobal () { - await this.#loadFile(this.#configData.data.globalconfig, Locations.global) + await this.#loadFile(this.#get('globalconfig'), Locations.global) } async #loadUser () { @@ -386,24 +404,27 @@ class Config { // the localPrefix can be set by the CLI config, but otherwise is // found by walking up the folder tree. either way, we load it before // we return to make sure localPrefix is set - await this.#time('load:localprefix', async () => { - await this.#loadLocalPrefix() - - // if we have not detected a local package json yet, try now that we - // have a local prefix - if (this.#localPackage == null) { - this.#localPackage = await fileExists(this.#localPrefix, 'package.json') + await this.#time('load:localprefix', () => this.#findLocalPrefix()) + const localPrefix = this.#get('local-prefix') + + if (this.#defaultLocalPrefix.workspace === localPrefix) { + // set the workspace in the default layer, which allows it to be overridden easily + this.#set('workspace', [localPrefix], Locations.default) + if (await fileExists(localPrefix, '.npmrc')) { + log.warn('config', `ignoring workspace config at ${localPrefix}/.npmrc`) } - }) + } + + this.#localPackage = await fileExists(localPrefix, 'package.json') const config = this.#configData.get(Locations.project) - if (this.global) { + if (this.#get('global')) { config.ignore('global mode enabled') return } - const projectFile = resolve(this.#localPrefix, '.npmrc') + const projectFile = resolve(localPrefix, '.npmrc') // if we're in the ~ directory, and there happens to be a node_modules // folder (which is not TOO uncommon, it turns out), then we can end // up loading the "project" config where the "userconfig" will be, @@ -413,25 +434,8 @@ class Config { config.ignore('same as "user" config') return } - await this.#loadFile(projectFile, Locations.project) - } - #loadEnv () { - const data = Object.entries(this.#env).reduce((acc, [key, val]) => { - if (!SetGlobal.npm.testKey(key) || !val) { - return acc - } - const configKey = key.slice(SetGlobal.npm.envPrefix.length) - if (isNerfed(configKey)) { - // don't normalize nerf-darted keys - acc[configKey] = val - } else { - // don't replace _ at the start of the key - acc[configKey.replace(/(?!^)_/g, '-').toLowerCase()] = val - } - return acc - }, {}) - this.#loadObject(Locations.env, data) + await this.#loadFile(projectFile, Locations.project) } async #loadFile (file, where) { @@ -446,67 +450,72 @@ class Config { return this.#configData.get(where).load(data, error, file) } - async #loadLocalPrefix () { - const cliPrefix = this.#get('prefix', Locations.cli) - if (cliPrefix) { - this.#localPrefix = cliPrefix - return + async #findGlobalPrefix () { + await this.#time('whichnode', async () => { + const node = await which(this.#argv[0]).catch(() => {}) + if (node?.toUpperCase() !== this.#execPath.toUpperCase()) { + log.verbose('config', 'node symlink', node) + this.#execPath = node + SetGlobal.setProcess(this.#process, 'execPath', node) + } + }) + + let prefix + if (this.#env.PREFIX) { + prefix = this.#env.PREFIX + } else if (this.#platform === 'win32') { + // c:\node\node.exe --> prefix=c:\node\ + prefix = dirname(this.#execPath) + } else { + // /usr/local/bin/node --> prefix=/usr/local + prefix = dirname(dirname(this.#execPath)) + // destdir only is respected on Unix + if (this.#env.DESTDIR) { + prefix = join(this.#env.DESTDIR, prefix) + } } - const cliWorkspaces = this.#get('workspaces', Locations.cli) + this.#defaultGlobalPrefix = prefix + } + + async #findLocalPrefix () { + const prefix = { root: null, workspace: null } for (const p of walkUp(this.#cwd)) { - if (p === this.#cwdRoot) { + // This property tells us to stop looking if we reach this directory no + // matter what else has been found + if (p === this.#localPrefixRoot) { break } const hasPackageJson = await fileExists(p, 'package.json') - if (!this.#localPrefix && (hasPackageJson || await dirExists(p, 'node_modules'))) { - this.#localPrefix = p - this.#localPackage = hasPackageJson - - // if workspaces are disabled, or we're in global mode, return now - if (cliWorkspaces === false || this.global) { - return - } - - // otherwise, continue the loop + if (!prefix.root && (hasPackageJson || await dirExists(p, 'node_modules'))) { + prefix.root = p continue } - if (this.#localPrefix && hasPackageJson) { - // if we already set localPrefix but this dir has a package.json + if (prefix.root && hasPackageJson) { + // if we already set localPrefix but this dir up the chain has a package.json // then we need to see if `p` is a workspace root by reading its package.json // however, if reading it fails then we should just move on const pkg = await rpj(resolve(p, 'package.json')).catch(() => false) + if (!pkg) { continue } - const workspaces = await mapWorkspaces({ cwd: p, pkg }) - for (const w of workspaces.values()) { - if (w === this.#localPrefix) { - // see if there's a .npmrc file in the workspace, if so log a warning - if (await fileExists(this.#localPrefix, '.npmrc')) { - log.warn(`ignoring workspace config at ${this.#localPrefix}/.npmrc`) - } - - // set the workspace in the default layer, which allows it to be overridden easily - this.#set('workspace', [this.#localPrefix], Locations.default) - this.#localPrefix = p - this.#localPackage = hasPackageJson - log.info(`found workspace root at ${this.#localPrefix}`) + for (const w of await mapWorkspaces({ cwd: p, pkg }).values()) { + if (w === prefix.root) { + prefix.workspace = p // we found a root, so we return now - return + break } } } } - if (!this.#localPrefix) { - this.#localPrefix = this.#cwd - } + this.#defaultLocalPrefix = prefix } // Set environment variables for any non-default configs, @@ -521,9 +530,6 @@ class Config { // This ensures that all npm config values that are not the defaults are // shared appropriately with child processes, without false positives. #setEnvs () { - this.#setNpmEnv('global-prefix', this.#globalPrefix) - this.#setNpmEnv('local-prefix', this.#localPrefix) - // if the key is deprecated, skip it always. // if the key is the default value, // if the environ is NOT the default value, @@ -556,13 +562,14 @@ class Config { } } - // these depend on derived values so they use the flat data - this.#setNpmEnv('user-agent', this.flat.userAgent) - this.#setEnv('COLOR', this.flat.color ? '1' : '0') - this.#setEnv('NODE_ENV', this.flat.omit.includes('dev') ? 'production' : null) + this.#setNpmEnv('global-prefix', this.#get('global-prefix')) + this.#setNpmEnv('local-prefix', this.#get('local-prefix')) + this.#setNpmEnv('user-agent', this.#get('user-agent')) + this.#setEnv('COLOR', this.#get('color') ? '1' : '0') + this.#setEnv('NODE_ENV', this.#get('omit').includes('dev') ? 'production' : null) // XXX make this the bin/npm-cli.js file explicitly instead // otherwise using npm programmatically is a bit of a pain. - this.#setEnv('npm_execpath', this.flat.npmBin ?? null) + this.#setEnv('npm_execpath', this.#get('npm-bin') ?? null) // also set some other common nice envs that we want to rely on this.#setEnv('INIT_CWD', this.#cwd) @@ -649,6 +656,127 @@ class Config { } } + // ============================================= + // + // Credentials + // + // ============================================= + + #clearByURI (uri) { + const nerfed = nerfDart(uri) + const def = nerfDart(this.#get('registry')) + if (def === nerfed) { + this.#delete(`-authtoken`, Locations.user) + this.#delete(`_authToken`, Locations.user) + this.#delete(`_authtoken`, Locations.user) + this.#delete(`_auth`, Locations.user) + this.#delete(`_password`, Locations.user) + this.#delete(`username`, Locations.user) + // de-nerf email if it's nerfed to the default registry + const email = this.#get(`${nerfed}:email`, Locations.user) + if (email) { + this.#set('email', email) + } + } + for (const k of NerfDarts) { + this.#delete(`${nerfed}:${k}`) + } + } + + #setByURI (uri, { token, username, password, email, certfile, keyfile }) { + const nerfed = nerfDart(uri) + + // email is either provided, a top level key, or nothing + email = email || this.#get('email', Locations.user) + + // field that hasn't been used as documented for a LONG time, + // and as of npm 7.10.0, isn't used at all. We just always + // send auth if we have it, only to the URIs under the nerf dart. + this.#delete(`${nerfed}:always-auth`, Locations.user) + + this.#delete(`${nerfed}:email`, Locations.user) + if (certfile && keyfile) { + this.#set(`${nerfed}:certfile`, certfile, Locations.user) + this.#set(`${nerfed}:keyfile`, keyfile, Locations.user) + // cert/key may be used in conjunction with other credentials, thus no `else` + } + if (token) { + this.#set(`${nerfed}:_authToken`, token, Locations.user) + this.#delete(`${nerfed}:_password`, Locations.user) + this.#delete(`${nerfed}:username`, Locations.user) + } else if (username || password) { + if (!username) { + throw new Error('must include username') + } + if (!password) { + throw new Error('must include password') + } + this.#delete(`${nerfed}:_authToken`, Locations.user) + this.#set(`${nerfed}:username`, username, Locations.user) + // note: not encrypted, no idea why we bothered to do this, but oh well + // protects against shoulder-hacks if password is memorable, I guess? + const encoded = Buffer.from(password, 'utf8').toString('base64') + this.#set(`${nerfed}:_password`, encoded, Locations.user) + } else if (!certfile || !keyfile) { + throw new Error('No credentials to set.') + } + } + + // this has to be a bit more complicated to support legacy data of all forms + #getByURI (uri) { + const nerfed = nerfDart(uri) + const def = nerfDart(this.#get('registry')) + const creds = {} + + // email is handled differently, it used to always be nerfed and now it never should be + // if it's set nerfed to the default registry, then we copy it to the unnerfed key + // TODO: evaluate removing 'email' from the credentials object returned here + const email = this.#get(`${nerfed}:email`) || this.#get('email') + if (email) { + if (nerfed === def) { + this.#set('email', email) + } + creds.email = email + } + + const certfileReg = this.#get(`${nerfed}:certfile`) + const keyfileReg = this.#get(`${nerfed}:keyfile`) + if (certfileReg && keyfileReg) { + creds.certfile = certfileReg + creds.keyfile = keyfileReg + // cert/key may be used in conjunction with other credentials, thus no `return` + } + + const tokenReg = this.#get(`${nerfed}:_authToken`) + if (tokenReg) { + creds.token = tokenReg + return creds + } + + const userReg = this.#get(`${nerfed}:username`) + const passReg = this.#get(`${nerfed}:_password`) + if (userReg && passReg) { + creds.username = userReg + creds.password = Buffer.from(passReg, 'base64').toString('utf8') + const auth = `${creds.username}:${creds.password}` + creds.auth = Buffer.from(auth, 'utf8').toString('base64') + return creds + } + + const authReg = this.#get(`${nerfed}:_auth`) + if (authReg) { + const authDecode = Buffer.from(authReg, 'base64').toString('utf8') + const authSplit = authDecode.split(':') + creds.username = authSplit.shift() + creds.password = authSplit.join(':') + creds.auth = authReg + return creds + } + + // at this point, nothing else is usable so just return what we do have + return creds + } + // ============================================= // // Utils/Misc diff --git a/workspaces/config/lib/set-globals.js b/workspaces/config/lib/set-globals.js index f3043e8d43e0c..f99d0b592c280 100644 --- a/workspaces/config/lib/set-globals.js +++ b/workspaces/config/lib/set-globals.js @@ -37,7 +37,7 @@ const envReplace = (env, f) => f.replace(envExpr, (orig, esc, name) => { const setProcess = (proc, key, val) => { if (!ProcessKeys.has(key)) { - throw new Error(`attempted to set non-allowed process: ${key}`) + throw new Error(`not allowed to set process key: ${key}`) } proc[key] = val } diff --git a/workspaces/config/lib/type-defs.js b/workspaces/config/lib/type-defs.js deleted file mode 100644 index 3eab7ae4c1b42..0000000000000 --- a/workspaces/config/lib/type-defs.js +++ /dev/null @@ -1,239 +0,0 @@ -const nopt = require('nopt').lib -const semver = require('semver') -const querystring = require('querystring') -const { resolve } = require('path') -const { networkInterfaces } = require('os') - -const validateUmask = (data, k, val) => { - if (typeof val === 'string') { - if (/^0o?[0-7]+$/.test(val)) { - data[k] = parseInt(val.replace(/^0o?/, ''), 8) - } else if (/^[1-9][0-9]*$/.test(val)) { - data[k] = parseInt(val, 10) - } - return false - } - - if (typeof val !== 'number') { - return false - } - - val = Math.floor(val) - - if (val < 0 || val > 511) { - return false - } - - data[k] = val -} - -// Override nopt path validaton to use the HOME and PLATFORM -// values set by @npmcli/config constructor -const validatePath = (data, k, val) => { - if (typeof val !== 'string') { - return false - } - - const isWin = typeDefs.Path.PLATFORM === 'win32' - const homePattern = isWin ? /^~(\/|\\)/ : /^~\// - const home = typeDefs.Path.HOME - - if (home && val.match(homePattern)) { - data[k] = resolve(home, val.slice(2)) - } else { - data[k] = resolve(val) - } -} - -function validatePositiveNumber (data, k, val) { - if (isNaN(val)) { - return false - } - val = +val - if (val < 1) { - return false - } - data[k] = val -} - -const validateQs = (data, k, val) => { - data[k] = querystring.parse(val.replace(/\s+/g, '&')) -} - -const validateCsv = (data, k, val) => { - data[k] = val.split(',') -} - -const validateScope = (data, k, val) => { - data[k] = !/^@/.test(val) ? `@${val}` : val -} - -const IpAddress = (() => { - try { - return [ - null, - ...Object.values(networkInterfaces()).flatMap(i => i.map(ii => ii.address)), - ] - } catch { - return [null] - } -})() - -const isStrictBool = (val) => { - if (typeof val === 'boolean' || val === 'true' || val === 'false') { - return true - } -} - -// `description` gets shown during a runtime validation warning -// `typeDescription` gets displayed in the docs for the `Type:` -const typeDefs = { - String: { - ...nopt.typeDefs.String, - type: Symbol('String'), - typeDescription: 'String', - description: 'a string', - }, - Boolean: { - ...nopt.typeDefs.Boolean, - type: Symbol('Boolean'), - typeDescription: 'Boolean', - description: 'a boolean value (true or false)', - isBoolean: true, - }, - // todo: when type is all numbers, allow string verion of those numbers too - Number: { - ...nopt.typeDefs.Number, - type: Symbol('Number'), - typeDescription: 'Number', - description: 'a numeric value', - hint: 'number', - }, - PositiveInteger: { - type: Symbol('PositiveInteger'), - validate: validatePositiveNumber, - typeDescription: 'Positive integer', - description: 'an integer greater than or equal to 1', - hint: '1|2|3|n', - }, - Date: { - ...nopt.typeDefs.Date, - type: Symbol('Date'), - typeDescription: 'Date', - description: 'a valid Date string', - }, - URL: { - ...nopt.typeDefs.url, - type: Symbol('URL'), - typeDescription: 'URL', - description: 'a full url with "http://"', - }, - Querystring: { - type: Symbol('Querystring'), - validate: validateQs, - typeDescription: 'Querystring', - description: 'a space-delimited querystring', - hint: 'key=val key2=val2', - }, - CSV: { - type: Symbol('CSV'), - validate: validateCsv, - typeDescription: 'Comma-delimited string', - description: 'a comma-delimited string', - }, - Scope: { - type: Symbol('Scope'), - validate: validateScope, - typeDescription: 'Scope', - description: 'an npm scope with or without the leading @', - hint: '@scope', - }, - Spec: { - type: Symbol('Spec'), - validate: nopt.typeDefs.String.validate, - typeDescription: 'Spec', - description: 'an npm package spec', - hint: 'package-spec', - }, - Path: { - ...nopt.typeDefs.path, - type: Symbol('Path'), - validate: validatePath, - typeDescription: 'Path', - description: 'a valid filesystem path', - }, - Semver: { - type: Symbol('Semver'), - validate: (data, k, val) => { - const valid = semver.valid(val) - if (!valid) { - return false - } - data[k] = valid - }, - typeDescription: 'SemVer string', - description: 'a full valid SemVer string', - }, - Umask: { - type: Symbol('Umask'), - validate: validateUmask, - typeDescription: 'Octal numeric string in range 0000..0777 (0..511)', - description: 'an octal number in range 0o000..0o777 (0..511)', - }, - IpAddress: { - // this one cant be a symbol because it is an actual array of local - // ip addresses for the current runtime - type: IpAddress, - typeDescription: 'IP Address', - // this explicitly has no description since it is an array of values - // that will be shown to the user when it is invalid - description: null, - }, - BooleanOrString: { - type: Symbol('BooleanOrString'), - validate: (data, k, val) => { - if (isStrictBool(val)) { - return nopt.typeDefs.Boolean.validate(data, k, val) - } - return nopt.typeDefs.String.validate(data, k, val) - }, - typeDescription: ['Boolean', 'String'], - description: ['a boolean value (true or false)', 'a string'], - isBoolean: true, - }, - BooleanOrNumber: { - type: Symbol('BooleanOrNumber'), - validate: (data, k, val) => { - if (isStrictBool(val)) { - return nopt.typeDefs.Boolean.validate(data, k, val) - } - return nopt.typeDefs.Number.validate(data, k, val) - }, - typeDescription: ['Boolean', 'Number'], - description: ['a boolean value (true or false)', 'a a numeric valid'], - isBoolean: true, - }, - // the array type has no validation or description its presence in a - // definition signals that more than one value of the other types are allowed - Array: { - type: Symbol('Array'), - }, -} - -const byType = new Map() -const Types = {} -const TypesList = [] -const getType = (k) => byType.get(k) - -for (const [key, value] of Object.entries(typeDefs)) { - byType.set(value.type, value) - Types[key] = value.type - TypesList.push(value.type) -} - -module.exports = { - typeDefs, - Types, - TypesList, - getType, -} diff --git a/workspaces/config/package.json b/workspaces/config/package.json index bb7a4e35caf02..032760eaa7b46 100644 --- a/workspaces/config/package.json +++ b/workspaces/config/package.json @@ -39,6 +39,7 @@ "dependencies": { "@npmcli/arborist": "^6.1.5", "@npmcli/map-workspaces": "^3.0.0", + "@npmcli/mock-globals": "^1.0.0", "ci-info": "^3.7.1", "ini": "^3.0.0", "nopt": "file:../../../nopt/nopt-7.0.0.tgz", diff --git a/workspaces/config/tap-snapshots/test/definitions/snapshots.js.test.cjs b/workspaces/config/tap-snapshots/test/definitions/snapshots.js.test.cjs new file mode 100644 index 0000000000000..6a96f15efed98 --- /dev/null +++ b/workspaces/config/tap-snapshots/test/definitions/snapshots.js.test.cjs @@ -0,0 +1,4033 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/definitions/snapshots.js TAP definitions _auth > must match snapshot 1`] = ` +#### \`_auth\` + +* Default: null +* Type: null or String + +A basic-auth string to use when authenticating against the npm registry. +This will ONLY be used to authenticate against the npm registry. For other +registries you will need to scope it like "//other-registry.tld/:_auth" + +Warning: This should generally not be set via a command-line option. It is +safer to use a registry-provided authentication bearer token stored in the +~/.npmrc file by running \`npm login\`. +---------------------------------------- +USAGE: --_auth <_auth> +---------------------------------------- +INVALID: Must be one of: null, a string +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(String) +---------------------------------------- +DERIVED: _auth +` + +exports[`test/definitions/snapshots.js TAP definitions access > must match snapshot 1`] = ` +#### \`access\` + +* Default: 'public' for new packages, existing packages it will not change the + current level +* Type: null, "restricted", or "public" + +If do not want your scoped package to be publicly viewable (and installable) +set \`--access=restricted\`. + +Unscoped packages can not be set to \`restricted\`. + +Note: This defaults to not changing the current access level for existing +packages. Specifying a value of \`restricted\` or \`public\` during publish will +change the access for an existing package the same way that \`npm access set +status\` would. +---------------------------------------- +USAGE: --access +---------------------------------------- +INVALID: Must be one of: null, "restricted", "public" +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,restricted,public +---------------------------------------- +DERIVED: access +` + +exports[`test/definitions/snapshots.js TAP definitions all > must match snapshot 1`] = ` +#### \`all\` + +* Default: false +* Type: Boolean + +When running \`npm outdated\` and \`npm ls\`, setting \`--all\` will show all +outdated or installed packages, rather than only those directly depended +upon by the current project. +---------------------------------------- +USAGE: -a|--no-all|--all +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: all +` + +exports[`test/definitions/snapshots.js TAP definitions allow-same-version > must match snapshot 1`] = ` +#### \`allow-same-version\` + +* Default: false +* Type: Boolean + +Prevents throwing an error when \`npm version\` is used to set the new version +to the same value as the current version. +---------------------------------------- +USAGE: --no-allow-same-version|--allow-same-version +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: allowSameVersion +` + +exports[`test/definitions/snapshots.js TAP definitions also > must match snapshot 1`] = ` +#### \`also\` + +* Default: null +* Type: null, "dev", or "development" +* DEPRECATED: Please use --include=dev instead. + +When set to \`dev\` or \`development\`, this is an alias for \`--include=dev\`. +---------------------------------------- +USAGE: --also +---------------------------------------- +INVALID: Must be one of: null, "dev", "development" +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,dev,development +---------------------------------------- +DERIVED: omit,include +` + +exports[`test/definitions/snapshots.js TAP definitions audit > must match snapshot 1`] = ` +#### \`audit\` + +* Default: true +* Type: Boolean + +When "true" submit audit reports alongside the current npm command to the +default registry and all registries configured for scopes. See the +documentation for [\`npm audit\`](/commands/npm-audit) for details on what is +submitted. +---------------------------------------- +USAGE: --no-audit|--audit +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: true +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: audit +` + +exports[`test/definitions/snapshots.js TAP definitions audit-level > must match snapshot 1`] = ` +#### \`audit-level\` + +* Default: null +* Type: null, "info", "low", "moderate", "high", "critical", or "none" + +The minimum level of vulnerability for \`npm audit\` to exit with a non-zero +exit code. +---------------------------------------- +USAGE: --audit-level +---------------------------------------- +INVALID: Must be one of: null, "info", "low", "moderate", "high", "critical", "none" +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,info,low,moderate,high,critical,none +---------------------------------------- +DERIVED: auditLevel +` + +exports[`test/definitions/snapshots.js TAP definitions auth-type > must match snapshot 1`] = ` +#### \`auth-type\` + +* Default: "web" +* Type: "legacy" or "web" + +What authentication strategy to use with \`login\`. +---------------------------------------- +USAGE: --auth-type +---------------------------------------- +INVALID: Must be one of: "legacy", "web" +---------------------------------------- +DEFAULT: "web" +---------------------------------------- +TYPES: legacy,web +---------------------------------------- +DERIVED: authType +` + +exports[`test/definitions/snapshots.js TAP definitions before > must match snapshot 1`] = ` +#### \`before\` + +* Default: null +* Type: null or Date + +If passed to \`npm install\`, will rebuild the npm tree such that only +versions that were available **on or before** the \`--before\` time get +installed. If there's no versions available for the current set of direct +dependencies, the command will error. + +If the requested version is a \`dist-tag\` and the given tag does not pass the +\`--before\` filter, the most recent version less than or equal to that tag +will be used. For example, \`foo@latest\` might install \`foo@1.2\` even though +\`latest\` is \`2.0\`. +---------------------------------------- +USAGE: --before +---------------------------------------- +INVALID: Must be one of: null, a valid Date string +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(Date) +---------------------------------------- +DERIVED: before +` + +exports[`test/definitions/snapshots.js TAP definitions bin-links > must match snapshot 1`] = ` +#### \`bin-links\` + +* Default: true +* Type: Boolean + +Tells npm to create symlinks (or \`.cmd\` shims on Windows) for package +executables. + +Set to false to have it not do this. This can be used to work around the +fact that some file systems don't support symlinks, even on ostensibly Unix +systems. +---------------------------------------- +USAGE: --no-bin-links|--bin-links +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: true +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: binLinks +` + +exports[`test/definitions/snapshots.js TAP definitions browser > must match snapshot 1`] = ` +#### \`browser\` + +* Default: OS X: \`"open"\`, Windows: \`"start"\`, Others: \`"xdg-open"\` +* Type: null, Boolean, or String + +The browser that is called by npm commands to open websites. + +Set to \`false\` to suppress browser behavior and instead print urls to +terminal. + +Set to \`true\` to use default system URL opener. +---------------------------------------- +USAGE: --no-browser|--browser +---------------------------------------- +INVALID: Must be one of: null, a boolean value (true or false), a string +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(BooleanOrString) +---------------------------------------- +DERIVED: browser +` + +exports[`test/definitions/snapshots.js TAP definitions ca > must match snapshot 1`] = ` +#### \`ca\` + +* Default: null +* Type: null or String (can be set multiple times) + +The Certificate Authority signing certificate that is trusted for SSL +connections to the registry. Values should be in PEM format (Windows calls +it "Base-64 encoded X.509 (.CER)") with newlines replaced by the string +"\\n". For example: + +\`\`\`ini +ca="-----BEGIN CERTIFICATE-----\\nXXXX\\nXXXX\\n-----END CERTIFICATE-----" +\`\`\` + +Set to \`null\` to only allow "known" registrars, or to a specific CA cert to +trust only that specific signing authority. + +Multiple CAs can be trusted by specifying an array of certificates: + +\`\`\`ini +ca[]="..." +ca[]="..." +\`\`\` + +See also the \`strict-ssl\` config. +---------------------------------------- +USAGE: --ca [--ca ...] +---------------------------------------- +INVALID: Must be one or more of: null, a string +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(String),Symbol(Array) +---------------------------------------- +DERIVED: ca +` + +exports[`test/definitions/snapshots.js TAP definitions cache > must match snapshot 1`] = ` +#### \`cache\` + +* Default: Windows: \`%LocalAppData%\\npm-cache\`, Posix: \`~/.npm\` +* Type: Path + +The location of npm's cache directory. +---------------------------------------- +USAGE: --cache +---------------------------------------- +INVALID: Must be a valid filesystem path +---------------------------------------- +DEFAULT: "~/.npm" +---------------------------------------- +TYPES: Symbol(Path) +---------------------------------------- +DERIVED: cache,npxCache,logsDir +` + +exports[`test/definitions/snapshots.js TAP definitions cache-max > must match snapshot 1`] = ` +#### \`cache-max\` + +* Default: Infinity +* Type: Number +* DEPRECATED: This option has been deprecated in favor of \`--prefer-online\` + +\`--cache-max=0\` is an alias for \`--prefer-online\` +---------------------------------------- +USAGE: --cache-max +---------------------------------------- +INVALID: Must be a numeric value +---------------------------------------- +DEFAULT: Infinity +---------------------------------------- +TYPES: Symbol(Number) +---------------------------------------- +DERIVED: preferOnline +` + +exports[`test/definitions/snapshots.js TAP definitions cache-min > must match snapshot 1`] = ` +#### \`cache-min\` + +* Default: 0 +* Type: Number +* DEPRECATED: This option has been deprecated in favor of \`--prefer-offline\`. + +\`--cache-min=9999 (or bigger)\` is an alias for \`--prefer-offline\`. +---------------------------------------- +USAGE: --cache-min +---------------------------------------- +INVALID: Must be a numeric value +---------------------------------------- +DEFAULT: 0 +---------------------------------------- +TYPES: Symbol(Number) +---------------------------------------- +DERIVED: preferOffline +` + +exports[`test/definitions/snapshots.js TAP definitions cafile > must match snapshot 1`] = ` +#### \`cafile\` + +* Default: null +* Type: null or Path + +A path to a file containing one or multiple Certificate Authority signing +certificates. Similar to the \`ca\` setting, but allows for multiple CA's, as +well as for the CA information to be stored in a file on disk. +---------------------------------------- +USAGE: --cafile +---------------------------------------- +INVALID: Must be a valid filesystem path +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(Path) +---------------------------------------- +DERIVED: ca +` + +exports[`test/definitions/snapshots.js TAP definitions call > must match snapshot 1`] = ` +#### \`call\` + +* Default: "" +* Type: String + +Optional companion option for \`npm exec\`, \`npx\` that allows for specifying a +custom command to be run along with the installed packages. + +\`\`\`bash +npm exec --package yo --package generator-node --call "yo node" +\`\`\` +---------------------------------------- +USAGE: -c|--call +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "" +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: call +` + +exports[`test/definitions/snapshots.js TAP definitions cert > must match snapshot 1`] = ` +#### \`cert\` + +* Default: null +* Type: null or String +* DEPRECATED: \`key\` and \`cert\` are no longer used for most registry + operations. Use registry scoped \`keyfile\` and \`certfile\` instead. Example: + //other-registry.tld/:keyfile=/path/to/key.pem + //other-registry.tld/:certfile=/path/to/cert.crt + +A client certificate to pass when accessing the registry. Values should be +in PEM format (Windows calls it "Base-64 encoded X.509 (.CER)") with +newlines replaced by the string "\\n". For example: + +\`\`\`ini +cert="-----BEGIN CERTIFICATE-----\\nXXXX\\nXXXX\\n-----END CERTIFICATE-----" +\`\`\` + +It is _not_ the path to a certificate file, though you can set a +registry-scoped "certfile" path like +"//other-registry.tld/:certfile=/path/to/cert.pem". +---------------------------------------- +USAGE: --cert +---------------------------------------- +INVALID: Must be one of: null, a string +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(String) +---------------------------------------- +DERIVED: cert +` + +exports[`test/definitions/snapshots.js TAP definitions ci-name > must match snapshot 1`] = ` +#### \`ci-name\` + +* Default: The name of the current CI system, or \`null\` when not on a known CI + platform. +* Type: null or String + +The name of a continuous integration system. If not set explicitly, npm will +detect the current CI environment using the +[\`ci-info\`](http://npm.im/ci-info) module. +---------------------------------------- +USAGE: --ci-name +---------------------------------------- +INVALID: Must be one of: null, a string +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(String) +---------------------------------------- +DERIVED: ciName,userAgent +` + +exports[`test/definitions/snapshots.js TAP definitions cidr > must match snapshot 1`] = ` +#### \`cidr\` + +* Default: null +* Type: null or String (can be set multiple times) + +This is a list of CIDR address to be used when configuring limited access +tokens with the \`npm token create\` command. +---------------------------------------- +USAGE: --cidr [--cidr ...] +---------------------------------------- +INVALID: Must be one or more of: null, a string +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(String),Symbol(Array) +---------------------------------------- +DERIVED: cidr +` + +exports[`test/definitions/snapshots.js TAP definitions color > must match snapshot 1`] = ` +#### \`color\` + +* Default: true unless the NO_COLOR environ is set to something other than '0' +* Type: "always" or Boolean + +If false, never shows colors. If \`"always"\` then always shows colors. If +true, then only prints color codes for tty file descriptors. +---------------------------------------- +USAGE: --no-color|--color +---------------------------------------- +INVALID: Must be one of: "always", a boolean value (true or false) +---------------------------------------- +DEFAULT: true +---------------------------------------- +TYPES: always,Symbol(Boolean) +---------------------------------------- +DERIVED: color,logColor +` + +exports[`test/definitions/snapshots.js TAP definitions commit-hooks > must match snapshot 1`] = ` +#### \`commit-hooks\` + +* Default: true +* Type: Boolean + +Run git commit hooks when using the \`npm version\` command. +---------------------------------------- +USAGE: --no-commit-hooks|--commit-hooks +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: true +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: commitHooks +` + +exports[`test/definitions/snapshots.js TAP definitions depth > must match snapshot 1`] = ` +#### \`depth\` + +* Default: \`Infinity\` if \`--all\` is set, otherwise \`1\` +* Type: null or Number + +The depth to go when recursing packages for \`npm ls\`. + +If not set, \`npm ls\` will show only the immediate dependencies of the root +project. If \`--all\` is set, then npm will show all dependencies by default. +---------------------------------------- +USAGE: --depth +---------------------------------------- +INVALID: Must be one of: null, a numeric value +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(Number) +---------------------------------------- +DERIVED: depth +` + +exports[`test/definitions/snapshots.js TAP definitions description > must match snapshot 1`] = ` +#### \`description\` + +* Default: true +* Type: Boolean + +Show the description in \`npm search\` +---------------------------------------- +USAGE: --no-description|--description +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: true +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: search.description +` + +exports[`test/definitions/snapshots.js TAP definitions dev > must match snapshot 1`] = ` +#### \`dev\` + +* Default: false +* Type: Boolean +* DEPRECATED: Please use --include=dev instead. + +Alias for \`--include=dev\`. +---------------------------------------- +USAGE: --no-dev|--dev +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: omit,include +` + +exports[`test/definitions/snapshots.js TAP definitions diff > must match snapshot 1`] = ` +#### \`diff\` + +* Default: [] +* Type: Spec (can be set multiple times) + +Define arguments to compare in \`npm diff\`. +---------------------------------------- +USAGE: --diff [--diff ...] +---------------------------------------- +INVALID: Must be one or more an npm package spec +---------------------------------------- +DEFAULT: [] +---------------------------------------- +TYPES: Symbol(Spec),Symbol(Array) +---------------------------------------- +DERIVED: diff +` + +exports[`test/definitions/snapshots.js TAP definitions diff-dst-prefix > must match snapshot 1`] = ` +#### \`diff-dst-prefix\` + +* Default: "b/" +* Type: String + +Destination prefix to be used in \`npm diff\` output. +---------------------------------------- +USAGE: --diff-dst-prefix +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "b/" +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: diffDstPrefix +` + +exports[`test/definitions/snapshots.js TAP definitions diff-ignore-all-space > must match snapshot 1`] = ` +#### \`diff-ignore-all-space\` + +* Default: false +* Type: Boolean + +Ignore whitespace when comparing lines in \`npm diff\`. +---------------------------------------- +USAGE: --no-diff-ignore-all-space|--diff-ignore-all-space +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: diffIgnoreAllSpace +` + +exports[`test/definitions/snapshots.js TAP definitions diff-name-only > must match snapshot 1`] = ` +#### \`diff-name-only\` + +* Default: false +* Type: Boolean + +Prints only filenames when using \`npm diff\`. +---------------------------------------- +USAGE: --no-diff-name-only|--diff-name-only +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: diffNameOnly +` + +exports[`test/definitions/snapshots.js TAP definitions diff-no-prefix > must match snapshot 1`] = ` +#### \`diff-no-prefix\` + +* Default: false +* Type: Boolean + +Do not show any source or destination prefix in \`npm diff\` output. + +Note: this causes \`npm diff\` to ignore the \`--diff-src-prefix\` and +\`--diff-dst-prefix\` configs. +---------------------------------------- +USAGE: --no-diff-no-prefix|--diff-no-prefix +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: diffNoPrefix +` + +exports[`test/definitions/snapshots.js TAP definitions diff-src-prefix > must match snapshot 1`] = ` +#### \`diff-src-prefix\` + +* Default: "a/" +* Type: String + +Source prefix to be used in \`npm diff\` output. +---------------------------------------- +USAGE: --diff-src-prefix +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "a/" +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: diffSrcPrefix +` + +exports[`test/definitions/snapshots.js TAP definitions diff-text > must match snapshot 1`] = ` +#### \`diff-text\` + +* Default: false +* Type: Boolean + +Treat all files as text in \`npm diff\`. +---------------------------------------- +USAGE: --no-diff-text|--diff-text +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: diffText +` + +exports[`test/definitions/snapshots.js TAP definitions diff-unified > must match snapshot 1`] = ` +#### \`diff-unified\` + +* Default: 3 +* Type: Number + +The number of lines of context to print in \`npm diff\`. +---------------------------------------- +USAGE: --diff-unified +---------------------------------------- +INVALID: Must be a numeric value +---------------------------------------- +DEFAULT: 3 +---------------------------------------- +TYPES: Symbol(Number) +---------------------------------------- +DERIVED: diffUnified +` + +exports[`test/definitions/snapshots.js TAP definitions dry-run > must match snapshot 1`] = ` +#### \`dry-run\` + +* Default: false +* Type: Boolean + +Indicates that you don't want npm to make any changes and that it should +only report what it would have done. This can be passed into any of the +commands that modify your local installation, eg, \`install\`, \`update\`, +\`dedupe\`, \`uninstall\`, as well as \`pack\` and \`publish\`. + +Note: This is NOT honored by other network related commands, eg \`dist-tags\`, +\`owner\`, etc. +---------------------------------------- +USAGE: --no-dry-run|--dry-run +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: dryRun +` + +exports[`test/definitions/snapshots.js TAP definitions editor > must match snapshot 1`] = ` +#### \`editor\` + +* Default: The EDITOR or VISUAL environment variables, or + '%SYSTEMROOT%\\notepad.exe' on Windows, or 'vi' on Unix systems +* Type: String + +The command to run for \`npm edit\` and \`npm config edit\`. +---------------------------------------- +USAGE: --editor +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "code -r -w" +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: editor +` + +exports[`test/definitions/snapshots.js TAP definitions engine-strict > must match snapshot 1`] = ` +#### \`engine-strict\` + +* Default: false +* Type: Boolean + +If set to true, then npm will stubbornly refuse to install (or even consider +installing) any package that claims to not be compatible with the current +Node.js version. + +This can be overridden by setting the \`--force\` flag. +---------------------------------------- +USAGE: --no-engine-strict|--engine-strict +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: engineStrict +` + +exports[`test/definitions/snapshots.js TAP definitions fetch-retries > must match snapshot 1`] = ` +#### \`fetch-retries\` + +* Default: 2 +* Type: Number + +The "retries" config for the \`retry\` module to use when fetching packages +from the registry. + +npm will retry idempotent read requests to the registry in the case of +network failures or 5xx HTTP errors. +---------------------------------------- +USAGE: --fetch-retries +---------------------------------------- +INVALID: Must be a numeric value +---------------------------------------- +DEFAULT: 2 +---------------------------------------- +TYPES: Symbol(Number) +---------------------------------------- +DERIVED: retry.retries +` + +exports[`test/definitions/snapshots.js TAP definitions fetch-retry-factor > must match snapshot 1`] = ` +#### \`fetch-retry-factor\` + +* Default: 10 +* Type: Number + +The "factor" config for the \`retry\` module to use when fetching packages. +---------------------------------------- +USAGE: --fetch-retry-factor +---------------------------------------- +INVALID: Must be a numeric value +---------------------------------------- +DEFAULT: 10 +---------------------------------------- +TYPES: Symbol(Number) +---------------------------------------- +DERIVED: retry.factor +` + +exports[`test/definitions/snapshots.js TAP definitions fetch-retry-maxtimeout > must match snapshot 1`] = ` +#### \`fetch-retry-maxtimeout\` + +* Default: 60000 (1 minute) +* Type: Number + +The "maxTimeout" config for the \`retry\` module to use when fetching +packages. +---------------------------------------- +USAGE: --fetch-retry-maxtimeout +---------------------------------------- +INVALID: Must be a numeric value +---------------------------------------- +DEFAULT: 60000 +---------------------------------------- +TYPES: Symbol(Number) +---------------------------------------- +DERIVED: retry.maxTimeout +` + +exports[`test/definitions/snapshots.js TAP definitions fetch-retry-mintimeout > must match snapshot 1`] = ` +#### \`fetch-retry-mintimeout\` + +* Default: 10000 (10 seconds) +* Type: Number + +The "minTimeout" config for the \`retry\` module to use when fetching +packages. +---------------------------------------- +USAGE: --fetch-retry-mintimeout +---------------------------------------- +INVALID: Must be a numeric value +---------------------------------------- +DEFAULT: 10000 +---------------------------------------- +TYPES: Symbol(Number) +---------------------------------------- +DERIVED: retry.minTimeout +` + +exports[`test/definitions/snapshots.js TAP definitions fetch-timeout > must match snapshot 1`] = ` +#### \`fetch-timeout\` + +* Default: 300000 (5 minutes) +* Type: Number + +The maximum amount of time to wait for HTTP requests to complete. +---------------------------------------- +USAGE: --fetch-timeout +---------------------------------------- +INVALID: Must be a numeric value +---------------------------------------- +DEFAULT: 300000 +---------------------------------------- +TYPES: Symbol(Number) +---------------------------------------- +DERIVED: timeout +` + +exports[`test/definitions/snapshots.js TAP definitions force > must match snapshot 1`] = ` +#### \`force\` + +* Default: false +* Type: Boolean + +Removes various protections against unfortunate side effects, common +mistakes, unnecessary performance degradation, and malicious input. + +* Allow clobbering non-npm files in global installs. +* Allow the \`npm version\` command to work on an unclean git repository. +* Allow deleting the cache folder with \`npm cache clean\`. +* Allow installing packages that have an \`engines\` declaration requiring a + different version of npm. +* Allow installing packages that have an \`engines\` declaration requiring a + different version of \`node\`, even if \`--engine-strict\` is enabled. +* Allow \`npm audit fix\` to install modules outside your stated dependency + range (including SemVer-major changes). +* Allow unpublishing all versions of a published package. +* Allow conflicting peerDependencies to be installed in the root project. +* Implicitly set \`--yes\` during \`npm init\`. +* Allow clobbering existing values in \`npm pkg\` +* Allow unpublishing of entire packages (not just a single version). + +If you don't have a clear idea of what you want to do, it is strongly +recommended that you do not use this option! +---------------------------------------- +USAGE: -f|--no-force|--force +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: force +` + +exports[`test/definitions/snapshots.js TAP definitions foreground-scripts > must match snapshot 1`] = ` +#### \`foreground-scripts\` + +* Default: false +* Type: Boolean + +Run all build scripts (ie, \`preinstall\`, \`install\`, and \`postinstall\`) +scripts for installed packages in the foreground process, sharing standard +input, output, and error with the main npm process. + +Note that this will generally make installs run slower, and be much noisier, +but can be useful for debugging. +---------------------------------------- +USAGE: --no-foreground-scripts|--foreground-scripts +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: foregroundScripts +` + +exports[`test/definitions/snapshots.js TAP definitions format-package-lock > must match snapshot 1`] = ` +#### \`format-package-lock\` + +* Default: true +* Type: Boolean + +Format \`package-lock.json\` or \`npm-shrinkwrap.json\` as a human readable +file. +---------------------------------------- +USAGE: --no-format-package-lock|--format-package-lock +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: true +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: formatPackageLock +` + +exports[`test/definitions/snapshots.js TAP definitions fund > must match snapshot 1`] = ` +#### \`fund\` + +* Default: true +* Type: Boolean + +When "true" displays the message at the end of each \`npm install\` +acknowledging the number of dependencies looking for funding. See [\`npm +fund\`](/commands/npm-fund) for details. +---------------------------------------- +USAGE: --no-fund|--fund +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: true +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: fund +` + +exports[`test/definitions/snapshots.js TAP definitions git > must match snapshot 1`] = ` +#### \`git\` + +* Default: "git" +* Type: String + +The command to use for git commands. If git is installed on the computer, +but is not in the \`PATH\`, then set this to the full path to the git binary. +---------------------------------------- +USAGE: --git +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "git" +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: git +` + +exports[`test/definitions/snapshots.js TAP definitions git-tag-version > must match snapshot 1`] = ` +#### \`git-tag-version\` + +* Default: true +* Type: Boolean + +Tag the commit when using the \`npm version\` command. Setting this to false +results in no commit being made at all. +---------------------------------------- +USAGE: --no-git-tag-version|--git-tag-version +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: true +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: gitTagVersion +` + +exports[`test/definitions/snapshots.js TAP definitions global > must match snapshot 1`] = ` +#### \`global\` + +* Default: false +* Type: Boolean + +Operates in "global" mode, so that packages are installed into the \`prefix\` +folder instead of the current working directory. See +[folders](/configuring-npm/folders) for more on the differences in behavior. + +* packages are installed into the \`{prefix}/lib/node_modules\` folder, instead + of the current working directory. +* bin files are linked to \`{prefix}/bin\` +* man pages are linked to \`{prefix}/share/man\` +---------------------------------------- +USAGE: -g|--no-global|--global +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: global,location,localPrefix +` + +exports[`test/definitions/snapshots.js TAP definitions global-style > must match snapshot 1`] = ` +#### \`global-style\` + +* Default: false +* Type: Boolean +* DEPRECATED: This option has been deprecated in favor of + \`--install-strategy=shallow\` + +Only install direct dependencies in the top level \`node_modules\`, but hoist +on deeper dependendencies. Sets \`--install-strategy=shallow\`. +---------------------------------------- +USAGE: --no-global-style|--global-style +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: installStrategy +` + +exports[`test/definitions/snapshots.js TAP definitions globalconfig > must match snapshot 1`] = ` +#### \`globalconfig\` + +* Default: The global --prefix setting plus 'etc/npmrc'. For example, + '/usr/local/etc/npmrc' +* Type: null or Path + +The config file to read for global config options. +---------------------------------------- +USAGE: --globalconfig +---------------------------------------- +INVALID: Must be a valid filesystem path +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(Path) +---------------------------------------- +DERIVED: globalconfig,prefix,globalPrefix +` + +exports[`test/definitions/snapshots.js TAP definitions heading > must match snapshot 1`] = ` +#### \`heading\` + +* Default: "npm" +* Type: String + +The string that starts all the debugging log output. +---------------------------------------- +USAGE: --heading +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "npm" +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: heading +` + +exports[`test/definitions/snapshots.js TAP definitions https-proxy > must match snapshot 1`] = ` +#### \`https-proxy\` + +* Default: null +* Type: null or URL + +A proxy to use for outgoing https requests. If the \`HTTPS_PROXY\` or +\`https_proxy\` or \`HTTP_PROXY\` or \`http_proxy\` environment variables are set, +proxy settings will be honored by the underlying \`make-fetch-happen\` +library. +---------------------------------------- +USAGE: --https-proxy +---------------------------------------- +INVALID: Must be a full url with "http://" +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(URL) +---------------------------------------- +DERIVED: httpsProxy +` + +exports[`test/definitions/snapshots.js TAP definitions if-present > must match snapshot 1`] = ` +#### \`if-present\` + +* Default: false +* Type: Boolean + +If true, npm will not exit with an error code when \`run-script\` is invoked +for a script that isn't defined in the \`scripts\` section of \`package.json\`. +This option can be used when it's desirable to optionally run a script when +it's present and fail if the script fails. This is useful, for example, when +running scripts that may only apply for some builds in an otherwise generic +CI setup. + +This value is not exported to the environment for child processes. +---------------------------------------- +USAGE: --no-if-present|--if-present +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: ifPresent +` + +exports[`test/definitions/snapshots.js TAP definitions ignore-scripts > must match snapshot 1`] = ` +#### \`ignore-scripts\` + +* Default: false +* Type: Boolean + +If true, npm does not run scripts specified in package.json files. + +Note that commands explicitly intended to run a particular script, such as +\`npm start\`, \`npm stop\`, \`npm restart\`, \`npm test\`, and \`npm run-script\` +will still run their intended script if \`ignore-scripts\` is set, but they +will *not* run any pre- or post-scripts. +---------------------------------------- +USAGE: --no-ignore-scripts|--ignore-scripts +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: ignoreScripts +` + +exports[`test/definitions/snapshots.js TAP definitions include > must match snapshot 1`] = ` +#### \`include\` + +* Default: [] +* Type: "prod", "dev", "optional", or "peer" (can be set multiple times) + +Option that allows for defining which types of dependencies to install. + +This is the inverse of \`--omit=\`. + +Dependency types specified in \`--include\` will not be omitted, regardless of +the order in which omit/include are specified on the command-line. +---------------------------------------- +USAGE: --include [--include ...] +---------------------------------------- +INVALID: Must be one or more of: "prod", "dev", "optional", "peer" +---------------------------------------- +DEFAULT: [] +---------------------------------------- +TYPES: Symbol(Array),prod,dev,optional,peer +---------------------------------------- +DERIVED: omit,include +` + +exports[`test/definitions/snapshots.js TAP definitions include-staged > must match snapshot 1`] = ` +#### \`include-staged\` + +* Default: false +* Type: Boolean + +Allow installing "staged" published packages, as defined by [npm RFC PR +#92](https://github.com/npm/rfcs/pull/92). + +This is experimental, and not implemented by the npm public registry. +---------------------------------------- +USAGE: --no-include-staged|--include-staged +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: includeStaged +` + +exports[`test/definitions/snapshots.js TAP definitions include-workspace-root > must match snapshot 1`] = ` +#### \`include-workspace-root\` + +* Default: false +* Type: Boolean + +Include the workspace root when workspaces are enabled for a command. + +When false, specifying individual workspaces via the \`workspace\` config, or +all workspaces via the \`workspaces\` flag, will cause npm to operate only on +the specified workspaces, and not on the root project. + +This value is not exported to the environment for child processes. +---------------------------------------- +USAGE: -iwr|--no-include-workspace-root|--include-workspace-root +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: includeWorkspaceRoot +` + +exports[`test/definitions/snapshots.js TAP definitions init-author-email > must match snapshot 1`] = ` +#### \`init-author-email\` + +* Default: "" +* Type: String + +The value \`npm init\` should use by default for the package author's email. +---------------------------------------- +USAGE: --init-author-email +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "" +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions init-author-name > must match snapshot 1`] = ` +#### \`init-author-name\` + +* Default: "" +* Type: String + +The value \`npm init\` should use by default for the package author's name. +---------------------------------------- +USAGE: --init-author-name +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "" +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions init-author-url > must match snapshot 1`] = ` +#### \`init-author-url\` + +* Default: "" +* Type: "" or URL + +The value \`npm init\` should use by default for the package author's +homepage. +---------------------------------------- +USAGE: --init-author-url +---------------------------------------- +INVALID: Must be a full url with "http://" +---------------------------------------- +DEFAULT: "" +---------------------------------------- +TYPES: ,Symbol(URL) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions init-license > must match snapshot 1`] = ` +#### \`init-license\` + +* Default: "ISC" +* Type: String + +The value \`npm init\` should use by default for the package license. +---------------------------------------- +USAGE: --init-license +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "ISC" +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions init-module > must match snapshot 1`] = ` +#### \`init-module\` + +* Default: "~/.npm-init.js" +* Type: Path + +A module that will be loaded by the \`npm init\` command. See the +documentation for the +[init-package-json](https://github.com/npm/init-package-json) module for +more information, or [npm init](/commands/npm-init). +---------------------------------------- +USAGE: --init-module +---------------------------------------- +INVALID: Must be a valid filesystem path +---------------------------------------- +DEFAULT: "~/.npm-init.js" +---------------------------------------- +TYPES: Symbol(Path) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions init-version > must match snapshot 1`] = ` +#### \`init-version\` + +* Default: "1.0.0" +* Type: SemVer string + +The value that \`npm init\` should use by default for the package version +number, if not already set in package.json. +---------------------------------------- +USAGE: --init-version +---------------------------------------- +INVALID: Must be a full valid SemVer string +---------------------------------------- +DEFAULT: "1.0.0" +---------------------------------------- +TYPES: Symbol(Semver) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions init.author.email > must match snapshot 1`] = ` +#### \`init.author.email\` + +* Default: "" +* Type: String +* DEPRECATED: Use \`--init-author-email\` instead. + +Alias for \`--init-author-email\` +---------------------------------------- +USAGE: --init.author.email +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "" +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions init.author.name > must match snapshot 1`] = ` +#### \`init.author.name\` + +* Default: "" +* Type: String +* DEPRECATED: Use \`--init-author-name\` instead. + +Alias for \`--init-author-name\` +---------------------------------------- +USAGE: --init.author.name +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "" +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions init.author.url > must match snapshot 1`] = ` +#### \`init.author.url\` + +* Default: "" +* Type: "" or URL +* DEPRECATED: Use \`--init-author-url\` instead. + +Alias for \`--init-author-url\` +---------------------------------------- +USAGE: --init.author.url +---------------------------------------- +INVALID: Must be a full url with "http://" +---------------------------------------- +DEFAULT: "" +---------------------------------------- +TYPES: ,Symbol(URL) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions init.license > must match snapshot 1`] = ` +#### \`init.license\` + +* Default: "ISC" +* Type: String +* DEPRECATED: Use \`--init-license\` instead. + +Alias for \`--init-license\` +---------------------------------------- +USAGE: --init.license +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "ISC" +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions init.module > must match snapshot 1`] = ` +#### \`init.module\` + +* Default: "~/.npm-init.js" +* Type: Path +* DEPRECATED: Use \`--init-module\` instead. + +Alias for \`--init-module\` +---------------------------------------- +USAGE: --init.module +---------------------------------------- +INVALID: Must be a valid filesystem path +---------------------------------------- +DEFAULT: "~/.npm-init.js" +---------------------------------------- +TYPES: Symbol(Path) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions init.version > must match snapshot 1`] = ` +#### \`init.version\` + +* Default: "1.0.0" +* Type: SemVer string +* DEPRECATED: Use \`--init-version\` instead. + +Alias for \`--init-version\` +---------------------------------------- +USAGE: --init.version +---------------------------------------- +INVALID: Must be a full valid SemVer string +---------------------------------------- +DEFAULT: "1.0.0" +---------------------------------------- +TYPES: Symbol(Semver) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions install-links > must match snapshot 1`] = ` +#### \`install-links\` + +* Default: true +* Type: Boolean + +When set file: protocol dependencies will be packed and installed as regular +dependencies instead of creating a symlink. This option has no effect on +workspaces. +---------------------------------------- +USAGE: --no-install-links|--install-links +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: true +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: installLinks +` + +exports[`test/definitions/snapshots.js TAP definitions install-strategy > must match snapshot 1`] = ` +#### \`install-strategy\` + +* Default: "hoisted" +* Type: "hoisted", "nested", or "shallow" + +Sets the strategy for installing packages in node_modules. hoisted +(default): Install non-duplicated in top-level, and duplicated as necessary +within directory structure. nested: (formerly --legacy-bundling) install in +place, no hoisting. shallow (formerly --global-style) only install direct +deps at top-level. linked: (coming soon) install in node_modules/.store, +link in place, unhoisted. +---------------------------------------- +USAGE: --install-strategy +---------------------------------------- +INVALID: Must be one of: "hoisted", "nested", "shallow" +---------------------------------------- +DEFAULT: "hoisted" +---------------------------------------- +TYPES: hoisted,nested,shallow +---------------------------------------- +DERIVED: installStrategy +` + +exports[`test/definitions/snapshots.js TAP definitions json > must match snapshot 1`] = ` +#### \`json\` + +* Default: false +* Type: Boolean + +Whether or not to output JSON data, rather than the normal output. + +* In \`npm pkg set\` it enables parsing set values with JSON.parse() before + saving them to your \`package.json\`. + +Not supported by all npm commands. +---------------------------------------- +USAGE: --no-json|--json +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: json +` + +exports[`test/definitions/snapshots.js TAP definitions key > must match snapshot 1`] = ` +#### \`key\` + +* Default: null +* Type: null or String +* DEPRECATED: \`key\` and \`cert\` are no longer used for most registry + operations. Use registry scoped \`keyfile\` and \`certfile\` instead. Example: + //other-registry.tld/:keyfile=/path/to/key.pem + //other-registry.tld/:certfile=/path/to/cert.crt + +A client key to pass when accessing the registry. Values should be in PEM +format with newlines replaced by the string "\\n". For example: + +\`\`\`ini +key="-----BEGIN PRIVATE KEY-----\\nXXXX\\nXXXX\\n-----END PRIVATE KEY-----" +\`\`\` + +It is _not_ the path to a key file, though you can set a registry-scoped +"keyfile" path like "//other-registry.tld/:keyfile=/path/to/key.pem". +---------------------------------------- +USAGE: --key +---------------------------------------- +INVALID: Must be one of: null, a string +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(String) +---------------------------------------- +DERIVED: key +` + +exports[`test/definitions/snapshots.js TAP definitions legacy-bundling > must match snapshot 1`] = ` +#### \`legacy-bundling\` + +* Default: false +* Type: Boolean +* DEPRECATED: This option has been deprecated in favor of + \`--install-strategy=nested\` + +Instead of hoisting package installs in \`node_modules\`, install packages in +the same manner that they are depended on. This may cause very deep +directory structures and duplicate package installs as there is no +de-duplicating. Sets \`--install-strategy=nested\`. +---------------------------------------- +USAGE: --no-legacy-bundling|--legacy-bundling +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: installStrategy +` + +exports[`test/definitions/snapshots.js TAP definitions legacy-peer-deps > must match snapshot 1`] = ` +#### \`legacy-peer-deps\` + +* Default: false +* Type: Boolean + +Causes npm to completely ignore \`peerDependencies\` when building a package +tree, as in npm versions 3 through 6. + +If a package cannot be installed because of overly strict \`peerDependencies\` +that collide, it provides a way to move forward resolving the situation. + +This differs from \`--omit=peer\`, in that \`--omit=peer\` will avoid unpacking +\`peerDependencies\` on disk, but will still design a tree such that +\`peerDependencies\` _could_ be unpacked in a correct place. + +Use of \`legacy-peer-deps\` is not recommended, as it will not enforce the +\`peerDependencies\` contract that meta-dependencies may rely on. +---------------------------------------- +USAGE: --no-legacy-peer-deps|--legacy-peer-deps +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: legacyPeerDeps +` + +exports[`test/definitions/snapshots.js TAP definitions link > must match snapshot 1`] = ` +#### \`link\` + +* Default: false +* Type: Boolean + +Used with \`npm ls\`, limiting output to only those packages that are linked. +---------------------------------------- +USAGE: --no-link|--link +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions local-address > must match snapshot 1`] = ` +#### \`local-address\` + +* Default: null +* Type: null or IP Address + +The IP address of the local interface to use when making connections to the +npm registry. Must be IPv4 in versions of Node prior to 0.12. +---------------------------------------- +USAGE: --local-address +---------------------------------------- +INVALID: Must be one of: null, {...LOCAL-IPS} +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(IpAddress) +---------------------------------------- +DERIVED: localAddress +` + +exports[`test/definitions/snapshots.js TAP definitions location > must match snapshot 1`] = ` +#### \`location\` + +* Default: "user" unless \`--global\` is passed, which will also set this value + to "global" +* Type: "global", "user", or "project" + +When passed to \`npm config\` this refers to which config file to use. + +When set to "global" mode, packages are installed into the \`prefix\` folder +instead of the current working directory. See +[folders](/configuring-npm/folders) for more on the differences in behavior. + +* packages are installed into the \`{prefix}/lib/node_modules\` folder, instead + of the current working directory. +* bin files are linked to \`{prefix}/bin\` +* man pages are linked to \`{prefix}/share/man\` +---------------------------------------- +USAGE: -L|--location +---------------------------------------- +INVALID: Must be one of: "global", "user", "project" +---------------------------------------- +DEFAULT: "user" +---------------------------------------- +TYPES: global,user,project +---------------------------------------- +DERIVED: global,location,localPrefix +` + +exports[`test/definitions/snapshots.js TAP definitions lockfile-version > must match snapshot 1`] = ` +#### \`lockfile-version\` + +* Default: Version 3 if no lockfile, auto-converting v1 lockfiles to v3, + otherwise maintain current lockfile version. +* Type: null, 1, 2, or 3 + +Set the lockfile format version to be used in package-lock.json and +npm-shrinkwrap-json files. Possible options are: + +1: The lockfile version used by npm versions 5 and 6. Lacks some data that +is used during the install, resulting in slower and possibly less +deterministic installs. Prevents lockfile churn when interoperating with +older npm versions. + +2: The default lockfile version used by npm version 7 and 8. Includes both +the version 1 lockfile data and version 3 lockfile data, for maximum +determinism and interoperability, at the expense of more bytes on disk. + +3: Only the new lockfile information introduced in npm version 7. Smaller on +disk than lockfile version 2, but not interoperable with older npm versions. +Ideal if all users are on npm version 7 and higher. +---------------------------------------- +USAGE: --lockfile-version +---------------------------------------- +INVALID: Must be one of: null, 1, 2, 3 +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol([1,2,3]) +---------------------------------------- +DERIVED: lockfileVersion +` + +exports[`test/definitions/snapshots.js TAP definitions loglevel > must match snapshot 1`] = ` +#### \`loglevel\` + +* Default: "notice" +* Type: "silent", "error", "warn", "notice", "http", "info", "verbose", or + "silly" + +What level of logs to report. All logs are written to a debug log, with the +path to that file printed if the execution of a command fails. + +Any logs of a higher level than the setting are shown. The default is +"notice". + +See also the \`foreground-scripts\` config. +---------------------------------------- +USAGE: --loglevel +---------------------------------------- +INVALID: Must be one of: "silent", "error", "warn", "notice", "http", "info", "verbose", "silly" +---------------------------------------- +DEFAULT: "notice" +---------------------------------------- +TYPES: silent,error,warn,notice,http,info,verbose,silly +---------------------------------------- +DERIVED: silent +` + +exports[`test/definitions/snapshots.js TAP definitions logs-dir > must match snapshot 1`] = ` +#### \`logs-dir\` + +* Default: A directory named \`_logs\` inside the cache +* Type: null or Path + +The location of npm's log directory. See [\`npm logging\`](/using-npm/logging) +for more information. +---------------------------------------- +USAGE: --logs-dir +---------------------------------------- +INVALID: Must be a valid filesystem path +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(Path) +---------------------------------------- +DERIVED: cache,npxCache,logsDir +` + +exports[`test/definitions/snapshots.js TAP definitions logs-max > must match snapshot 1`] = ` +#### \`logs-max\` + +* Default: 10 +* Type: Number + +The maximum number of log files to store. + +If set to 0, no log files will be written for the current run. +---------------------------------------- +USAGE: --logs-max +---------------------------------------- +INVALID: Must be a numeric value +---------------------------------------- +DEFAULT: 10 +---------------------------------------- +TYPES: Symbol(Number) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions long > must match snapshot 1`] = ` +#### \`long\` + +* Default: false +* Type: Boolean + +Show extended information in \`ls\`, \`search\`, and \`help-search\`. +---------------------------------------- +USAGE: -l|--no-long|--long +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions maxsockets > must match snapshot 1`] = ` +#### \`maxsockets\` + +* Default: 15 +* Type: Number + +The maximum number of connections to use per origin (protocol/host/port +combination). +---------------------------------------- +USAGE: --maxsockets +---------------------------------------- +INVALID: Must be a numeric value +---------------------------------------- +DEFAULT: 15 +---------------------------------------- +TYPES: Symbol(Number) +---------------------------------------- +DERIVED: maxSockets +` + +exports[`test/definitions/snapshots.js TAP definitions message > must match snapshot 1`] = ` +#### \`message\` + +* Default: "%s" +* Type: String + +Commit message which is used by \`npm version\` when creating version commit. + +Any "%s" in the message will be replaced with the version number. +---------------------------------------- +USAGE: -m|--message +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "%s" +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: message +` + +exports[`test/definitions/snapshots.js TAP definitions node-options > must match snapshot 1`] = ` +#### \`node-options\` + +* Default: null +* Type: null or String + +Options to pass through to Node.js via the \`NODE_OPTIONS\` environment +variable. This does not impact how npm itself is executed but it does impact +how lifecycle scripts are called. +---------------------------------------- +USAGE: --node-options +---------------------------------------- +INVALID: Must be one of: null, a string +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(String) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions noproxy > must match snapshot 1`] = ` +#### \`noproxy\` + +* Default: The value of the NO_PROXY environment variable +* Type: String or Comma-delimited string (can be set multiple times) + +Domain extensions that should bypass any proxies. + +Also accepts a comma-delimited string. +---------------------------------------- +USAGE: --noproxy [--noproxy ...] +---------------------------------------- +INVALID: Must be one or more of: a string, a comma-delimited string +---------------------------------------- +DEFAULT: "" +---------------------------------------- +TYPES: Symbol(String),Symbol(CSV),Symbol(Array) +---------------------------------------- +DERIVED: noProxy +` + +exports[`test/definitions/snapshots.js TAP definitions offline > must match snapshot 1`] = ` +#### \`offline\` + +* Default: false +* Type: Boolean + +Force offline mode: no network requests will be done during install. To +allow the CLI to fill in missing cache data, see \`--prefer-offline\`. +---------------------------------------- +USAGE: --no-offline|--offline +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: offline +` + +exports[`test/definitions/snapshots.js TAP definitions omit > must match snapshot 1`] = ` +#### \`omit\` + +* Default: 'dev' if the \`NODE_ENV\` environment variable is set to + 'production', otherwise empty. +* Type: "prod", "dev", "optional", or "peer" (can be set multiple times) + +Dependency types to omit from the installation tree on disk. + +Note that these dependencies _are_ still resolved and added to the +\`package-lock.json\` or \`npm-shrinkwrap.json\` file. They are just not +physically installed on disk. + +If a package type appears in both the \`--include\` and \`--omit\` lists, then +it will be included. + +If the resulting omit list includes \`'dev'\`, then the \`NODE_ENV\` environment +variable will be set to \`'production'\` for all lifecycle scripts. +---------------------------------------- +USAGE: --omit [--omit ...] +---------------------------------------- +INVALID: Must be one or more of: "prod", "dev", "optional", "peer" +---------------------------------------- +DEFAULT: [] +---------------------------------------- +TYPES: Symbol(Array),prod,dev,optional,peer +---------------------------------------- +DERIVED: omit,include +` + +exports[`test/definitions/snapshots.js TAP definitions omit-lockfile-registry-resolved > must match snapshot 1`] = ` +#### \`omit-lockfile-registry-resolved\` + +* Default: false +* Type: Boolean + +This option causes npm to create lock files without a \`resolved\` key for +registry dependencies. Subsequent installs will need to resolve tarball +endpoints with the configured registry, likely resulting in a longer install +time. +---------------------------------------- +USAGE: --no-omit-lockfile-registry-resolved|--omit-lockfile-registry-resolved +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: omitLockfileRegistryResolved +` + +exports[`test/definitions/snapshots.js TAP definitions only > must match snapshot 1`] = ` +#### \`only\` + +* Default: null +* Type: null, "prod", or "production" +* DEPRECATED: Use \`--omit=dev\` to omit dev dependencies from the install. + +When set to \`prod\` or \`production\`, this is an alias for \`--omit=dev\`. +---------------------------------------- +USAGE: --only +---------------------------------------- +INVALID: Must be one of: null, "prod", "production" +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,prod,production +---------------------------------------- +DERIVED: omit,include +` + +exports[`test/definitions/snapshots.js TAP definitions optional > must match snapshot 1`] = ` +#### \`optional\` + +* Default: null +* Type: null or Boolean +* DEPRECATED: Use \`--omit=optional\` to exclude optional dependencies, or + \`--include=optional\` to include them. + +Default value does install optional deps unless otherwise omitted. + +Alias for --include=optional or --omit=optional +---------------------------------------- +USAGE: --no-optional|--optional +---------------------------------------- +INVALID: Must be one of: null, a boolean value (true or false) +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(Boolean) +---------------------------------------- +DERIVED: omit,include +` + +exports[`test/definitions/snapshots.js TAP definitions otp > must match snapshot 1`] = ` +#### \`otp\` + +* Default: null +* Type: null or String + +This is a one-time password from a two-factor authenticator. It's needed +when publishing or changing package permissions with \`npm access\`. + +If not set, and a registry response fails with a challenge for a one-time +password, npm will prompt on the command line for one. +---------------------------------------- +USAGE: --otp +---------------------------------------- +INVALID: Must be one of: null, a string +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(String) +---------------------------------------- +DERIVED: otp +` + +exports[`test/definitions/snapshots.js TAP definitions pack-destination > must match snapshot 1`] = ` +#### \`pack-destination\` + +* Default: "." +* Type: String + +Directory in which \`npm pack\` will save tarballs. +---------------------------------------- +USAGE: --pack-destination +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "." +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: packDestination +` + +exports[`test/definitions/snapshots.js TAP definitions package > must match snapshot 1`] = ` +#### \`package\` + +* Default: [] +* Type: Spec (can be set multiple times) + +The package or packages to install for [\`npm exec\`](/commands/npm-exec) +---------------------------------------- +USAGE: --package [--package ...] +---------------------------------------- +INVALID: Must be one or more an npm package spec +---------------------------------------- +DEFAULT: [] +---------------------------------------- +TYPES: Symbol(Spec),Symbol(Array) +---------------------------------------- +DERIVED: package +` + +exports[`test/definitions/snapshots.js TAP definitions package-lock > must match snapshot 1`] = ` +#### \`package-lock\` + +* Default: true +* Type: Boolean + +If set to false, then ignore \`package-lock.json\` files when installing. This +will also prevent _writing_ \`package-lock.json\` if \`save\` is true. + +This configuration does not affect \`npm ci\`. +---------------------------------------- +USAGE: --no-package-lock|--package-lock +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: true +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: packageLock,packageLockOnly +` + +exports[`test/definitions/snapshots.js TAP definitions package-lock-only > must match snapshot 1`] = ` +#### \`package-lock-only\` + +* Default: false +* Type: Boolean + +If set to true, the current operation will only use the \`package-lock.json\`, +ignoring \`node_modules\`. + +For \`update\` this means only the \`package-lock.json\` will be updated, +instead of checking \`node_modules\` and downloading dependencies. + +For \`list\` this means the output will be based on the tree described by the +\`package-lock.json\`, rather than the contents of \`node_modules\`. +---------------------------------------- +USAGE: --no-package-lock-only|--package-lock-only +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: packageLock,packageLockOnly +` + +exports[`test/definitions/snapshots.js TAP definitions parseable > must match snapshot 1`] = ` +#### \`parseable\` + +* Default: false +* Type: Boolean + +Output parseable results from commands that write to standard output. For +\`npm search\`, this will be tab-separated table format. +---------------------------------------- +USAGE: -p|--no-parseable|--parseable +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: parseable +` + +exports[`test/definitions/snapshots.js TAP definitions prefer-offline > must match snapshot 1`] = ` +#### \`prefer-offline\` + +* Default: false +* Type: Boolean + +If true, staleness checks for cached data will be bypassed, but missing data +will be requested from the server. To force full offline mode, use +\`--offline\`. +---------------------------------------- +USAGE: --no-prefer-offline|--prefer-offline +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: preferOffline +` + +exports[`test/definitions/snapshots.js TAP definitions prefer-online > must match snapshot 1`] = ` +#### \`prefer-online\` + +* Default: false +* Type: Boolean + +If true, staleness checks for cached data will be forced, making the CLI +look for updates immediately even for fresh package data. +---------------------------------------- +USAGE: --no-prefer-online|--prefer-online +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: preferOnline +` + +exports[`test/definitions/snapshots.js TAP definitions prefix > must match snapshot 1`] = ` +#### \`prefix\` + +* Default: In global mode, the folder where the node executable is installed. + Otherwise, the nearest parent folder containing either a package.json file + or a node_modules folder. +* Type: null or Path + +The location to install global items. If set on the command line, then it +forces non-global commands to run in the specified folder. +---------------------------------------- +USAGE: -C|--prefix +---------------------------------------- +INVALID: Must be a valid filesystem path +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(Path) +---------------------------------------- +DERIVED: prefix,globalconfig,globalPrefix,localPrefix +` + +exports[`test/definitions/snapshots.js TAP definitions preid > must match snapshot 1`] = ` +#### \`preid\` + +* Default: "" +* Type: String + +The "prerelease identifier" to use as a prefix for the "prerelease" part of +a semver. Like the \`rc\` in \`1.2.0-rc.8\`. +---------------------------------------- +USAGE: --preid +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "" +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: preid +` + +exports[`test/definitions/snapshots.js TAP definitions production > must match snapshot 1`] = ` +#### \`production\` + +* Default: null +* Type: null or Boolean +* DEPRECATED: Use \`--omit=dev\` instead. + +Alias for \`--omit=dev\` +---------------------------------------- +USAGE: --no-production|--production +---------------------------------------- +INVALID: Must be one of: null, a boolean value (true or false) +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(Boolean) +---------------------------------------- +DERIVED: omit,include +` + +exports[`test/definitions/snapshots.js TAP definitions progress > must match snapshot 1`] = ` +#### \`progress\` + +* Default: \`true\` unless running in a known CI system +* Type: Boolean + +When set to \`true\`, npm will display a progress bar during time intensive +operations, if \`process.stderr\` is a TTY. + +Set to \`false\` to suppress the progress bar. +---------------------------------------- +USAGE: --no-progress|--progress +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: true +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: progress +` + +exports[`test/definitions/snapshots.js TAP definitions proxy > must match snapshot 1`] = ` +#### \`proxy\` + +* Default: null +* Type: null, false, or URL + +A proxy to use for outgoing http requests. If the \`HTTP_PROXY\` or +\`http_proxy\` environment variables are set, proxy settings will be honored +by the underlying \`request\` library. +---------------------------------------- +USAGE: --no-proxy|--proxy +---------------------------------------- +INVALID: Must be a full url with "http://" +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,false,Symbol(URL) +---------------------------------------- +DERIVED: proxy +` + +exports[`test/definitions/snapshots.js TAP definitions read-only > must match snapshot 1`] = ` +#### \`read-only\` + +* Default: false +* Type: Boolean + +This is used to mark a token as unable to publish when configuring limited +access tokens with the \`npm token create\` command. +---------------------------------------- +USAGE: --no-read-only|--read-only +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: readOnly +` + +exports[`test/definitions/snapshots.js TAP definitions rebuild-bundle > must match snapshot 1`] = ` +#### \`rebuild-bundle\` + +* Default: true +* Type: Boolean + +Rebuild bundled dependencies after installation. +---------------------------------------- +USAGE: --no-rebuild-bundle|--rebuild-bundle +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: true +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: rebuildBundle +` + +exports[`test/definitions/snapshots.js TAP definitions registry > must match snapshot 1`] = ` +#### \`registry\` + +* Default: "https://registry.npmjs.org/" +* Type: URL + +The base URL of the npm registry. +---------------------------------------- +USAGE: --registry +---------------------------------------- +INVALID: Must be a full url with "http://" +---------------------------------------- +DEFAULT: "https://registry.npmjs.org/" +---------------------------------------- +TYPES: Symbol(URL) +---------------------------------------- +DERIVED: registry +` + +exports[`test/definitions/snapshots.js TAP definitions replace-registry-host > must match snapshot 1`] = ` +#### \`replace-registry-host\` + +* Default: "npmjs" +* Type: "npmjs", "never", "always", or String + +Defines behavior for replacing the registry host in a lockfile with the +configured registry. + +The default behavior is to replace package dist URLs from the default +registry (https://registry.npmjs.org) to the configured registry. If set to +"never", then use the registry value. If set to "always", then replace the +registry host with the configured host every time. + +You may also specify a bare hostname (e.g., "registry.npmjs.org"). +---------------------------------------- +USAGE: --replace-registry-host +---------------------------------------- +INVALID: Must be one of: "npmjs", "never", "always", a string +---------------------------------------- +DEFAULT: "npmjs" +---------------------------------------- +TYPES: npmjs,never,always,Symbol(String) +---------------------------------------- +DERIVED: replaceRegistryHost +` + +exports[`test/definitions/snapshots.js TAP definitions save > must match snapshot 1`] = ` +#### \`save\` + +* Default: \`true\` unless when using \`npm update\` where it defaults to \`false\` +* Type: Boolean + +Save installed packages to a \`package.json\` file as dependencies. + +When used with the \`npm rm\` command, removes the dependency from +\`package.json\`. + +Will also prevent writing to \`package-lock.json\` if set to \`false\`. +---------------------------------------- +USAGE: -S|--no-save|--save +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: true +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: save +` + +exports[`test/definitions/snapshots.js TAP definitions save-bundle > must match snapshot 1`] = ` +#### \`save-bundle\` + +* Default: false +* Type: Boolean + +If a package would be saved at install time by the use of \`--save\`, +\`--save-dev\`, or \`--save-optional\`, then also put it in the +\`bundleDependencies\` list. + +Ignored if \`--save-peer\` is set, since peerDependencies cannot be bundled. +---------------------------------------- +USAGE: -B|--no-save-bundle|--save-bundle +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: saveBundle +` + +exports[`test/definitions/snapshots.js TAP definitions save-dev > must match snapshot 1`] = ` +#### \`save-dev\` + +* Default: false +* Type: Boolean + +Save installed packages to a package.json file as \`devDependencies\`. +---------------------------------------- +USAGE: -D|--no-save-dev|--save-dev +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: saveType +` + +exports[`test/definitions/snapshots.js TAP definitions save-exact > must match snapshot 1`] = ` +#### \`save-exact\` + +* Default: false +* Type: Boolean + +Dependencies saved to package.json will be configured with an exact version +rather than using npm's default semver range operator. +---------------------------------------- +USAGE: -E|--no-save-exact|--save-exact +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: savePrefix +` + +exports[`test/definitions/snapshots.js TAP definitions save-optional > must match snapshot 1`] = ` +#### \`save-optional\` + +* Default: false +* Type: Boolean + +Save installed packages to a package.json file as \`optionalDependencies\`. +---------------------------------------- +USAGE: -O|--no-save-optional|--save-optional +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: saveType +` + +exports[`test/definitions/snapshots.js TAP definitions save-peer > must match snapshot 1`] = ` +#### \`save-peer\` + +* Default: false +* Type: Boolean + +Save installed packages to a package.json file as \`peerDependencies\` +---------------------------------------- +USAGE: --no-save-peer|--save-peer +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: saveBundle,saveType +` + +exports[`test/definitions/snapshots.js TAP definitions save-prefix > must match snapshot 1`] = ` +#### \`save-prefix\` + +* Default: "^" +* Type: String + +Configure how versions of packages installed to a package.json file via +\`--save\` or \`--save-dev\` get prefixed. + +For example if a package has version \`1.2.3\`, by default its version is set +to \`^1.2.3\` which allows minor upgrades for that package, but after \`npm +config set save-prefix='~'\` it would be set to \`~1.2.3\` which only allows +patch upgrades. +---------------------------------------- +USAGE: --save-prefix +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "^" +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: savePrefix +` + +exports[`test/definitions/snapshots.js TAP definitions save-prod > must match snapshot 1`] = ` +#### \`save-prod\` + +* Default: false +* Type: Boolean + +Save installed packages into \`dependencies\` specifically. This is useful if +a package already exists in \`devDependencies\` or \`optionalDependencies\`, but +you want to move it to be a non-optional production dependency. + +This is the default behavior if \`--save\` is true, and neither \`--save-dev\` +or \`--save-optional\` are true. +---------------------------------------- +USAGE: -P|--no-save-prod|--save-prod +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: saveType +` + +exports[`test/definitions/snapshots.js TAP definitions scope > must match snapshot 1`] = ` +#### \`scope\` + +* Default: the scope of the current project, if any, or "" +* Type: Scope + +Associate an operation with a scope for a scoped registry. + +Useful when logging in to or out of a private registry: + +\`\`\` +# log in, linking the scope to the custom registry +npm login --scope=@mycorp --registry=https://registry.mycorp.com + +# log out, removing the link and the auth token +npm logout --scope=@mycorp +\`\`\` + +This will cause \`@mycorp\` to be mapped to the registry for future +installation of packages specified according to the pattern +\`@mycorp/package\`. + +This will also cause \`npm init\` to create a scoped package. + +\`\`\` +# accept all defaults, and create a package named "@foo/whatever", +# instead of just named "whatever" +npm init --scope=@foo --yes +\`\`\` +---------------------------------------- +USAGE: --scope <@scope> +---------------------------------------- +INVALID: Must be an npm scope with or without the leading @ +---------------------------------------- +DEFAULT: "" +---------------------------------------- +TYPES: Symbol(Scope) +---------------------------------------- +DERIVED: scope,projectScope +` + +exports[`test/definitions/snapshots.js TAP definitions script-shell > must match snapshot 1`] = ` +#### \`script-shell\` + +* Default: '/bin/sh' on POSIX systems, 'cmd.exe' on Windows +* Type: null or String + +The shell to use for scripts run with the \`npm exec\`, \`npm run\` and \`npm +init \` commands. +---------------------------------------- +USAGE: --script-shell +---------------------------------------- +INVALID: Must be one of: null, a string +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(String) +---------------------------------------- +DERIVED: scriptShell +` + +exports[`test/definitions/snapshots.js TAP definitions searchexclude > must match snapshot 1`] = ` +#### \`searchexclude\` + +* Default: "" +* Type: String + +Space-separated options that limit the results from search. +---------------------------------------- +USAGE: --searchexclude +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "" +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: search.exclude +` + +exports[`test/definitions/snapshots.js TAP definitions searchlimit > must match snapshot 1`] = ` +#### \`searchlimit\` + +* Default: 20 +* Type: Number + +Number of items to limit search results to. Will not apply at all to legacy +searches. +---------------------------------------- +USAGE: --searchlimit +---------------------------------------- +INVALID: Must be a numeric value +---------------------------------------- +DEFAULT: 20 +---------------------------------------- +TYPES: Symbol(Number) +---------------------------------------- +DERIVED: search.limit +` + +exports[`test/definitions/snapshots.js TAP definitions searchopts > must match snapshot 1`] = ` +#### \`searchopts\` + +* Default: "" +* Type: Querystring + +Space-separated options that are always passed to search. +---------------------------------------- +USAGE: --searchopts +---------------------------------------- +INVALID: Must be a space-delimited querystring +---------------------------------------- +DEFAULT: "" +---------------------------------------- +TYPES: Symbol(Querystring) +---------------------------------------- +DERIVED: search.opts +` + +exports[`test/definitions/snapshots.js TAP definitions searchstaleness > must match snapshot 1`] = ` +#### \`searchstaleness\` + +* Default: 900 +* Type: Number + +The age of the cache, in seconds, before another registry request is made if +using legacy search endpoint. +---------------------------------------- +USAGE: --searchstaleness +---------------------------------------- +INVALID: Must be a numeric value +---------------------------------------- +DEFAULT: 900 +---------------------------------------- +TYPES: Symbol(Number) +---------------------------------------- +DERIVED: search.staleness +` + +exports[`test/definitions/snapshots.js TAP definitions shell > must match snapshot 1`] = ` +#### \`shell\` + +* Default: SHELL environment variable, or "bash" on Posix, or "cmd.exe" on + Windows +* Type: String + +The shell to run for the \`npm explore\` command. +---------------------------------------- +USAGE: --shell +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "/bin/zsh" +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: shell +` + +exports[`test/definitions/snapshots.js TAP definitions shrinkwrap > must match snapshot 1`] = ` +#### \`shrinkwrap\` + +* Default: true +* Type: Boolean +* DEPRECATED: Use the --package-lock setting instead. + +Alias for --package-lock +---------------------------------------- +USAGE: --no-shrinkwrap|--shrinkwrap +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: true +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: packageLock +` + +exports[`test/definitions/snapshots.js TAP definitions sign-git-commit > must match snapshot 1`] = ` +#### \`sign-git-commit\` + +* Default: false +* Type: Boolean + +If set to true, then the \`npm version\` command will commit the new package +version using \`-S\` to add a signature. + +Note that git requires you to have set up GPG keys in your git configs for +this to work properly. +---------------------------------------- +USAGE: --no-sign-git-commit|--sign-git-commit +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: signGitCommit +` + +exports[`test/definitions/snapshots.js TAP definitions sign-git-tag > must match snapshot 1`] = ` +#### \`sign-git-tag\` + +* Default: false +* Type: Boolean + +If set to true, then the \`npm version\` command will tag the version using +\`-s\` to add a signature. + +Note that git requires you to have set up GPG keys in your git configs for +this to work properly. +---------------------------------------- +USAGE: --no-sign-git-tag|--sign-git-tag +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: signGitTag +` + +exports[`test/definitions/snapshots.js TAP definitions strict-peer-deps > must match snapshot 1`] = ` +#### \`strict-peer-deps\` + +* Default: false +* Type: Boolean + +If set to \`true\`, and \`--legacy-peer-deps\` is not set, then _any_ +conflicting \`peerDependencies\` will be treated as an install failure, even +if npm could reasonably guess the appropriate resolution based on non-peer +dependency relationships. + +By default, conflicting \`peerDependencies\` deep in the dependency graph will +be resolved using the nearest non-peer dependency specification, even if +doing so will result in some packages receiving a peer dependency outside +the range set in their package's \`peerDependencies\` object. + +When such and override is performed, a warning is printed, explaining the +conflict and the packages involved. If \`--strict-peer-deps\` is set, then +this warning is treated as a failure. +---------------------------------------- +USAGE: --no-strict-peer-deps|--strict-peer-deps +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: strictPeerDeps +` + +exports[`test/definitions/snapshots.js TAP definitions strict-ssl > must match snapshot 1`] = ` +#### \`strict-ssl\` + +* Default: true +* Type: Boolean + +Whether or not to do SSL key validation when making requests to the registry +via https. + +See also the \`ca\` config. +---------------------------------------- +USAGE: --no-strict-ssl|--strict-ssl +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: true +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: strictSSL +` + +exports[`test/definitions/snapshots.js TAP definitions tag > must match snapshot 1`] = ` +#### \`tag\` + +* Default: "latest" +* Type: String + +If you ask npm to install a package and don't tell it a specific version, +then it will install the specified tag. + +Also the tag that is added to the package@version specified by the \`npm tag\` +command, if no explicit tag is given. + +When used by the \`npm diff\` command, this is the tag used to fetch the +tarball that will be compared with the local files by default. +---------------------------------------- +USAGE: --tag +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "latest" +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: defaultTag +` + +exports[`test/definitions/snapshots.js TAP definitions tag-version-prefix > must match snapshot 1`] = ` +#### \`tag-version-prefix\` + +* Default: "v" +* Type: String + +If set, alters the prefix used when tagging a new version when performing a +version increment using \`npm version\`. To remove the prefix altogether, set +it to the empty string: \`""\`. + +Because other tools may rely on the convention that npm version tags look +like \`v1.0.0\`, _only use this property if it is absolutely necessary_. In +particular, use care when overriding this setting for public packages. +---------------------------------------- +USAGE: --tag-version-prefix +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "v" +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: tagVersionPrefix +` + +exports[`test/definitions/snapshots.js TAP definitions timing > must match snapshot 1`] = ` +#### \`timing\` + +* Default: false +* Type: Boolean + +If true, writes timing information to a process specific json file in the +cache or \`logs-dir\`. The file name ends with \`-timing.json\`. + +You can quickly view it with this [json](https://npm.im/json) command line: +\`cat ~/.npm/_logs/*-timing.json | npm exec -- json -g\`. + +Timing information will also be reported in the terminal. To suppress this +while still writing the timing file, use \`--silent\`. +---------------------------------------- +USAGE: --no-timing|--timing +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions tmp > must match snapshot 1`] = ` +#### \`tmp\` + +* Default: The value returned by the Node.js \`os.tmpdir()\` method + +* Type: Path +* DEPRECATED: This setting is no longer used. npm stores temporary files in a + special location in the cache, and they are managed by + [\`cacache\`](http://npm.im/cacache). + +Historically, the location where temporary files were stored. No longer +relevant. +---------------------------------------- +USAGE: --tmp +---------------------------------------- +INVALID: Must be a valid filesystem path +---------------------------------------- +DEFAULT: "/var/folders/yt/5msscr_d2g57cy9jdm_r1ndh0000gn/T" +---------------------------------------- +TYPES: Symbol(Path) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions umask > must match snapshot 1`] = ` +#### \`umask\` + +* Default: 0 +* Type: Octal numeric string in range 0000..0777 (0..511) + +The "umask" value to use when setting the file creation mode on files and +folders. + +Folders and executables are given a mode which is \`0o777\` masked against +this value. Other files are given a mode which is \`0o666\` masked against +this value. + +Note that the underlying system will _also_ apply its own umask value to +files and folders that are created, and npm does not circumvent this, but +rather adds the \`--umask\` config to it. + +Thus, the effective default umask value on most POSIX systems is 0o22, +meaning that folders and executables are created with a mode of 0o755 and +other files are created with a mode of 0o644. +---------------------------------------- +USAGE: --umask +---------------------------------------- +INVALID: Must be an octal number in range 0o000..0o777 (0..511) +---------------------------------------- +DEFAULT: 0 +---------------------------------------- +TYPES: Symbol(Umask) +---------------------------------------- +DERIVED: umask +` + +exports[`test/definitions/snapshots.js TAP definitions unicode > must match snapshot 1`] = ` +#### \`unicode\` + +* Default: false on windows, true on mac/unix systems with a unicode locale, + as defined by the \`LC_ALL\`, \`LC_CTYPE\`, or \`LANG\` environment variables. +* Type: Boolean + +When set to true, npm uses unicode characters in the tree output. When +false, it uses ascii characters instead of unicode glyphs. +---------------------------------------- +USAGE: --no-unicode|--unicode +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: true +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: unicode +` + +exports[`test/definitions/snapshots.js TAP definitions update-notifier > must match snapshot 1`] = ` +#### \`update-notifier\` + +* Default: true +* Type: Boolean + +Set to false to suppress the update notification when using an older version +of npm than the latest. +---------------------------------------- +USAGE: --no-update-notifier|--update-notifier +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: true +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions usage > must match snapshot 1`] = ` +#### \`usage\` + +* Default: false +* Type: Boolean + +Show short usage output about the command specified. +---------------------------------------- +USAGE: -?|-H|-h|--no-usage|--usage +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions user-agent > must match snapshot 1`] = ` +#### \`user-agent\` + +* Default: "npm/{npm-version} node/{node-version} {platform} {arch} + workspaces/{workspaces} {ci}" +* Type: String + +Sets the User-Agent request header. The following fields are replaced with +their actual counterparts: + +* \`{npm-version}\` - The npm version in use +* \`{node-version}\` - The Node.js version in use +* \`{platform}\` - The value of \`process.platform\` +* \`{arch}\` - The value of \`process.arch\` +* \`{workspaces}\` - Set to \`true\` if the \`workspaces\` or \`workspace\` options + are set. +* \`{ci}\` - The value of the \`ci-name\` config, if set, prefixed with \`ci/\`, or + an empty string if \`ci-name\` is empty. +---------------------------------------- +USAGE: --user-agent +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "npm/{npm-version} node/{node-version} {platform} {arch} workspaces/{workspaces} {ci}" +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: userAgent +` + +exports[`test/definitions/snapshots.js TAP definitions userconfig > must match snapshot 1`] = ` +#### \`userconfig\` + +* Default: "~/.npmrc" +* Type: Path + +The location of user-level configuration settings. + +This may be overridden by the \`npm_config_userconfig\` environment variable +or the \`--userconfig\` command line option, but may _not_ be overridden by +settings in the \`globalconfig\` file. +---------------------------------------- +USAGE: --userconfig +---------------------------------------- +INVALID: Must be a valid filesystem path +---------------------------------------- +DEFAULT: "~/.npmrc" +---------------------------------------- +TYPES: Symbol(Path) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions version > must match snapshot 1`] = ` +#### \`version\` + +* Default: false +* Type: Boolean + +If true, output the npm version and exit successfully. + +Only relevant when specified explicitly on the command line. +---------------------------------------- +USAGE: -v|--no-version|--version +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions versions > must match snapshot 1`] = ` +#### \`versions\` + +* Default: false +* Type: Boolean + +If true, output the npm version as well as node's \`process.versions\` map and +the version in the current working directory's \`package.json\` file if one +exists, and exit successfully. + +Only relevant when specified explicitly on the command line. +---------------------------------------- +USAGE: --no-versions|--versions +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: false +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions viewer > must match snapshot 1`] = ` +#### \`viewer\` + +* Default: "man" on Posix, "browser" on Windows +* Type: String + +The program to use to view help content. + +Set to \`"browser"\` to view html help content in the default web browser. +---------------------------------------- +USAGE: --viewer +---------------------------------------- +INVALID: Must be a string +---------------------------------------- +DEFAULT: "man" +---------------------------------------- +TYPES: Symbol(String) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions which > must match snapshot 1`] = ` +#### \`which\` + +* Default: null +* Type: null or Positive integer + +If there are multiple funding sources, which 1-indexed source URL to open. +---------------------------------------- +USAGE: --which <1|2|3|n> +---------------------------------------- +INVALID: Must be one of: null, an integer greater than or equal to 1 +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(PositiveInteger) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP definitions workspace > must match snapshot 1`] = ` +#### \`workspace\` + +* Default: [] +* Type: String or Path (can be set multiple times) + +Enable running a command in the context of the configured workspaces of the +current project while filtering by running only the workspaces defined by +this configuration option. + +Valid values for the \`workspace\` config are either: + +* Workspace names +* Path to a workspace directory +* Path to a parent workspace directory (will result in selecting all + workspaces within that folder) + +When set for the \`npm init\` command, this may be set to the folder of a +workspace which does not yet exist, to create the folder and set it up as a +brand new workspace within the project. + +This value is not exported to the environment for child processes. +---------------------------------------- +USAGE: -w|--workspace [-w|--workspace ...] +---------------------------------------- +INVALID: Must be one or more a valid filesystem path +---------------------------------------- +DEFAULT: [] +---------------------------------------- +TYPES: Symbol(String),Symbol(Path),Symbol(Array) +---------------------------------------- +DERIVED: userAgent +` + +exports[`test/definitions/snapshots.js TAP definitions workspaces > must match snapshot 1`] = ` +#### \`workspaces\` + +* Default: null +* Type: null or Boolean + +Set to true to run the command in the context of **all** configured +workspaces. + +Explicitly setting this to false will cause commands like \`install\` to +ignore workspaces altogether. When not set explicitly: + +- Commands that operate on the \`node_modules\` tree (install, update, etc.) +will link workspaces into the \`node_modules\` folder. - Commands that do +other things (test, exec, publish, etc.) will operate on the root project, +_unless_ one or more workspaces are specified in the \`workspace\` config. + +This value is not exported to the environment for child processes. +---------------------------------------- +USAGE: -ws|--no-workspaces|--workspaces +---------------------------------------- +INVALID: Must be one of: null, a boolean value (true or false) +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(Boolean) +---------------------------------------- +DERIVED: localPrefix,userAgent,workspacesEnabled +` + +exports[`test/definitions/snapshots.js TAP definitions workspaces-update > must match snapshot 1`] = ` +#### \`workspaces-update\` + +* Default: true +* Type: Boolean + +If set to true, the npm cli will run an update after operations that may +possibly change the workspaces installed to the \`node_modules\` folder. +---------------------------------------- +USAGE: --no-workspaces-update|--workspaces-update +---------------------------------------- +INVALID: Must be a boolean value (true or false) +---------------------------------------- +DEFAULT: true +---------------------------------------- +TYPES: Symbol(Boolean) +---------------------------------------- +DERIVED: workspacesUpdate +` + +exports[`test/definitions/snapshots.js TAP definitions yes > must match snapshot 1`] = ` +#### \`yes\` + +* Default: null +* Type: null or Boolean + +Automatically answer "yes" to any prompts that npm might print on the +command line. +---------------------------------------- +USAGE: -y|--no-yes|--yes +---------------------------------------- +INVALID: Must be one of: null, a boolean value (true or false) +---------------------------------------- +DEFAULT: null +---------------------------------------- +TYPES: null,Symbol(Boolean) +---------------------------------------- +DERIVED: +` + +exports[`test/definitions/snapshots.js TAP derived _auth > must match snapshot 1`] = ` +Array [ + "_auth", +] +` + +exports[`test/definitions/snapshots.js TAP derived access > must match snapshot 1`] = ` +Array [ + "access", +] +` + +exports[`test/definitions/snapshots.js TAP derived all > must match snapshot 1`] = ` +Array [ + "all", +] +` + +exports[`test/definitions/snapshots.js TAP derived allow-same-version > must match snapshot 1`] = ` +Array [ + "allow-same-version", +] +` + +exports[`test/definitions/snapshots.js TAP derived audit > must match snapshot 1`] = ` +Array [ + "audit", +] +` + +exports[`test/definitions/snapshots.js TAP derived audit-level > must match snapshot 1`] = ` +Array [ + "audit-level", +] +` + +exports[`test/definitions/snapshots.js TAP derived auth-type > must match snapshot 1`] = ` +Array [ + "auth-type", +] +` + +exports[`test/definitions/snapshots.js TAP derived before > must match snapshot 1`] = ` +Array [ + "before", +] +` + +exports[`test/definitions/snapshots.js TAP derived bin-links > must match snapshot 1`] = ` +Array [ + "bin-links", +] +` + +exports[`test/definitions/snapshots.js TAP derived browser > must match snapshot 1`] = ` +Array [ + "browser", +] +` + +exports[`test/definitions/snapshots.js TAP derived ca > must match snapshot 1`] = ` +Array [ + "ca", + "cafile", +] +` + +exports[`test/definitions/snapshots.js TAP derived cache > must match snapshot 1`] = ` +Array [ + "cache", + "logs-dir", +] +` + +exports[`test/definitions/snapshots.js TAP derived call > must match snapshot 1`] = ` +Array [ + "call", +] +` + +exports[`test/definitions/snapshots.js TAP derived cert > must match snapshot 1`] = ` +Array [ + "cert", +] +` + +exports[`test/definitions/snapshots.js TAP derived ci-name > must match snapshot 1`] = ` +Array [ + "ci-name", +] +` + +exports[`test/definitions/snapshots.js TAP derived cidr > must match snapshot 1`] = ` +Array [ + "cidr", +] +` + +exports[`test/definitions/snapshots.js TAP derived color > must match snapshot 1`] = ` +Array [ + "color", +] +` + +exports[`test/definitions/snapshots.js TAP derived commit-hooks > must match snapshot 1`] = ` +Array [ + "commit-hooks", +] +` + +exports[`test/definitions/snapshots.js TAP derived default-tag > must match snapshot 1`] = ` +Array [ + "default-tag", +] +` + +exports[`test/definitions/snapshots.js TAP derived depth > must match snapshot 1`] = ` +Array [ + "depth", +] +` + +exports[`test/definitions/snapshots.js TAP derived diff > must match snapshot 1`] = ` +Array [ + "diff", +] +` + +exports[`test/definitions/snapshots.js TAP derived diff-dst-prefix > must match snapshot 1`] = ` +Array [ + "diff-dst-prefix", +] +` + +exports[`test/definitions/snapshots.js TAP derived diff-ignore-all-space > must match snapshot 1`] = ` +Array [ + "diff-ignore-all-space", +] +` + +exports[`test/definitions/snapshots.js TAP derived diff-name-only > must match snapshot 1`] = ` +Array [ + "diff-name-only", +] +` + +exports[`test/definitions/snapshots.js TAP derived diff-no-prefix > must match snapshot 1`] = ` +Array [ + "diff-no-prefix", +] +` + +exports[`test/definitions/snapshots.js TAP derived diff-src-prefix > must match snapshot 1`] = ` +Array [ + "diff-src-prefix", +] +` + +exports[`test/definitions/snapshots.js TAP derived diff-text > must match snapshot 1`] = ` +Array [ + "diff-text", +] +` + +exports[`test/definitions/snapshots.js TAP derived diff-unified > must match snapshot 1`] = ` +Array [ + "diff-unified", +] +` + +exports[`test/definitions/snapshots.js TAP derived dry-run > must match snapshot 1`] = ` +Array [ + "dry-run", +] +` + +exports[`test/definitions/snapshots.js TAP derived editor > must match snapshot 1`] = ` +Array [ + "editor", +] +` + +exports[`test/definitions/snapshots.js TAP derived engine-strict > must match snapshot 1`] = ` +Array [ + "engine-strict", +] +` + +exports[`test/definitions/snapshots.js TAP derived force > must match snapshot 1`] = ` +Array [ + "force", +] +` + +exports[`test/definitions/snapshots.js TAP derived foreground-scripts > must match snapshot 1`] = ` +Array [ + "foreground-scripts", +] +` + +exports[`test/definitions/snapshots.js TAP derived format-package-lock > must match snapshot 1`] = ` +Array [ + "format-package-lock", +] +` + +exports[`test/definitions/snapshots.js TAP derived fund > must match snapshot 1`] = ` +Array [ + "fund", +] +` + +exports[`test/definitions/snapshots.js TAP derived git > must match snapshot 1`] = ` +Array [ + "git", +] +` + +exports[`test/definitions/snapshots.js TAP derived git-tag-version > must match snapshot 1`] = ` +Array [ + "git-tag-version", +] +` + +exports[`test/definitions/snapshots.js TAP derived global > must match snapshot 1`] = ` +Array [ + "global", + "location", +] +` + +exports[`test/definitions/snapshots.js TAP derived global-prefix > must match snapshot 1`] = ` +Array [ + "global-prefix", + "prefix", + "globalconfig", +] +` + +exports[`test/definitions/snapshots.js TAP derived globalconfig > must match snapshot 1`] = ` +Array [ + "globalconfig", + "prefix", +] +` + +exports[`test/definitions/snapshots.js TAP derived heading > must match snapshot 1`] = ` +Array [ + "heading", +] +` + +exports[`test/definitions/snapshots.js TAP derived https-proxy > must match snapshot 1`] = ` +Array [ + "https-proxy", +] +` + +exports[`test/definitions/snapshots.js TAP derived if-present > must match snapshot 1`] = ` +Array [ + "if-present", +] +` + +exports[`test/definitions/snapshots.js TAP derived ignore-scripts > must match snapshot 1`] = ` +Array [ + "ignore-scripts", +] +` + +exports[`test/definitions/snapshots.js TAP derived include > must match snapshot 1`] = ` +Array [ + "include", + "dev", + "production", + "optional", + "also", + "only", + "omit", +] +` + +exports[`test/definitions/snapshots.js TAP derived include-staged > must match snapshot 1`] = ` +Array [ + "include-staged", +] +` + +exports[`test/definitions/snapshots.js TAP derived include-workspace-root > must match snapshot 1`] = ` +Array [ + "include-workspace-root", +] +` + +exports[`test/definitions/snapshots.js TAP derived install-links > must match snapshot 1`] = ` +Array [ + "install-links", +] +` + +exports[`test/definitions/snapshots.js TAP derived install-strategy > must match snapshot 1`] = ` +Array [ + "install-strategy", + "global-style", + "legacy-bundling", +] +` + +exports[`test/definitions/snapshots.js TAP derived json > must match snapshot 1`] = ` +Array [ + "json", +] +` + +exports[`test/definitions/snapshots.js TAP derived key > must match snapshot 1`] = ` +Array [ + "key", +] +` + +exports[`test/definitions/snapshots.js TAP derived legacy-peer-deps > must match snapshot 1`] = ` +Array [ + "legacy-peer-deps", +] +` + +exports[`test/definitions/snapshots.js TAP derived local-address > must match snapshot 1`] = ` +Array [ + "local-address", +] +` + +exports[`test/definitions/snapshots.js TAP derived local-prefix > must match snapshot 1`] = ` +Array [ + "local-prefix", + "prefix", + "workspaces", + "global", + "location", +] +` + +exports[`test/definitions/snapshots.js TAP derived location > must match snapshot 1`] = ` +Array [ + "location", + "global", +] +` + +exports[`test/definitions/snapshots.js TAP derived lockfile-version > must match snapshot 1`] = ` +Array [ + "lockfile-version", +] +` + +exports[`test/definitions/snapshots.js TAP derived log-color > must match snapshot 1`] = ` +Array [ + "log-color", + "color", +] +` + +exports[`test/definitions/snapshots.js TAP derived logs-dir > must match snapshot 1`] = ` +Array [ + "logs-dir", + "cache", +] +` + +exports[`test/definitions/snapshots.js TAP derived max-sockets > must match snapshot 1`] = ` +Array [ + "max-sockets", +] +` + +exports[`test/definitions/snapshots.js TAP derived message > must match snapshot 1`] = ` +Array [ + "message", +] +` + +exports[`test/definitions/snapshots.js TAP derived no-proxy > must match snapshot 1`] = ` +Array [ + "no-proxy", +] +` + +exports[`test/definitions/snapshots.js TAP derived node-bin > must match snapshot 1`] = ` +Array [ + "node-bin", +] +` + +exports[`test/definitions/snapshots.js TAP derived npm-bin > must match snapshot 1`] = ` +Array [ + "npm-bin", +] +` + +exports[`test/definitions/snapshots.js TAP derived npx-cache > must match snapshot 1`] = ` +Array [ + "npx-cache", + "cache", + "logs-dir", +] +` + +exports[`test/definitions/snapshots.js TAP derived offline > must match snapshot 1`] = ` +Array [ + "offline", +] +` + +exports[`test/definitions/snapshots.js TAP derived omit > must match snapshot 1`] = ` +Array [ + "omit", + "dev", + "production", + "optional", + "also", + "only", + "include", +] +` + +exports[`test/definitions/snapshots.js TAP derived omit-lockfile-registry-resolved > must match snapshot 1`] = ` +Array [ + "omit-lockfile-registry-resolved", +] +` + +exports[`test/definitions/snapshots.js TAP derived otp > must match snapshot 1`] = ` +Array [ + "otp", +] +` + +exports[`test/definitions/snapshots.js TAP derived pack-destination > must match snapshot 1`] = ` +Array [ + "pack-destination", +] +` + +exports[`test/definitions/snapshots.js TAP derived package > must match snapshot 1`] = ` +Array [ + "package", +] +` + +exports[`test/definitions/snapshots.js TAP derived package-lock > must match snapshot 1`] = ` +Array [ + "package-lock", + "package-lock-only", +] +` + +exports[`test/definitions/snapshots.js TAP derived package-lock-only > must match snapshot 1`] = ` +Array [ + "package-lock-only", + "package-lock", +] +` + +exports[`test/definitions/snapshots.js TAP derived parseable > must match snapshot 1`] = ` +Array [ + "parseable", +] +` + +exports[`test/definitions/snapshots.js TAP derived prefer-offline > must match snapshot 1`] = ` +Array [ + "prefer-offline", + "cache-min", +] +` + +exports[`test/definitions/snapshots.js TAP derived prefer-online > must match snapshot 1`] = ` +Array [ + "prefer-online", + "cache-max", +] +` + +exports[`test/definitions/snapshots.js TAP derived prefix > must match snapshot 1`] = ` +Array [ + "prefix", + "globalconfig", +] +` + +exports[`test/definitions/snapshots.js TAP derived preid > must match snapshot 1`] = ` +Array [ + "preid", +] +` + +exports[`test/definitions/snapshots.js TAP derived progress > must match snapshot 1`] = ` +Array [ + "progress", +] +` + +exports[`test/definitions/snapshots.js TAP derived project-scope > must match snapshot 1`] = ` +Array [ + "project-scope", + "scope", +] +` + +exports[`test/definitions/snapshots.js TAP derived proxy > must match snapshot 1`] = ` +Array [ + "proxy", +] +` + +exports[`test/definitions/snapshots.js TAP derived read-only > must match snapshot 1`] = ` +Array [ + "read-only", +] +` + +exports[`test/definitions/snapshots.js TAP derived rebuild-bundle > must match snapshot 1`] = ` +Array [ + "rebuild-bundle", +] +` + +exports[`test/definitions/snapshots.js TAP derived registry > must match snapshot 1`] = ` +Array [ + "registry", +] +` + +exports[`test/definitions/snapshots.js TAP derived replace-registry-host > must match snapshot 1`] = ` +Array [ + "replace-registry-host", +] +` + +exports[`test/definitions/snapshots.js TAP derived retry.factor > must match snapshot 1`] = ` +Array [ + "retry.factor", +] +` + +exports[`test/definitions/snapshots.js TAP derived retry.max-timeout > must match snapshot 1`] = ` +Array [ + "retry.max-timeout", +] +` + +exports[`test/definitions/snapshots.js TAP derived retry.min-timeout > must match snapshot 1`] = ` +Array [ + "retry.min-timeout", +] +` + +exports[`test/definitions/snapshots.js TAP derived retry.retries > must match snapshot 1`] = ` +Array [ + "retry.retries", +] +` + +exports[`test/definitions/snapshots.js TAP derived save > must match snapshot 1`] = ` +Array [ + "save", +] +` + +exports[`test/definitions/snapshots.js TAP derived save-bundle > must match snapshot 1`] = ` +Array [ + "save-bundle", + "save-peer", +] +` + +exports[`test/definitions/snapshots.js TAP derived save-prefix > must match snapshot 1`] = ` +Array [ + "save-prefix", + "save-exact", +] +` + +exports[`test/definitions/snapshots.js TAP derived save-type > must match snapshot 1`] = ` +Array [ + "save-type", + "save-dev", + "save-optional", + "save-peer", + "save-prod", +] +` + +exports[`test/definitions/snapshots.js TAP derived scope > must match snapshot 1`] = ` +Array [ + "scope", +] +` + +exports[`test/definitions/snapshots.js TAP derived script-shell > must match snapshot 1`] = ` +Array [ + "script-shell", +] +` + +exports[`test/definitions/snapshots.js TAP derived search.description > must match snapshot 1`] = ` +Array [ + "search.description", + "description", +] +` + +exports[`test/definitions/snapshots.js TAP derived search.exclude > must match snapshot 1`] = ` +Array [ + "search.exclude", + "searchexclude", +] +` + +exports[`test/definitions/snapshots.js TAP derived search.limit > must match snapshot 1`] = ` +Array [ + "search.limit", + "searchlimit", +] +` + +exports[`test/definitions/snapshots.js TAP derived search.opts > must match snapshot 1`] = ` +Array [ + "search.opts", + "searchopts", +] +` + +exports[`test/definitions/snapshots.js TAP derived search.staleness > must match snapshot 1`] = ` +Array [ + "search.staleness", +] +` + +exports[`test/definitions/snapshots.js TAP derived shell > must match snapshot 1`] = ` +Array [ + "shell", +] +` + +exports[`test/definitions/snapshots.js TAP derived sign-git-commit > must match snapshot 1`] = ` +Array [ + "sign-git-commit", +] +` + +exports[`test/definitions/snapshots.js TAP derived sign-git-tag > must match snapshot 1`] = ` +Array [ + "sign-git-tag", +] +` + +exports[`test/definitions/snapshots.js TAP derived silent > must match snapshot 1`] = ` +Array [ + "silent", + "loglevel", +] +` + +exports[`test/definitions/snapshots.js TAP derived strict-peer-deps > must match snapshot 1`] = ` +Array [ + "strict-peer-deps", +] +` + +exports[`test/definitions/snapshots.js TAP derived strict-ssl > must match snapshot 1`] = ` +Array [ + "strict-ssl", +] +` + +exports[`test/definitions/snapshots.js TAP derived tag-version-prefix > must match snapshot 1`] = ` +Array [ + "tag-version-prefix", +] +` + +exports[`test/definitions/snapshots.js TAP derived timeout > must match snapshot 1`] = ` +Array [ + "timeout", +] +` + +exports[`test/definitions/snapshots.js TAP derived umask > must match snapshot 1`] = ` +Array [ + "umask", +] +` + +exports[`test/definitions/snapshots.js TAP derived unicode > must match snapshot 1`] = ` +Array [ + "unicode", +] +` + +exports[`test/definitions/snapshots.js TAP derived user-agent > must match snapshot 1`] = ` +Array [ + "user-agent", + "ci-name", + "workspaces", + "workspace", + "npm-version", +] +` + +exports[`test/definitions/snapshots.js TAP derived workspaces-enabled > must match snapshot 1`] = ` +Array [ + "workspaces-enabled", + "workspaces", +] +` + +exports[`test/definitions/snapshots.js TAP derived workspaces-update > must match snapshot 1`] = ` +Array [ + "workspaces-update", +] +` + +exports[`test/definitions/snapshots.js TAP values Arborist > must match snapshot 1`] = ` +Function Arborist(classArboristextendsBase) +` + +exports[`test/definitions/snapshots.js TAP values hash-algorithm > must match snapshot 1`] = ` +sha1 +` + +exports[`test/definitions/snapshots.js TAP values npm-command > must match snapshot 1`] = ` + +` + +exports[`test/definitions/snapshots.js TAP values npm-version > must match snapshot 1`] = ` +{NPM-VERSION} +` diff --git a/workspaces/config/tap-snapshots/test/index.js.test.cjs b/workspaces/config/tap-snapshots/test/index.js.test.cjs deleted file mode 100644 index 6680fd2377960..0000000000000 --- a/workspaces/config/tap-snapshots/test/index.js.test.cjs +++ /dev/null @@ -1,240 +0,0 @@ -/* IMPORTANT - * This snapshot file is auto-generated, but designed for humans. - * It should be checked into source control and tracked carefully. - * Re-generate by setting TAP_SNAPSHOT=1 and running tests. - * Make sure to inspect the output below. Do not ignore changes! - */ -'use strict' -exports[`test/index.js TAP credentials management def_auth > default registry 1`] = ` -Object { - "auth": "aGVsbG86d29ybGQ=", - "password": "world", - "username": "hello", -} -` - -exports[`test/index.js TAP credentials management def_auth > default registry after set 1`] = ` -Object { - "auth": "aGVsbG86d29ybGQ=", - "password": "world", - "username": "hello", -} -` - -exports[`test/index.js TAP credentials management def_auth > other registry 1`] = ` -Object {} -` - -exports[`test/index.js TAP credentials management def_passNoUser > default registry 1`] = ` -Object { - "email": "i@izs.me", -} -` - -exports[`test/index.js TAP credentials management def_passNoUser > other registry 1`] = ` -Object { - "email": "i@izs.me", -} -` - -exports[`test/index.js TAP credentials management def_userNoPass > default registry 1`] = ` -Object { - "email": "i@izs.me", -} -` - -exports[`test/index.js TAP credentials management def_userNoPass > other registry 1`] = ` -Object { - "email": "i@izs.me", -} -` - -exports[`test/index.js TAP credentials management def_userpass > default registry 1`] = ` -Object { - "auth": "aGVsbG86d29ybGQ=", - "email": "i@izs.me", - "password": "world", - "username": "hello", -} -` - -exports[`test/index.js TAP credentials management def_userpass > default registry after set 1`] = ` -Object { - "auth": "aGVsbG86d29ybGQ=", - "email": "i@izs.me", - "password": "world", - "username": "hello", -} -` - -exports[`test/index.js TAP credentials management def_userpass > other registry 1`] = ` -Object { - "email": "i@izs.me", -} -` - -exports[`test/index.js TAP credentials management nerfed_auth > default registry 1`] = ` -Object { - "auth": "aGVsbG86d29ybGQ=", - "password": "world", - "username": "hello", -} -` - -exports[`test/index.js TAP credentials management nerfed_auth > default registry after set 1`] = ` -Object { - "auth": "aGVsbG86d29ybGQ=", - "password": "world", - "username": "hello", -} -` - -exports[`test/index.js TAP credentials management nerfed_auth > other registry 1`] = ` -Object {} -` - -exports[`test/index.js TAP credentials management nerfed_authToken > default registry 1`] = ` -Object { - "token": "0bad1de4", -} -` - -exports[`test/index.js TAP credentials management nerfed_authToken > default registry after set 1`] = ` -Object { - "token": "0bad1de4", -} -` - -exports[`test/index.js TAP credentials management nerfed_authToken > other registry 1`] = ` -Object {} -` - -exports[`test/index.js TAP credentials management nerfed_mtls > default registry 1`] = ` -Object { - "certfile": "/path/to/cert", - "keyfile": "/path/to/key", -} -` - -exports[`test/index.js TAP credentials management nerfed_mtls > default registry after set 1`] = ` -Object { - "certfile": "/path/to/cert", - "keyfile": "/path/to/key", -} -` - -exports[`test/index.js TAP credentials management nerfed_mtls > other registry 1`] = ` -Object {} -` - -exports[`test/index.js TAP credentials management nerfed_mtlsAuthToken > default registry 1`] = ` -Object { - "certfile": "/path/to/cert", - "keyfile": "/path/to/key", - "token": "0bad1de4", -} -` - -exports[`test/index.js TAP credentials management nerfed_mtlsAuthToken > default registry after set 1`] = ` -Object { - "certfile": "/path/to/cert", - "keyfile": "/path/to/key", - "token": "0bad1de4", -} -` - -exports[`test/index.js TAP credentials management nerfed_mtlsAuthToken > other registry 1`] = ` -Object {} -` - -exports[`test/index.js TAP credentials management nerfed_mtlsUserPass > default registry 1`] = ` -Object { - "auth": "aGVsbG86d29ybGQ=", - "certfile": "/path/to/cert", - "email": "i@izs.me", - "keyfile": "/path/to/key", - "password": "world", - "username": "hello", -} -` - -exports[`test/index.js TAP credentials management nerfed_mtlsUserPass > default registry after set 1`] = ` -Object { - "auth": "aGVsbG86d29ybGQ=", - "certfile": "/path/to/cert", - "email": "i@izs.me", - "keyfile": "/path/to/key", - "password": "world", - "username": "hello", -} -` - -exports[`test/index.js TAP credentials management nerfed_mtlsUserPass > other registry 1`] = ` -Object { - "email": "i@izs.me", -} -` - -exports[`test/index.js TAP credentials management nerfed_userpass > default registry 1`] = ` -Object { - "auth": "aGVsbG86d29ybGQ=", - "email": "i@izs.me", - "password": "world", - "username": "hello", -} -` - -exports[`test/index.js TAP credentials management nerfed_userpass > default registry after set 1`] = ` -Object { - "auth": "aGVsbG86d29ybGQ=", - "email": "i@izs.me", - "password": "world", - "username": "hello", -} -` - -exports[`test/index.js TAP credentials management nerfed_userpass > other registry 1`] = ` -Object { - "email": "i@izs.me", -} -` - -exports[`test/index.js TAP credentials management none_authToken > default registry 1`] = ` -Object { - "token": "0bad1de4", -} -` - -exports[`test/index.js TAP credentials management none_authToken > default registry after set 1`] = ` -Object { - "token": "0bad1de4", -} -` - -exports[`test/index.js TAP credentials management none_authToken > other registry 1`] = ` -Object {} -` - -exports[`test/index.js TAP credentials management none_emptyConfig > default registry 1`] = ` -Object {} -` - -exports[`test/index.js TAP credentials management none_emptyConfig > other registry 1`] = ` -Object {} -` - -exports[`test/index.js TAP credentials management none_lcAuthToken > default registry 1`] = ` -Object {} -` - -exports[`test/index.js TAP credentials management none_lcAuthToken > other registry 1`] = ` -Object {} -` - -exports[`test/index.js TAP credentials management none_noConfig > default registry 1`] = ` -Object {} -` - -exports[`test/index.js TAP credentials management none_noConfig > other registry 1`] = ` -Object {} -` diff --git a/workspaces/config/tap-snapshots/test/type-description.js.test.cjs b/workspaces/config/tap-snapshots/test/type-description.js.test.cjs deleted file mode 100644 index 9d80f7e09c315..0000000000000 --- a/workspaces/config/tap-snapshots/test/type-description.js.test.cjs +++ /dev/null @@ -1,449 +0,0 @@ -/* IMPORTANT - * This snapshot file is auto-generated, but designed for humans. - * It should be checked into source control and tracked carefully. - * Re-generate by setting TAP_SNAPSHOT=1 and running tests. - * Make sure to inspect the output below. Do not ignore changes! - */ -'use strict' -exports[`test/type-description.js TAP > must match snapshot 1`] = ` -Object { - "_exit": Array [ - "boolean value (true or false)", - ], - "access": Array [ - null, - "restricted", - "public", - ], - "all": Array [ - "boolean value (true or false)", - ], - "allow-same-version": Array [ - "boolean value (true or false)", - ], - "also": Array [ - null, - "dev", - "development", - ], - "always-auth": Array [ - "boolean value (true or false)", - ], - "audit": Array [ - "boolean value (true or false)", - ], - "audit-level": Array [ - "low", - "moderate", - "high", - "critical", - "none", - null, - ], - "auth-type": Array [ - "legacy", - "sso", - "saml", - "oauth", - ], - "before": Array [ - null, - "valid Date string", - ], - "bin-links": Array [ - "boolean value (true or false)", - ], - "browser": Array [ - null, - "boolean value (true or false)", - Function String(), - ], - "ca": Array [ - null, - Function String(), - Function Array(), - ], - "cache": Array [ - "valid filesystem path", - ], - "cache-lock-retries": Array [ - "numeric value", - ], - "cache-lock-stale": Array [ - "numeric value", - ], - "cache-lock-wait": Array [ - "numeric value", - ], - "cache-max": Array [ - "numeric value", - ], - "cache-min": Array [ - "numeric value", - ], - "cafile": Array [ - "valid filesystem path", - ], - "call": Array [ - Function String(), - ], - "cert": Array [ - null, - Function String(), - ], - "cidr": Array [ - null, - Function String(), - Function Array(), - ], - "color": Array [ - "always", - "boolean value (true or false)", - ], - "commit-hooks": Array [ - "boolean value (true or false)", - ], - "depth": Array [ - "numeric value", - ], - "description": Array [ - "boolean value (true or false)", - ], - "dev": Array [ - "boolean value (true or false)", - ], - "dry-run": Array [ - "boolean value (true or false)", - ], - "editor": Array [ - Function String(), - ], - "engine-strict": Array [ - "boolean value (true or false)", - ], - "fetch-retries": Array [ - "numeric value", - ], - "fetch-retry-factor": Array [ - "numeric value", - ], - "fetch-retry-maxtimeout": Array [ - "numeric value", - ], - "fetch-retry-mintimeout": Array [ - "numeric value", - ], - "force": Array [ - "boolean value (true or false)", - ], - "format-package-lock": Array [ - "boolean value (true or false)", - ], - "fund": Array [ - "boolean value (true or false)", - ], - "git": Array [ - Function String(), - ], - "git-tag-version": Array [ - "boolean value (true or false)", - ], - "global": Array [ - "boolean value (true or false)", - ], - "global-style": Array [ - "boolean value (true or false)", - ], - "globalconfig": Array [ - "valid filesystem path", - ], - "heading": Array [ - Function String(), - ], - "https-proxy": Array [ - null, - "full url with \\"http://\\"", - ], - "if-present": Array [ - "boolean value (true or false)", - ], - "ignore-prepublish": Array [ - "boolean value (true or false)", - ], - "ignore-scripts": Array [ - "boolean value (true or false)", - ], - "include": Array [ - Function Array(), - "prod", - "dev", - "optional", - "peer", - ], - "include-staged": Array [ - "boolean value (true or false)", - ], - "init-author-email": Array [ - Function String(), - ], - "init-author-name": Array [ - Function String(), - ], - "init-author-url": Array [ - "", - "full url with \\"http://\\"", - ], - "init-license": Array [ - Function String(), - ], - "init-module": Array [ - "valid filesystem path", - ], - "init-version": Array [ - "full valid SemVer string", - ], - "json": Array [ - "boolean value (true or false)", - ], - "key": Array [ - null, - Function String(), - ], - "legacy-bundling": Array [ - "boolean value (true or false)", - ], - "legacy-peer-deps": Array [ - "boolean value (true or false)", - ], - "link": Array [ - "boolean value (true or false)", - ], - "loglevel": Array [ - "silent", - "error", - "warn", - "notice", - "http", - "timing", - "info", - "verbose", - "silly", - ], - "logs-max": Array [ - "numeric value", - ], - "long": Array [ - "boolean value (true or false)", - ], - "maxsockets": Array [ - "numeric value", - ], - "message": Array [ - Function String(), - ], - "metrics-registry": Array [ - null, - Function String(), - ], - "multiple-numbers": Array [ - Function Array(), - "numeric value", - ], - "node-options": Array [ - null, - Function String(), - ], - "node-version": Array [ - null, - "full valid SemVer string", - ], - "noproxy": Array [ - null, - Function String(), - Function Array(), - ], - "offline": Array [ - "boolean value (true or false)", - ], - "omit": Array [ - Function Array(), - "dev", - "optional", - "peer", - ], - "only": Array [ - null, - "dev", - "development", - "prod", - "production", - ], - "optional": Array [ - "boolean value (true or false)", - ], - "otp": Array [ - null, - Function String(), - ], - "package": Array [ - Function String(), - Function Array(), - ], - "package-lock": Array [ - "boolean value (true or false)", - ], - "package-lock-only": Array [ - "boolean value (true or false)", - ], - "parseable": Array [ - "boolean value (true or false)", - ], - "prefer-offline": Array [ - "boolean value (true or false)", - ], - "prefer-online": Array [ - "boolean value (true or false)", - ], - "prefix": Array [ - "valid filesystem path", - ], - "preid": Array [ - Function String(), - ], - "production": Array [ - "boolean value (true or false)", - ], - "progress": Array [ - "boolean value (true or false)", - ], - "proxy": Array [ - null, - false, - "full url with \\"http://\\"", - ], - "read-only": Array [ - "boolean value (true or false)", - ], - "rebuild-bundle": Array [ - "boolean value (true or false)", - ], - "registry": Array [ - null, - "full url with \\"http://\\"", - ], - "rollback": Array [ - "boolean value (true or false)", - ], - "save": Array [ - "boolean value (true or false)", - ], - "save-bundle": Array [ - "boolean value (true or false)", - ], - "save-dev": Array [ - "boolean value (true or false)", - ], - "save-exact": Array [ - "boolean value (true or false)", - ], - "save-optional": Array [ - "boolean value (true or false)", - ], - "save-prefix": Array [ - Function String(), - ], - "save-prod": Array [ - "boolean value (true or false)", - ], - "scope": Array [ - Function String(), - ], - "script-shell": Array [ - null, - Function String(), - ], - "scripts-prepend-node-path": Array [ - "boolean value (true or false)", - "auto", - "warn-only", - ], - "searchexclude": Array [ - null, - Function String(), - ], - "searchlimit": Array [ - "numeric value", - ], - "searchopts": Array [ - Function String(), - ], - "searchstaleness": Array [ - "numeric value", - ], - "send-metrics": Array [ - "boolean value (true or false)", - ], - "shell": Array [ - Function String(), - ], - "shrinkwrap": Array [ - "boolean value (true or false)", - ], - "sign-git-commit": Array [ - "boolean value (true or false)", - ], - "sign-git-tag": Array [ - "boolean value (true or false)", - ], - "sso-poll-frequency": Array [ - "numeric value", - ], - "sso-type": Array [ - null, - "oauth", - "saml", - ], - "strict-ssl": Array [ - "boolean value (true or false)", - ], - "tag": Array [ - Function String(), - ], - "tag-version-prefix": Array [ - Function String(), - ], - "timing": Array [ - "boolean value (true or false)", - ], - "tmp": Array [ - "valid filesystem path", - ], - "umask": Array [ - "octal number in range 0o000..0o777 (0..511)", - ], - "unicode": Array [ - "boolean value (true or false)", - ], - "update-notifier": Array [ - "boolean value (true or false)", - ], - "usage": Array [ - "boolean value (true or false)", - ], - "user-agent": Array [ - Function String(), - ], - "userconfig": Array [ - "valid filesystem path", - ], - "version": Array [ - "boolean value (true or false)", - ], - "versions": Array [ - "boolean value (true or false)", - ], - "viewer": Array [ - Function String(), - ], -} -` diff --git a/workspaces/config/test/config/definition.js b/workspaces/config/test/config/definition.js deleted file mode 100644 index a17a1a09a2240..0000000000000 --- a/workspaces/config/test/config/definition.js +++ /dev/null @@ -1,218 +0,0 @@ -const t = require('tap') -const Definition = require('../../../../lib/utils/config/definition.js') -const { - typeDefs: { - semver: { type: semver }, - Umask: { type: Umask }, - url: { type: url }, - path: { type: path }, - }, -} = require('@npmcli/config') - -t.test('basic definition', async t => { - const def = new Definition('key', { - default: 'some default value', - type: [Number, String], - description: 'just a test thingie', - }) - t.same(def, { - constructor: Definition, - key: 'key', - default: 'some default value', - defaultDescription: '"some default value"', - type: [Number, String], - hint: '', - usage: '--key ', - typeDescription: 'Number or String', - description: 'just a test thingie', - envExport: true, - }) - t.matchSnapshot(def.describe(), 'human-readable description') - - const deprecated = new Definition('deprecated', { - deprecated: 'do not use this', - default: 1234, - description: ' it should not be used\n ever\n\n not even once.\n\n', - type: Number, - defaultDescription: 'A number bigger than 1', - typeDescription: 'An expression of a numeric quantity using numerals', - }) - t.matchSnapshot(deprecated.describe(), 'description of deprecated thing') - - const nullOrUmask = new Definition('key', { - default: null, - type: [null, Umask], - description: 'asdf', - }) - t.equal(nullOrUmask.typeDescription, 'null or Octal numeric string in range 0000..0777 (0..511)') - const nullDateOrBool = new Definition('key', { - default: 7, - type: [null, Date, Boolean], - description: 'asdf', - }) - t.equal(nullDateOrBool.typeDescription, 'null, Date, or Boolean') - const manyPaths = new Definition('key', { - default: ['asdf'], - type: [path, Array], - description: 'asdf', - }) - t.equal(manyPaths.typeDescription, 'Path (can be set multiple times)') - const pathOrUrl = new Definition('key', { - default: ['https://example.com'], - type: [path, url], - description: 'asdf', - }) - t.equal(pathOrUrl.typeDescription, 'Path or URL') - const multi12 = new Definition('key', { - default: [], - type: [1, 2, Array], - description: 'asdf', - }) - t.equal(multi12.typeDescription, '1 or 2 (can be set multiple times)') - const multi123 = new Definition('key', { - default: [], - type: [1, 2, 3, Array], - description: 'asdf', - }) - t.equal(multi123.typeDescription, '1, 2, or 3 (can be set multiple times)') - const multi123Semver = new Definition('key', { - default: [], - type: [1, 2, 3, Array, semver], - description: 'asdf', - }) - t.equal(multi123Semver.typeDescription, '1, 2, 3, or SemVer string (can be set multiple times)') - const hasUsage = new Definition('key', { - default: 'test default', - type: String, - description: 'test description', - usage: 'test usage', - }) - t.equal(hasUsage.usage, 'test usage') - const hasShort = new Definition('key', { - default: 'test default', - short: 't', - type: String, - description: 'test description', - }) - t.equal(hasShort.usage, '-t|--key ') - const multiHasShort = new Definition('key', { - default: 'test default', - short: 't', - type: [null, String], - description: 'test description', - }) - t.equal(multiHasShort.usage, '-t|--key ') - const hardCodedTypes = new Definition('key', { - default: 'test default', - type: ['string1', 'string2'], - description: 'test description', - }) - t.equal(hardCodedTypes.usage, '--key ') - const hardCodedOptionalTypes = new Definition('key', { - default: 'test default', - type: [null, 'string1', 'string2'], - description: 'test description', - }) - t.equal(hardCodedOptionalTypes.usage, '--key ') - const hasHint = new Definition('key', { - default: 'test default', - type: String, - description: 'test description', - hint: '', - }) - t.equal(hasHint.usage, '--key ') - const optionalBool = new Definition('key', { - default: null, - type: [null, Boolean], - description: 'asdf', - }) - t.equal(optionalBool.usage, '--key') - - const noExported = new Definition('methane', { - envExport: false, - type: String, - typeDescription: 'Greenhouse Gas', - default: 'CH4', - description: ` - This is bad for the environment, for our children, do not put it there. - `, - }) - t.equal(noExported.envExport, false, 'envExport flag is false') - t.equal(noExported.describe(), `#### \`methane\` - -* Default: "CH4" -* Type: Greenhouse Gas - -This is bad for the environment, for our children, do not put it there. - -This value is not exported to the environment for child processes.`) -}) - -t.test('missing fields', async t => { - t.throws(() => new Definition('lacks-default', { - description: 'no default', - type: String, - }), { message: 'config lacks default: lacks-default' }) - t.throws(() => new Definition('lacks-type', { - description: 'no type', - default: 1234, - }), { message: 'config lacks type: lacks-type' }) - t.throws(() => new Definition(null, { - description: 'falsey key', - default: 1234, - type: Number, - }), { message: 'config lacks key: null' }) - t.throws(() => new Definition('extra-field', { - type: String, - default: 'extra', - extra: 'more than is wanted', - description: 'this is not ok', - }), { message: 'config defines unknown field extra: extra-field' }) -}) - -t.test('long description', async t => { - const { stdout: { columns } } = process - t.teardown(() => process.stdout.columns = columns) - - const long = new Definition('walden', { - description: ` - WHEN I WROTE the following pages, or rather the bulk of them, I lived - alone, in the woods, a mile from any neighbor, in a house which I had - built myself, on the shore of Walden Pond, in Concord, Massachusetts, and - earned my living by the labor of my hands only. I lived there two years - and two months. At present I am a sojourner in civilized life again. - - I should not obtrude my affairs so much on the notice of my readers if - very particular inquiries had not been made by my townsmen concerning my - mode of life, which some would call impertinent, though they do not - appear to me at all impertinent, but, considering the circumstances, very - natural and pertinent. - - \`\`\` - this.is('a', { - code: 'sample', - }) - - with (multiple) { - blocks() - } - \`\`\` - `, - default: true, - type: Boolean, - }) - process.stdout.columns = 40 - t.matchSnapshot(long.describe(), 'cols=40') - - process.stdout.columns = 9000 - t.matchSnapshot(long.describe(), 'cols=9000') - - process.stdout.columns = 0 - t.matchSnapshot(long.describe(), 'cols=0') - - process.stdout.columns = -1 - t.matchSnapshot(long.describe(), 'cols=-1') - - process.stdout.columns = NaN - t.matchSnapshot(long.describe(), 'cols=NaN') -}) diff --git a/workspaces/config/test/config/definitions.js b/workspaces/config/test/config/definitions.js deleted file mode 100644 index a5c321e61ead2..0000000000000 --- a/workspaces/config/test/config/definitions.js +++ /dev/null @@ -1,933 +0,0 @@ -const t = require('tap') -const { resolve } = require('path') -const mockGlobals = require('@npmcli/mock-globals') -const tmock = require('../../../fixtures/tmock') -const pkg = require('../../../../package.json') - -// have to fake the node version, or else it'll only pass on this one -mockGlobals(t, { 'process.version': 'v14.8.0', 'process.env.NODE_ENV': undefined }) - -const mockDefs = (mocks = {}) => tmock(t, '{LIB}/utils/config/definitions.js', mocks).definitions - -const isWin = (isWindows) => ({ '{LIB}/utils/is-windows.js': { isWindows } }) - -t.test('basic flattening function camelCases from css-case', t => { - const flat = {} - const obj = { 'prefer-online': true } - const definitions = mockDefs() - definitions['prefer-online'].flatten('prefer-online', obj, flat) - t.strictSame(flat, { preferOnline: true }) - t.end() -}) - -t.test('editor', t => { - t.test('has EDITOR and VISUAL, use EDITOR', t => { - mockGlobals(t, { 'process.env': { EDITOR: 'vim', VISUAL: 'mate' } }) - const defs = mockDefs() - t.equal(defs.editor.default, 'vim') - t.end() - }) - t.test('has VISUAL but no EDITOR, use VISUAL', t => { - mockGlobals(t, { 'process.env': { EDITOR: undefined, VISUAL: 'mate' } }) - const defs = mockDefs() - t.equal(defs.editor.default, 'mate') - t.end() - }) - t.test('has neither EDITOR nor VISUAL, system specific', t => { - mockGlobals(t, { - 'process.env': { - EDITOR: undefined, - VISUAL: undefined, - SYSTEMROOT: 'C:\\Windows', - }, - }) - const defsWin = mockDefs(isWin(true)) - t.equal(defsWin.editor.default, 'C:\\Windows\\notepad.exe') - const defsNix = mockDefs(isWin(false)) - t.equal(defsNix.editor.default, 'vi') - t.end() - }) - t.end() -}) - -t.test('shell', t => { - t.test('windows, env.ComSpec then cmd.exe', t => { - mockGlobals(t, { 'process.env.ComSpec': 'command.com' }) - const defsComSpec = mockDefs(isWin(true)) - t.equal(defsComSpec.shell.default, 'command.com') - mockGlobals(t, { 'process.env.ComSpec': undefined }) - const defsNoComSpec = mockDefs(isWin(true)) - t.equal(defsNoComSpec.shell.default, 'cmd') - t.end() - }) - - t.test('nix, SHELL then sh', t => { - mockGlobals(t, { 'process.env.SHELL': '/usr/local/bin/bash' }) - const defsShell = mockDefs(isWin(false)) - t.equal(defsShell.shell.default, '/usr/local/bin/bash') - mockGlobals(t, { 'process.env.SHELL': undefined }) - const defsNoShell = mockDefs(isWin(false)) - t.equal(defsNoShell.shell.default, 'sh') - t.end() - }) - - t.end() -}) - -t.test('local-address allowed types', t => { - t.test('get list from os.networkInterfaces', t => { - const os = { - tmpdir: () => '/tmp', - networkInterfaces: () => ({ - eth420: [{ address: '127.0.0.1' }], - eth69: [{ address: 'no place like home' }], - }), - } - const defs = mockDefs({ os }) - t.same(defs['local-address'].type, [ - null, - '127.0.0.1', - 'no place like home', - ]) - t.end() - }) - t.test('handle os.networkInterfaces throwing', t => { - const os = { - tmpdir: () => '/tmp', - networkInterfaces: () => { - throw new Error('no network interfaces for some reason') - }, - } - const defs = mockDefs({ os }) - t.same(defs['local-address'].type, [null]) - t.end() - }) - t.end() -}) - -t.test('unicode allowed?', t => { - const setGlobal = (obj = {}) => mockGlobals(t, { 'process.env': obj }) - - setGlobal({ LC_ALL: 'utf8', LC_CTYPE: 'UTF-8', LANG: 'Unicode utf-8' }) - - const lcAll = mockDefs() - t.equal(lcAll.unicode.default, true) - setGlobal({ LC_ALL: 'no unicode for youUUUU!' }) - const noLcAll = mockDefs() - t.equal(noLcAll.unicode.default, false) - - setGlobal({ LC_ALL: undefined }) - const lcCtype = mockDefs() - t.equal(lcCtype.unicode.default, true) - setGlobal({ LC_CTYPE: 'something other than unicode version 8' }) - const noLcCtype = mockDefs() - t.equal(noLcCtype.unicode.default, false) - - setGlobal({ LC_CTYPE: undefined }) - const lang = mockDefs() - t.equal(lang.unicode.default, true) - setGlobal({ LANG: 'ISO-8859-1' }) - const noLang = mockDefs() - t.equal(noLang.unicode.default, false) - t.end() -}) - -t.test('cache', t => { - mockGlobals(t, { 'process.env.LOCALAPPDATA': 'app/data/local' }) - const defsWinLocalAppData = mockDefs(isWin(true)) - t.equal(defsWinLocalAppData.cache.default, 'app/data/local/npm-cache') - - mockGlobals(t, { 'process.env.LOCALAPPDATA': undefined }) - const defsWinNoLocalAppData = mockDefs(isWin(true)) - t.equal(defsWinNoLocalAppData.cache.default, '~/npm-cache') - - const defsNix = mockDefs(isWin(false)) - t.equal(defsNix.cache.default, '~/.npm') - - const flat = {} - defsNix.cache.flatten('cache', { cache: '/some/cache/value' }, flat) - const { join } = require('path') - t.equal(flat.cache, join('/some/cache/value', '_cacache')) - t.equal(flat.npxCache, join('/some/cache/value', '_npx')) - - t.end() -}) - -t.test('flatteners that populate flat.omit array', t => { - t.test('also', t => { - const flat = {} - const obj = {} - - // ignored if setting is not dev or development - obj.also = 'ignored' - mockDefs().also.flatten('also', obj, flat) - t.strictSame(obj, { also: 'ignored', omit: [], include: [] }, 'nothing done') - t.strictSame(flat, { omit: [] }, 'nothing done') - - obj.also = 'development' - mockDefs().also.flatten('also', obj, flat) - t.strictSame(obj, { - also: 'development', - omit: [], - include: ['dev'], - }, 'marked dev as included') - t.strictSame(flat, { omit: [] }, 'nothing omitted, so nothing changed') - - obj.omit = ['dev', 'optional'] - obj.include = [] - mockDefs().also.flatten('also', obj, flat) - t.strictSame(obj, { - also: 'development', - omit: ['optional'], - include: ['dev'], - }, 'marked dev as included') - t.strictSame(flat, { omit: ['optional'] }, 'removed dev from omit') - t.end() - }) - - t.test('include', t => { - const flat = {} - const obj = { include: ['dev'] } - mockDefs().include.flatten('include', obj, flat) - t.strictSame(flat, { omit: [] }, 'not omitting anything') - obj.omit = ['optional', 'dev'] - mockDefs().include.flatten('include', obj, flat) - t.strictSame(flat, { omit: ['optional'] }, 'only omitting optional') - t.end() - }) - - t.test('omit', t => { - const flat = {} - const obj = { include: ['dev'], omit: ['dev', 'optional'] } - mockDefs().omit.flatten('omit', obj, flat) - t.strictSame(flat, { omit: ['optional'] }, 'do not omit what is included') - - mockGlobals(t, { 'process.env.NODE_ENV': 'production' }) - const defProdEnv = mockDefs() - t.strictSame(defProdEnv.omit.default, ['dev'], 'omit dev in production') - t.end() - }) - - t.test('only', t => { - const flat = {} - const obj = { only: 'asdf' } - mockDefs().only.flatten('only', obj, flat) - t.strictSame(flat, { omit: [] }, 'ignored if value is not production') - - obj.only = 'prod' - mockDefs().only.flatten('only', obj, flat) - t.strictSame(flat, { omit: ['dev'] }, 'omit dev when --only=prod') - - obj.include = ['dev'] - flat.omit = [] - mockDefs().only.flatten('only', obj, flat) - t.strictSame(flat, { omit: [] }, 'do not omit when included') - - t.end() - }) - - t.test('optional', t => { - const flat = {} - const obj = { optional: null } - - mockDefs().optional.flatten('optional', obj, flat) - t.strictSame(obj, { - optional: null, - omit: [], - include: [], - }, 'do nothing by default') - t.strictSame(flat, { omit: [] }, 'do nothing by default') - - obj.optional = true - mockDefs().optional.flatten('optional', obj, flat) - t.strictSame(obj, { - omit: [], - optional: true, - include: ['optional'], - }, 'include optional when set') - t.strictSame(flat, { omit: [] }, 'nothing to omit in flatOptions') - - delete obj.include - obj.optional = false - mockDefs().optional.flatten('optional', obj, flat) - t.strictSame(obj, { - omit: ['optional'], - optional: false, - include: [], - }, 'omit optional when set false') - t.strictSame(flat, { omit: ['optional'] }, 'omit optional when set false') - - t.end() - }) - - t.test('production', t => { - const flat = {} - const obj = { production: true } - mockDefs().production.flatten('production', obj, flat) - t.strictSame(obj, { - production: true, - omit: ['dev'], - include: [], - }, '--production sets --omit=dev') - t.strictSame(flat, { omit: ['dev'] }, '--production sets --omit=dev') - - delete obj.omit - obj.production = false - delete flat.omit - mockDefs().production.flatten('production', obj, flat) - t.strictSame(obj, { - production: false, - include: ['dev'], - omit: [], - }, '--no-production explicitly includes dev') - t.strictSame(flat, { omit: [] }, '--no-production has no effect') - - obj.production = true - obj.include = ['dev'] - mockDefs().production.flatten('production', obj, flat) - t.strictSame(obj, { - production: true, - include: ['dev'], - omit: [], - }, 'omit and include dev') - t.strictSame(flat, { omit: [] }, 'do not omit dev when included') - - t.end() - }) - - t.test('dev', t => { - const flat = {} - const obj = { dev: true } - mockDefs().dev.flatten('dev', obj, flat) - t.strictSame(obj, { - dev: true, - omit: [], - include: ['dev'], - }) - t.end() - }) - - t.end() -}) - -t.test('cache-max', t => { - const flat = {} - const obj = { 'cache-max': 10342 } - mockDefs()['cache-max'].flatten('cache-max', obj, flat) - t.strictSame(flat, {}, 'no effect if not <= 0') - obj['cache-max'] = 0 - mockDefs()['cache-max'].flatten('cache-max', obj, flat) - t.strictSame(flat, { preferOnline: true }, 'preferOnline if <= 0') - t.end() -}) - -t.test('cache-min', t => { - const flat = {} - const obj = { 'cache-min': 123 } - mockDefs()['cache-min'].flatten('cache-min', obj, flat) - t.strictSame(flat, {}, 'no effect if not >= 9999') - obj['cache-min'] = 9999 - mockDefs()['cache-min'].flatten('cache-min', obj, flat) - t.strictSame(flat, { preferOffline: true }, 'preferOffline if >=9999') - t.end() -}) - -t.test('color', t => { - const setTTY = (stream, value) => mockGlobals(t, { [`process.${stream}.isTTY`]: value }) - - const flat = {} - const obj = { color: 'always' } - - mockDefs().color.flatten('color', obj, flat) - t.strictSame(flat, { color: true, logColor: true }, 'true when --color=always') - - obj.color = false - mockDefs().color.flatten('color', obj, flat) - t.strictSame(flat, { color: false, logColor: false }, 'true when --no-color') - - setTTY('stdout', false) - setTTY('stderr', false) - - obj.color = true - mockDefs().color.flatten('color', obj, flat) - t.strictSame(flat, { color: false, logColor: false }, 'no color when stdout not tty') - setTTY('stdout', true) - mockDefs().color.flatten('color', obj, flat) - t.strictSame(flat, { color: true, logColor: false }, '--color turns on color when stdout is tty') - setTTY('stdout', false) - - obj.color = true - mockDefs().color.flatten('color', obj, flat) - t.strictSame(flat, { color: false, logColor: false }, 'no color when stderr not tty') - setTTY('stderr', true) - mockDefs().color.flatten('color', obj, flat) - t.strictSame(flat, { color: false, logColor: true }, '--color turns on color when stderr is tty') - setTTY('stderr', false) - - const setColor = (value) => mockGlobals(t, { 'process.env.NO_COLOR': value }) - - setColor(undefined) - const defsAllowColor = mockDefs() - t.equal(defsAllowColor.color.default, true, 'default true when no NO_COLOR env') - - setColor('0') - const defsNoColor0 = mockDefs() - t.equal(defsNoColor0.color.default, true, 'default true when no NO_COLOR=0') - - setColor('1') - const defsNoColor1 = mockDefs() - t.equal(defsNoColor1.color.default, false, 'default false when no NO_COLOR=1') - - t.end() -}) - -t.test('progress', t => { - const setEnv = ({ tty, term } = {}) => mockGlobals(t, { - 'process.stderr.isTTY': tty, - 'process.env.TERM': term, - }) - - const flat = {} - - mockDefs().progress.flatten('progress', {}, flat) - t.strictSame(flat, { progress: false }) - - setEnv({ tty: true, term: 'notdumb' }) - mockDefs().progress.flatten('progress', { progress: true }, flat) - t.strictSame(flat, { progress: true }) - - setEnv({ tty: false, term: 'notdumb' }) - mockDefs().progress.flatten('progress', { progress: true }, flat) - t.strictSame(flat, { progress: false }) - - setEnv({ tty: true, term: 'dumb' }) - mockDefs().progress.flatten('progress', { progress: true }, flat) - t.strictSame(flat, { progress: false }) - - t.end() -}) - -t.test('retry options', t => { - const obj = {} - // : flat.retry[