From 6df246fd721c8c6697035ca70ec109c689cafeef Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Sun, 30 Oct 2022 17:42:46 -0700 Subject: chore: bring in @npmcli/config as a workspace --- .github/workflows/ci-npmcli-config.yml | 94 + .gitignore | 1 + .release-please-manifest.json | 3 +- DEPENDENCIES.md | 7 +- node_modules/@npmcli/config | 1 + node_modules/@npmcli/config/LICENSE | 15 - node_modules/@npmcli/config/lib/env-replace.js | 14 - node_modules/@npmcli/config/lib/errors.js | 22 - node_modules/@npmcli/config/lib/index.js | 917 ------- node_modules/@npmcli/config/lib/nerf-dart.js | 18 - node_modules/@npmcli/config/lib/parse-field.js | 81 - node_modules/@npmcli/config/lib/set-envs.js | 111 - node_modules/@npmcli/config/lib/type-defs.js | 59 - .../@npmcli/config/lib/type-description.js | 21 - node_modules/@npmcli/config/lib/umask.js | 31 - node_modules/@npmcli/config/package.json | 54 - package-lock.json | 41 +- release-please-config.json | 3 +- workspaces/config/.eslintrc.js | 17 + workspaces/config/.gitignore | 21 + workspaces/config/CHANGELOG.md | 133 + workspaces/config/LICENSE | 15 + workspaces/config/README.md | 260 ++ workspaces/config/lib/env-replace.js | 14 + workspaces/config/lib/errors.js | 22 + workspaces/config/lib/index.js | 917 +++++++ workspaces/config/lib/nerf-dart.js | 18 + workspaces/config/lib/parse-field.js | 81 + workspaces/config/lib/set-envs.js | 111 + workspaces/config/lib/type-defs.js | 59 + workspaces/config/lib/type-description.js | 21 + workspaces/config/lib/umask.js | 31 + workspaces/config/map.js | 1 + workspaces/config/package.json | 55 + workspaces/config/scripts/example.js | 43 + .../config/tap-snapshots/test/index.js.test.cjs | 240 ++ .../test/type-description.js.test.cjs | 449 ++++ workspaces/config/test/env-replace.js | 13 + workspaces/config/test/fixtures/cafile | 32 + workspaces/config/test/fixtures/defaults.js | 143 ++ workspaces/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/index.js | 1295 ++++++++++ workspaces/config/test/nerf-dart.js | 44 + workspaces/config/test/parse-field.js | 36 + workspaces/config/test/set-envs.js | 212 ++ workspaces/config/test/type-defs.js | 22 + workspaces/config/test/type-description.js | 14 + 50 files changed, 7283 insertions(+), 1363 deletions(-) create mode 100644 .github/workflows/ci-npmcli-config.yml create mode 120000 node_modules/@npmcli/config delete mode 100644 node_modules/@npmcli/config/LICENSE delete mode 100644 node_modules/@npmcli/config/lib/env-replace.js delete mode 100644 node_modules/@npmcli/config/lib/errors.js delete mode 100644 node_modules/@npmcli/config/lib/index.js delete mode 100644 node_modules/@npmcli/config/lib/nerf-dart.js delete mode 100644 node_modules/@npmcli/config/lib/parse-field.js delete mode 100644 node_modules/@npmcli/config/lib/set-envs.js delete mode 100644 node_modules/@npmcli/config/lib/type-defs.js delete mode 100644 node_modules/@npmcli/config/lib/type-description.js delete mode 100644 node_modules/@npmcli/config/lib/umask.js delete mode 100644 node_modules/@npmcli/config/package.json create mode 100644 workspaces/config/.eslintrc.js create mode 100644 workspaces/config/.gitignore create mode 100644 workspaces/config/CHANGELOG.md create mode 100644 workspaces/config/LICENSE create mode 100644 workspaces/config/README.md create mode 100644 workspaces/config/lib/env-replace.js create mode 100644 workspaces/config/lib/errors.js create mode 100644 workspaces/config/lib/index.js create mode 100644 workspaces/config/lib/nerf-dart.js create mode 100644 workspaces/config/lib/parse-field.js create mode 100644 workspaces/config/lib/set-envs.js create mode 100644 workspaces/config/lib/type-defs.js create mode 100644 workspaces/config/lib/type-description.js create mode 100644 workspaces/config/lib/umask.js create mode 100644 workspaces/config/map.js create mode 100644 workspaces/config/package.json create mode 100644 workspaces/config/scripts/example.js create mode 100644 workspaces/config/tap-snapshots/test/index.js.test.cjs create mode 100644 workspaces/config/tap-snapshots/test/type-description.js.test.cjs create mode 100644 workspaces/config/test/env-replace.js create mode 100644 workspaces/config/test/fixtures/cafile create mode 100644 workspaces/config/test/fixtures/defaults.js create mode 100644 workspaces/config/test/fixtures/definitions.js create mode 100644 workspaces/config/test/fixtures/flatten.js create mode 100644 workspaces/config/test/fixtures/shorthands.js create mode 100644 workspaces/config/test/fixtures/types.js create mode 100644 workspaces/config/test/index.js create mode 100644 workspaces/config/test/nerf-dart.js create mode 100644 workspaces/config/test/parse-field.js create mode 100644 workspaces/config/test/set-envs.js create mode 100644 workspaces/config/test/type-defs.js create mode 100644 workspaces/config/test/type-description.js diff --git a/.github/workflows/ci-npmcli-config.yml b/.github/workflows/ci-npmcli-config.yml new file mode 100644 index 000000000..72cc302e7 --- /dev/null +++ b/.github/workflows/ci-npmcli-config.yml @@ -0,0 +1,94 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: CI - @npmcli/config + +on: + workflow_dispatch: + pull_request: + paths: + - workspaces/config/** + push: + branches: + - main + - latest + paths: + - workspaces/config/** + 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/config + - name: Post Lint + run: node . run postlint --ignore-scripts -w @npmcli/config + + 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/config + - name: Check Git Status + if: matrix && matrix.platform.os != 'windows-latest' + run: node scripts/git-dirty.js diff --git a/.gitignore b/.gitignore index c857a68a6..2ab23adf7 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ !/workspaces/ /workspaces/* !/workspaces/arborist/ +!/workspaces/config/ !/workspaces/libnpmaccess/ !/workspaces/libnpmdiff/ !/workspaces/libnpmexec/ diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 60c37e232..ebe465127 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -11,5 +11,6 @@ "workspaces/libnpmpublish": "7.0.1", "workspaces/libnpmsearch": "6.0.0", "workspaces/libnpmteam": "5.0.0", - "workspaces/libnpmversion": "4.0.0" + "workspaces/libnpmversion": "4.0.0", + "workspaces/config": "6.0.1" } diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index efe532d54..7e9e26ec9 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -188,7 +188,9 @@ graph LR; npmcli-arborist-->treeverse; npmcli-config-->ini; npmcli-config-->nopt; + npmcli-config-->npmcli-eslint-config["@npmcli/eslint-config"]; npmcli-config-->npmcli-map-workspaces["@npmcli/map-workspaces"]; + npmcli-config-->npmcli-template-oss["@npmcli/template-oss"]; npmcli-config-->proc-log; npmcli-config-->read-package-json-fast; npmcli-config-->semver; @@ -613,10 +615,13 @@ graph LR; npmcli-arborist-->walk-up-path; npmcli-config-->ini; npmcli-config-->nopt; + npmcli-config-->npmcli-eslint-config["@npmcli/eslint-config"]; npmcli-config-->npmcli-map-workspaces["@npmcli/map-workspaces"]; + npmcli-config-->npmcli-template-oss["@npmcli/template-oss"]; npmcli-config-->proc-log; npmcli-config-->read-package-json-fast; npmcli-config-->semver; + npmcli-config-->tap; npmcli-config-->walk-up-path; npmcli-disparity-colors-->ansi-styles; npmcli-docs-->cmark-gfm; @@ -763,4 +768,4 @@ packages higher up the chain. - @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, readdir-scoped-modules, promzard - @npmcli/docs, npm-bundled, read-package-json-fast, @npmcli/fs, unique-filename, npm-install-checks, npm-package-arg, npm-packlist, normalize-package-data, @npmcli/package-json, bin-links, nopt, npmlog, parse-conflict-json, dezalgo, read - - @npmcli/eslint-config, @npmcli/template-oss, ignore-walk, npm-normalize-package-bin, @npmcli/name-from-folder, json-parse-even-better-errors, semver, @npmcli/move-file, 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, wrappy, treeverse, minify-registry-metadata, @npmcli/disparity-colors, @npmcli/ci-detect, mute-stream, ini, npm-audit-report, npm-user-validate + - @npmcli/eslint-config, @npmcli/template-oss, ignore-walk, npm-normalize-package-bin, @npmcli/name-from-folder, json-parse-even-better-errors, semver, @npmcli/move-file, 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, wrappy, treeverse, minify-registry-metadata, ini, @npmcli/disparity-colors, @npmcli/ci-detect, mute-stream, npm-audit-report, npm-user-validate diff --git a/node_modules/@npmcli/config b/node_modules/@npmcli/config new file mode 120000 index 000000000..bf09f370d --- /dev/null +++ b/node_modules/@npmcli/config @@ -0,0 +1 @@ +../../workspaces/config \ No newline at end of file diff --git a/node_modules/@npmcli/config/LICENSE b/node_modules/@npmcli/config/LICENSE deleted file mode 100644 index 19cec97b1..000000000 --- a/node_modules/@npmcli/config/LICENSE +++ /dev/null @@ -1,15 +0,0 @@ -The ISC License - -Copyright (c) npm, Inc. - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/node_modules/@npmcli/config/lib/env-replace.js b/node_modules/@npmcli/config/lib/env-replace.js deleted file mode 100644 index c851f6e4d..000000000 --- a/node_modules/@npmcli/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/node_modules/@npmcli/config/lib/errors.js b/node_modules/@npmcli/config/lib/errors.js deleted file mode 100644 index fa3e20798..000000000 --- a/node_modules/@npmcli/config/lib/errors.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict' - -class ErrInvalidAuth extends Error { - constructor (problems) { - let message = 'Invalid auth configuration found: ' - message += 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) - this.code = 'ERR_INVALID_AUTH' - this.problems = problems - } -} - -module.exports = { - ErrInvalidAuth, -} diff --git a/node_modules/@npmcli/config/lib/index.js b/node_modules/@npmcli/config/lib/index.js deleted file mode 100644 index e1d47ffcd..000000000 --- a/node_modules/@npmcli/config/lib/index.js +++ /dev/null @@ -1,917 +0,0 @@ -// 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 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 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') - -class Config { - static get typeDefs () { - return typeDefs - } - - 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(), - }) { - // 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 - types[key] = def.type - if (def.deprecated) { - this.deprecated[key] = def.deprecated.trim().replace(/\n +/, '\n') - } - } - - // 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 - - // set when we load configs - this.globalPrefix = null - this.localPrefix = 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 - } - - get loaded () { - return this[_loaded] - } - - get prefix () { - return this[_get]('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) - } - - [_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 (key, where) { - if (!this.loaded) { - throw new Error('call config.load() before reading values') - } - return this[_get](key, where) - } - - // 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 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] - } - - 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) - } - delete this.data.get(where).data[key] - } - - 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') - } - - 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')) - } - - loadHome () { - if (this.env.HOME) { - return this.home = this.env.HOME - } - this.home = homedir() - } - - 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) - } - } - } - - 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') - } - - 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 valid () { - for (const [where, { valid }] of this.data.entries()) { - if (valid === false || valid === null && !this.validate(where)) { - return false - } - } - return true - } - - 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] - } - } - - // 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 = [] - } - } - } - - 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) - } - } - } - - // 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 - } - } - } - - 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) - } - - if (this.sources.has(source)) { - const m = `double-loading config "${source}" as "${where}", ` + - `previously loaded as "${this.sources.get(source)}"` - throw new Error(m) - } - - 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) - } - } 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 - } - } - } - - [_checkDeprecated] (key, where, obj, kv) { - // XXX(npm9+) make this throw an error - if (this.deprecated[key]) { - log.warn('config', key, this.deprecated[key]) - } - } - - // Parse a field, coercing it to the best type available. - parseField (f, key, listElement = false) { - return parseField(f, key, this, listElement) - } - - 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) - } - - loadBuiltinConfig () { - return this[_loadFile](resolve(this.npmPath, 'npmrc'), 'builtin') - } - - async loadProjectConfig () { - // 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() - - 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') - return - } - - 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') - } - } - - async loadLocalPrefix () { - const cliPrefix = this[_get]('prefix', 'cli') - if (cliPrefix) { - this.localPrefix = cliPrefix - return - } - - const cliWorkspaces = this[_get]('workspaces', 'cli') - const isGlobal = this[_get]('global') || this[_get]('location') === 'global' - - for (const p of walkUp(this.cwd)) { - const hasNodeModules = await stat(resolve(p, 'node_modules')) - .then((st) => st.isDirectory()) - .catch(() => false) - - const hasPackageJson = await stat(resolve(p, 'package.json')) - .then((st) => st.isFile()) - .catch(() => false) - - if (!this.localPrefix && (hasNodeModules || hasPackageJson)) { - this.localPrefix = p - - // if workspaces are disabled, or we're in global mode, return now - if (cliWorkspaces === false || isGlobal) { - return - } - - // otherwise, continue the loop - continue - } - - 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 - 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 - const hasNpmrc = await stat(resolve(this.localPrefix, '.npmrc')) - .then((st) => st.isFile()) - .catch(() => false) - - if (hasNpmrc) { - 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 - log.info(`found workspace root at ${this.localPrefix}`) - // we found a root, so we return now - return - } - } - } - } - - if (!this.localPrefix) { - this.localPrefix = this.cwd - } - } - - loadUserConfig () { - return this[_loadFile](this[_get]('userconfig'), 'user') - } - - loadGlobalConfig () { - return this[_loadFile](this[_get]('globalconfig'), 'global') - } - - async save (where) { - if (!this.loaded) { - throw new Error('call config.load() before saving') - } - if (!confFileTypes.has(where)) { - throw new Error('invalid config location param: ' + where) - } - - const conf = this.data.get(where) - conf[_raw] = { ...conf.data } - conf[_loadError] = null - - if (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') - } - } - - const iniData = ini.stringify(conf.data).trim() + '\n' - if (!iniData.trim()) { - // ignore the unlink error (eg, if file doesn't exist) - await unlink(conf.source).catch(er => {}) - 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') - } - 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 = {} - - // 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') - } - 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 - } - - // 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] - } - - get valid () { - return this[_valid] - } - - set source (s) { - if (this[_source]) { - throw new Error('cannot set ConfigData source more than once') - } - 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') - } - 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') - } - this[_raw] = r - } - - get raw () { - return this[_raw] - } -} - -module.exports = Config diff --git a/node_modules/@npmcli/config/lib/nerf-dart.js b/node_modules/@npmcli/config/lib/nerf-dart.js deleted file mode 100644 index d6ae4aa2a..000000000 --- a/node_modules/@npmcli/config/lib/nerf-dart.js +++ /dev/null @@ -1,18 +0,0 @@ -const { URL } = require('url') - -/** - * Maps a URL to an identifier. - * - * Name courtesy schiffertronix media LLC, a New Jersey corporation - * - * @param {String} uri The URL to be nerfed. - * - * @returns {String} A nerfed URL. - */ -module.exports = (url) => { - const parsed = new URL(url) - const from = `${parsed.protocol}//${parsed.host}${parsed.pathname}` - const rel = new URL('.', from) - const res = `//${rel.host}${rel.pathname}` - return res -} diff --git a/node_modules/@npmcli/config/lib/parse-field.js b/node_modules/@npmcli/config/lib/parse-field.js deleted file mode 100644 index 0c905bf23..000000000 --- a/node_modules/@npmcli/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/node_modules/@npmcli/config/lib/set-envs.js b/node_modules/@npmcli/config/lib/set-envs.js deleted file mode 100644 index 0f5781aaf..000000000 --- a/node_modules/@npmcli/config/lib/set-envs.js +++ /dev/null @@ -1,111 +0,0 @@ -// 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 - -// 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()}` -} - -const envVal = val => Array.isArray(val) ? val.map(v => envVal(v)).join('\n\n') - : val === null || val === undefined || val === false ? '' - : 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 - } - - for (let i = 0; i < def.length; i++) { - /* istanbul ignore next - there are no array configs where the default - * is not an empty array, so this loop is a no-op, but it's the correct - * thing to do if we ever DO add a config like that. */ - if (def[i] !== val[i]) { - return false - } - } - return true -} - -const setEnv = (env, rawKey, rawVal) => { - const val = envVal(rawVal) - const key = envKey(rawKey, val) - if (key && val !== null) { - env[key] = val - } -} - -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 - } - - // 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 - } - env.NODE = env.npm_node_execpath = config.execPath -} - -module.exports = setEnvs diff --git a/node_modules/@npmcli/config/lib/type-defs.js b/node_modules/@npmcli/config/lib/type-defs.js deleted file mode 100644 index 20a827c3d..000000000 --- a/node_modules/@npmcli/config/lib/type-defs.js +++ /dev/null @@ -1,59 +0,0 @@ -const nopt = require('nopt') - -const { Umask, validate: validateUmask } = require('./umask.js') - -const semver = require('semver') -const validateSemver = (data, k, val) => { - const valid = semver.valid(val) - if (!valid) { - return false - } - data[k] = valid -} - -const noptValidatePath = nopt.typeDefs.path.validate -const validatePath = (data, k, val) => { - if (typeof val !== 'string') { - return false - } - return noptValidatePath(data, k, val) -} - -// add descriptions so we can validate more usefully -module.exports = { - ...nopt.typeDefs, - semver: { - type: semver, - validate: validateSemver, - description: 'full valid SemVer string', - }, - Umask: { - type: Umask, - validate: validateUmask, - description: 'octal number in range 0o000..0o777 (0..511)', - }, - url: { - ...nopt.typeDefs.url, - description: 'full url with "http://"', - }, - path: { - ...nopt.typeDefs.path, - validate: validatePath, - description: 'valid filesystem path', - }, - Number: { - ...nopt.typeDefs.Number, - description: 'numeric value', - }, - Boolean: { - ...nopt.typeDefs.Boolean, - description: 'boolean value (true or false)', - }, - Date: { - ...nopt.typeDefs.Date, - description: 'valid Date string', - }, -} - -// TODO: make nopt less of a global beast so this kludge isn't necessary -nopt.typeDefs = module.exports diff --git a/node_modules/@npmcli/config/lib/type-description.js b/node_modules/@npmcli/config/lib/type-description.js deleted file mode 100644 index f5e0d164f..000000000 --- a/node_modules/@npmcli/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/node_modules/@npmcli/config/lib/umask.js b/node_modules/@npmcli/config/lib/umask.js deleted file mode 100644 index 195fad238..000000000 --- a/node_modules/@npmcli/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/node_modules/@npmcli/config/package.json b/node_modules/@npmcli/config/package.json deleted file mode 100644 index 3293ffe5c..000000000 --- a/node_modules/@npmcli/config/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "@npmcli/config", - "version": "6.0.1", - "files": [ - "bin/", - "lib/" - ], - "main": "lib/index.js", - "description": "Configuration management for the npm cli", - "repository": { - "type": "git", - "url": "https://github.com/npm/config.git" - }, - "author": "GitHub Inc.", - "license": "ISC", - "scripts": { - "test": "tap", - "snap": "tap", - "lint": "eslint \"**/*.js\"", - "postlint": "template-oss-check", - "lintfix": "npm run lint -- --fix", - "posttest": "npm run lint", - "template-oss-apply": "template-oss-apply --force" - }, - "tap": { - "check-coverage": true, - "coverage-map": "map.js", - "nyc-arg": [ - "--exclude", - "tap-snapshots/**" - ] - }, - "devDependencies": { - "@npmcli/eslint-config": "^4.0.0", - "@npmcli/template-oss": "4.5.1", - "tap": "^16.0.1" - }, - "dependencies": { - "@npmcli/map-workspaces": "^3.0.0", - "ini": "^3.0.0", - "nopt": "^6.0.0", - "proc-log": "^3.0.0", - "read-package-json-fast": "^3.0.0", - "semver": "^7.3.5", - "walk-up-path": "^1.0.0" - }, - "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.5.1" - } -} diff --git a/package-lock.json b/package-lock.json index b28986fb3..2b468079d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2052,22 +2052,8 @@ } }, "node_modules/@npmcli/config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/config/-/config-6.0.1.tgz", - "integrity": "sha512-f8PGjhM7kKbMfEMmE8n1dW+m/7XFuvatLXqItO89ZKJwYl9Zs5d7CmsIe8n8i+4YmGYL3HqR26/mVb4oK2b6Zw==", - "inBundle": true, - "dependencies": { - "@npmcli/map-workspaces": "^3.0.0", - "ini": "^3.0.0", - "nopt": "^6.0.0", - "proc-log": "^3.0.0", - "read-package-json-fast": "^3.0.0", - "semver": "^7.3.5", - "walk-up-path": "^1.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } + "resolved": "workspaces/config", + "link": true }, "node_modules/@npmcli/disparity-colors": { "version": "3.0.0", @@ -13488,7 +13474,6 @@ }, "node_modules/walk-up-path": { "version": "1.0.0", - "inBundle": true, "license": "ISC" }, "node_modules/wcwidth": { @@ -13925,6 +13910,28 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "workspaces/config": { + "name": "@npmcli/config", + "version": "6.0.1", + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^3.0.0", + "ini": "^3.0.0", + "nopt": "^6.0.0", + "proc-log": "^3.0.0", + "read-package-json-fast": "^3.0.0", + "semver": "^7.3.5", + "walk-up-path": "^1.0.0" + }, + "devDependencies": { + "@npmcli/eslint-config": "^4.0.0", + "@npmcli/template-oss": "4.8.0", + "tap": "^16.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "workspaces/libnpmaccess": { "version": "7.0.0", "license": "ISC", diff --git a/release-please-config.json b/release-please-config.json index f5cc46284..389df4270 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -69,7 +69,8 @@ }, "workspaces/libnpmversion": { "prerelease": false - } + }, + "workspaces/config": {} }, "exclude-packages-from-root": true, "group-pull-request-title-pattern": "chore: release ${version}", diff --git a/workspaces/config/.eslintrc.js b/workspaces/config/.eslintrc.js new file mode 100644 index 000000000..5db9f8155 --- /dev/null +++ b/workspaces/config/.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/workspaces/config/.gitignore b/workspaces/config/.gitignore new file mode 100644 index 000000000..79af2bfca --- /dev/null +++ b/workspaces/config/.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/workspaces/config/CHANGELOG.md b/workspaces/config/CHANGELOG.md new file mode 100644 index 000000000..b3b30f863 --- /dev/null +++ b/workspaces/config/CHANGELOG.md @@ -0,0 +1,133 @@ +# Changelog + +## [6.0.1](https://github.com/npm/config/compare/v6.0.0...v6.0.1) (2022-10-17) + +### Dependencies + +* [`dca20cc`](https://github.com/npm/config/commit/dca20cc00c0cbebd9d1a1cf1962e32e99057ea8e) [#99](https://github.com/npm/config/pull/99) bump @npmcli/map-workspaces from 2.0.4 to 3.0.0 +* [`fc42456`](https://github.com/npm/config/commit/fc424565014cc155e902940221b6283cbb40faf4) [#100](https://github.com/npm/config/pull/100) bump proc-log from 2.0.1 to 3.0.0 + +## [6.0.0](https://github.com/npm/config/compare/v5.0.0...v6.0.0) (2022-10-13) + +### ⚠️ BREAKING CHANGES + +* this module no longer attempts to change file ownership automatically + +### Features + +* [`805535f`](https://github.com/npm/config/commit/805535ff6b7255a3a2fb5e7da392f53b1c2f3c04) [#96](https://github.com/npm/config/pull/96) do not alter file ownership (#96) (@nlf) + +### Dependencies + +* [`c62c19c`](https://github.com/npm/config/commit/c62c19cffc65a8b6e89cbd071bd7578f246312a9) [#95](https://github.com/npm/config/pull/95) bump read-package-json-fast from 2.0.3 to 3.0.0 + +## [5.0.0](https://github.com/npm/config/compare/v4.2.2...v5.0.0) (2022-10-06) + +### ⚠️ BREAKING CHANGES + +* unscoped auth configuration is no longer automatically scoped to a registry. the `validate` method is no longer called automatically. the `_auth` configuration key is no longer split into `username` and `_password`. errors will be thrown by `validate()` if problems are found. +* `@npmcli/config` is now compatible with the following semver range for node: `^14.17.0 || ^16.13.0 || >=18.0.0` + +### Features + +* [`344ccd3`](https://github.com/npm/config/commit/344ccd3d07979d0cb36dad8a7fe2e9cbbdbdbc9e) [#92](https://github.com/npm/config/pull/92) throw errors for invalid auth configuration (#92) (@nlf) +* [`aa25682`](https://github.com/npm/config/commit/aa256827d76ec9b1aea06eb3ebdd033067a5e604) [#87](https://github.com/npm/config/pull/87) postinstall for dependabot template-oss PR (@lukekarrys) + +## [4.2.2](https://github.com/npm/config/compare/v4.2.1...v4.2.2) (2022-08-25) + + +### Bug Fixes + +* warn on bare auth related configs ([#78](https://github.com/npm/config/issues/78)) ([d4e582a](https://github.com/npm/config/commit/d4e582ab7d8d9f4a8615619bb7d3263df5de66e6)) + +## [4.2.1](https://github.com/npm/config/compare/v4.2.0...v4.2.1) (2022-08-09) + + +### Bug Fixes + +* correctly handle nerf-darted env vars ([#74](https://github.com/npm/config/issues/74)) ([71f559b](https://github.com/npm/config/commit/71f559b08e01616b53f61e1cf385fc44162e2d66)) +* linting ([#75](https://github.com/npm/config/issues/75)) ([deb1001](https://github.com/npm/config/commit/deb10011d1b5e3df84b7d13284ea55b07dd62b63)) + + +### Dependencies + +* bump nopt from 5.0.0 to 6.0.0 ([#72](https://github.com/npm/config/issues/72)) ([d825726](https://github.com/npm/config/commit/d825726049644f5bbe0edf27b5600cc60ae14ee5)) + +## [4.2.0](https://github.com/npm/config/compare/v4.1.0...v4.2.0) (2022-07-18) + + +### Features + +* detect registry-scoped certfile and keyfile options ([#69](https://github.com/npm/config/issues/69)) ([e58a4f1](https://github.com/npm/config/commit/e58a4f18f0ec0820fe57ccaff34c4135ece12558)) + +## [4.1.0](https://github.com/npm/config/compare/v4.0.2...v4.1.0) (2022-04-13) + + +### Features + +* warn on deprecated config ([#62](https://github.com/npm/config/issues/62)) ([190065e](https://github.com/npm/config/commit/190065ef53d39a1e09486639c710dabdd73d8a7c)) + +### [4.0.2](https://github.com/npm/config/compare/v4.0.1...v4.0.2) (2022-04-05) + + +### Bug Fixes + +* replace deprecated String.prototype.substr() ([#59](https://github.com/npm/config/issues/59)) ([43893b6](https://github.com/npm/config/commit/43893b638f82ade945cba27fe9e483b32eea99ae)) + + +### Dependencies + +* bump ini from 2.0.0 to 3.0.0 ([#60](https://github.com/npm/config/issues/60)) ([965e2a4](https://github.com/npm/config/commit/965e2a40c7649ffd6e84fb83823a2b751bcda294)) +* update @npmcli/map-workspaces requirement from ^2.0.1 to ^2.0.2 ([#49](https://github.com/npm/config/issues/49)) ([9a0f182](https://github.com/npm/config/commit/9a0f182c4fa46dadccc631a244678a3c469ad63a)) + +### [4.0.1](https://www.github.com/npm/config/compare/v4.0.0...v4.0.1) (2022-03-02) + + +### Bug Fixes + +* skip workspace detection when in global mode ([#47](https://www.github.com/npm/config/issues/47)) ([bedff61](https://www.github.com/npm/config/commit/bedff61c6f074f21c1586afe391dc2cb6e821619)) + + +### Dependencies + +* update @npmcli/map-workspaces requirement from ^2.0.0 to ^2.0.1 ([#43](https://www.github.com/npm/config/issues/43)) ([c397ab8](https://www.github.com/npm/config/commit/c397ab88c459fc477ae9094ec0ee0b571e6bb8ed)) + +## [4.0.0](https://www.github.com/npm/config/compare/v3.0.1...v4.0.0) (2022-02-14) + + +### ⚠ BREAKING CHANGES + +* drop support for the `log` option + +### Features + +* remove `log` option ([#40](https://www.github.com/npm/config/issues/40)) ([bbf5128](https://www.github.com/npm/config/commit/bbf512818f30d0764e3951449c8f07856d70991e)) + + +### Bug Fixes + +* correct a polynomial regex ([#39](https://www.github.com/npm/config/issues/39)) ([9af098f](https://www.github.com/npm/config/commit/9af098fb874c1a8122ab7a5e009235a1f7df72f5)) + +### [3.0.1](https://www.github.com/npm/config/compare/v3.0.0...v3.0.1) (2022-02-10) + + +### Dependencies + +* update semver requirement from ^7.3.4 to ^7.3.5 ([2cb225a](https://www.github.com/npm/config/commit/2cb225a907180a3b569c8c9baf23da1a989a2f1f)) +* use proc-log instead of process.emit ([fd4cd42](https://www.github.com/npm/config/commit/fd4cd429ef875ce68aa0be9bba329cae4e7adfe3)) + +## [3.0.0](https://www.github.com/npm/config/compare/v2.4.0...v3.0.0) (2022-02-01) + + +### ⚠ BREAKING CHANGES + +* this drops support for node10 and non-LTS versions of node12 and node14 + +### Features + +* automatically detect workspace roots ([#28](https://www.github.com/npm/config/issues/28)) ([a3dc623](https://www.github.com/npm/config/commit/a3dc6234d57c7c80c66a8c33e17cf1d97f86f8d9)) + + +### Bug Fixes + +* template-oss ([#29](https://www.github.com/npm/config/issues/29)) ([6440fba](https://www.github.com/npm/config/commit/6440fba6e04b1f87e57b4c2ccc5ea84d8a69b823)) diff --git a/workspaces/config/LICENSE b/workspaces/config/LICENSE new file mode 100644 index 000000000..19cec97b1 --- /dev/null +++ b/workspaces/config/LICENSE @@ -0,0 +1,15 @@ +The ISC License + +Copyright (c) npm, Inc. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/workspaces/config/README.md b/workspaces/config/README.md new file mode 100644 index 000000000..32418381a --- /dev/null +++ b/workspaces/config/README.md @@ -0,0 +1,260 @@ +# `@npmcli/config` + +Configuration management for the npm cli. + +This module is the spiritual descendant of +[`npmconf`](http://npm.im/npmconf), and the code that once lived in npm's +`lib/config/` folder. + +It does the management of configuration files that npm uses, but +importantly, does _not_ define all the configuration defaults or types, as +those parts make more sense to live within the npm CLI itself. + +The only exceptions: + +- The `prefix` config value has some special semantics, setting the local + prefix if specified on the CLI options and not in global mode, or the + global prefix otherwise. +- The `project` config file is loaded based on the local prefix (which can + only be set by the CLI config options, and otherwise defaults to a walk + up the folder tree to the first parent containing a `node_modules` + folder, `package.json` file, or `package-lock.json` file.) +- The `userconfig` value, as set by the environment and CLI (defaulting to + `~/.npmrc`, is used to load user configs. +- The `globalconfig` value, as set by the environment, CLI, and + `userconfig` file (defaulting to `$PREFIX/etc/npmrc`) is used to load + global configs. +- A `builtin` config, read from a `npmrc` file in the root of the npm + project itself, overrides all defaults. + +The resulting hierarchy of configs: + +- CLI switches. eg `--some-key=some-value` on the command line. These are + parsed by [`nopt`](http://npm.im/nopt), which is not a great choice, but + it's the one that npm has used forever, and changing it will be + difficult. +- Environment variables. eg `npm_config_some_key=some_value` in the + environment. There is no way at this time to modify this prefix. +- INI-formatted project configs. eg `some-key = some-value` in the + `localPrefix` folder (ie, the `cwd`, or its nearest parent that contains + either a `node_modules` folder or `package.json` file.) +- INI-formatted userconfig file. eg `some-key = some-value` in `~/.npmrc`. + The `userconfig` config value can be overridden by the `cli`, `env`, or + `project` configs to change this value. +- INI-formatted globalconfig file. eg `some-key = some-value` in + the `globalPrefix` folder, which is inferred by looking at the location + of the node executable, or the `prefix` setting in the `cli`, `env`, + `project`, or `userconfig`. The `globalconfig` value at any of those + levels can override this. +- INI-formatted builtin config file. eg `some-key = some-value` in + `/usr/local/lib/node_modules/npm/npmrc`. This is not configurable, and + is determined by looking in the `npmPath` folder. +- Default values (passed in by npm when it loads this module). + +## USAGE + +```js +const Config = require('@npmcli/config') +// the types of all the configs we know about +const types = require('./config/types.js') +// default values for all the configs we know about +const defaults = require('./config/defaults.js') +// if you want -c to be short for --call and so on, define it here +const shorthands = require('./config/shorthands.js') + +const conf = new Config({ + // path to the npm module being run + npmPath: resolve(__dirname, '..'), + types, + shorthands, + defaults, + // optional, defaults to process.argv + argv: process.argv, + // optional, defaults to process.env + env: process.env, + // optional, defaults to process.execPath + execPath: process.execPath, + // optional, defaults to process.platform + platform: process.platform, + // optional, defaults to process.cwd() + cwd: process.cwd(), +}) + +// emits log events on the process object +// see `proc-log` for more info +process.on('log', (level, ...args) => { + console.log(level, ...args) +}) + +// returns a promise that fails if config loading fails, and +// resolves when the config object is ready for action +conf.load().then(() => { + conf.validate() + console.log('loaded ok! some-key = ' + conf.get('some-key')) +}).catch(er => { + console.error('error loading configs!', er) +}) +``` + +## API + +The `Config` class is the sole export. + +```js +const Config = require('@npmcli/config') +``` + +### static `Config.typeDefs` + +The type definitions passed to `nopt` for CLI option parsing and known +configuration validation. + +### constructor `new Config(options)` + +Options: + +- `types` Types of all known config values. Note that some are effectively + given semantic value in the config loading process itself. +- `shorthands` An object mapping a shorthand value to an array of CLI + arguments that replace it. +- `defaults` Default values for each of the known configuration keys. + These should be defined for all configs given a type, and must be valid. +- `npmPath` The path to the `npm` module, for loading the `builtin` config + file. +- `cwd` Optional, defaults to `process.cwd()`, used for inferring the + `localPrefix` and loading the `project` config. +- `platform` Optional, defaults to `process.platform`. Used when inferring + the `globalPrefix` from the `execPath`, since this is done diferently on + Windows. +- `execPath` Optional, defaults to `process.execPath`. Used to infer the + `globalPrefix`. +- `env` Optional, defaults to `process.env`. Source of the environment + variables for configuration. +- `argv` Optional, defaults to `process.argv`. Source of the CLI options + used for configuration. + +Returns a `config` object, which is not yet loaded. + +Fields: + +- `config.globalPrefix` The prefix for `global` operations. Set by the + `prefix` config value, or defaults based on the location of the + `execPath` option. +- `config.localPrefix` The prefix for `local` operations. Set by the + `prefix` config value on the CLI only, or defaults to either the `cwd` or + its nearest ancestor containing a `node_modules` folder or `package.json` + file. +- `config.sources` A read-only `Map` of the file (or a comment, if no file + found, or relevant) to the config level loaded from that source. +- `config.data` A `Map` of config level to `ConfigData` objects. These + objects should not be modified directly under any circumstances. + - `source` The source where this data was loaded from. + - `raw` The raw data used to generate this config data, as it was parsed + initially from the environment, config file, or CLI options. + - `data` The data object reflecting the inheritance of configs up to this + point in the chain. + - `loadError` Any errors encountered that prevented the loading of this + config data. +- `config.list` A list sorted in priority of all the config data objects in + the prototype chain. `config.list[0]` is the `cli` level, + `config.list[1]` is the `env` level, and so on. +- `cwd` The `cwd` param +- `env` The `env` param +- `argv` The `argv` param +- `execPath` The `execPath` param +- `platform` The `platform` param +- `defaults` The `defaults` param +- `shorthands` The `shorthands` param +- `types` The `types` param +- `npmPath` The `npmPath` param +- `globalPrefix` The effective `globalPrefix` +- `localPrefix` The effective `localPrefix` +- `prefix` If `config.get('global')` is true, then `globalPrefix`, + otherwise `localPrefix` +- `home` The user's home directory, found by looking at `env.HOME` or + calling `os.homedir()`. +- `loaded` A boolean indicating whether or not configs are loaded +- `valid` A getter that returns `true` if all the config objects are valid. + Any data objects that have been modified with `config.set(...)` will be + re-evaluated when `config.valid` is read. + +### `config.load()` + +Load configuration from the various sources of information. + +Returns a `Promise` that resolves when configuration is loaded, and fails +if a fatal error is encountered. + +### `config.find(key)` + +Find the effective place in the configuration levels a given key is set. +Returns one of: `cli`, `env`, `project`, `user`, `global`, `builtin`, or +`default`. + +Returns `null` if the key is not set. + +### `config.get(key, where = 'cli')` + +Load the given key from the config stack. + +### `config.set(key, value, where = 'cli')` + +Set the key to the specified value, at the specified level in the config +stack. + +### `config.delete(key, where = 'cli')` + +Delete the configuration key from the specified level in the config stack. + +### `config.validate(where)` + +Verify that all known configuration options are set to valid values, and +log a warning if they are invalid. + +Invalid auth options will cause this method to throw an error with a `code` +property of `ERR_INVALID_AUTH`, and a `problems` property listing the specific +concerns with the current configuration. + +If `where` is not set, then all config objects are validated. + +Returns `true` if all configs are valid. + +Note that it's usually enough (and more efficient) to just check +`config.valid`, since each data object is marked for re-evaluation on every +`config.set()` operation. + +### `config.repair(problems)` + +Accept an optional array of problems (as thrown by `config.validate()`) and +perform the necessary steps to resolve them. If no problems are provided, +this method will call `config.validate()` internally to retrieve them. + +Note that you must `await config.save('user')` in order to persist the changes. + +### `config.isDefault(key)` + +Returns `true` if the value is coming directly from the +default definitions, if the current value for the key config is +coming from any other source, returns `false`. + +This method can be used for avoiding or tweaking default values, e.g: + +> Given a global default definition of foo='foo' it's possible to read that +> value such as: +> +> ```js +> const save = config.get('foo') +> ``` +> +> Now in a different place of your app it's possible to avoid using the `foo` +> default value, by checking to see if the current config value is currently +> one that was defined by the default definitions: +> +> ```js +> const save = config.isDefault('foo') ? 'bar' : config.get('foo') +> ``` + +### `config.save(where)` + +Save the config file specified by the `where` param. Must be one of +`project`, `user`, `global`, `builtin`. diff --git a/workspaces/config/lib/env-replace.js b/workspaces/config/lib/env-replace.js new file mode 100644 index 000000000..c851f6e4d --- /dev/null +++ b/workspaces/config/lib/env-replace.js @@ -0,0 +1,14 @@ +// 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/errors.js b/workspaces/config/lib/errors.js new file mode 100644 index 000000000..fa3e20798 --- /dev/null +++ b/workspaces/config/lib/errors.js @@ -0,0 +1,22 @@ +'use strict' + +class ErrInvalidAuth extends Error { + constructor (problems) { + let message = 'Invalid auth configuration found: ' + message += 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) + this.code = 'ERR_INVALID_AUTH' + this.problems = problems + } +} + +module.exports = { + ErrInvalidAuth, +} diff --git a/workspaces/config/lib/index.js b/workspaces/config/lib/index.js new file mode 100644 index 000000000..e1d47ffcd --- /dev/null +++ b/workspaces/config/lib/index.js @@ -0,0 +1,917 @@ +// 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 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 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') + +class Config { + static get typeDefs () { + return typeDefs + } + + 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(), + }) { + // 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 + types[key] = def.type + if (def.deprecated) { + this.deprecated[key] = def.deprecated.trim().replace(/\n +/, '\n') + } + } + + // 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 + + // set when we load configs + this.globalPrefix = null + this.localPrefix = 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 + } + + get loaded () { + return this[_loaded] + } + + get prefix () { + return this[_get]('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) + } + + [_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 (key, where) { + if (!this.loaded) { + throw new Error('call config.load() before reading values') + } + return this[_get](key, where) + } + + // 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 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] + } + + 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) + } + delete this.data.get(where).data[key] + } + + 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') + } + + 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')) + } + + loadHome () { + if (this.env.HOME) { + return this.home = this.env.HOME + } + this.home = homedir() + } + + 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) + } + } + } + + 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') + } + + 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 valid () { + for (const [where, { valid }] of this.data.entries()) { + if (valid === false || valid === null && !this.validate(where)) { + return false + } + } + return true + } + + 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] + } + } + + // 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 = [] + } + } + } + + 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) + } + } + } + + // 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 + } + } + } + + 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) + } + + if (this.sources.has(source)) { + const m = `double-loading config "${source}" as "${where}", ` + + `previously loaded as "${this.sources.get(source)}"` + throw new Error(m) + } + + 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) + } + } 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 + } + } + } + + [_checkDeprecated] (key, where, obj, kv) { + // XXX(npm9+) make this throw an error + if (this.deprecated[key]) { + log.warn('config', key, this.deprecated[key]) + } + } + + // Parse a field, coercing it to the best type available. + parseField (f, key, listElement = false) { + return parseField(f, key, this, listElement) + } + + 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) + } + + loadBuiltinConfig () { + return this[_loadFile](resolve(this.npmPath, 'npmrc'), 'builtin') + } + + async loadProjectConfig () { + // 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() + + 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') + return + } + + 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') + } + } + + async loadLocalPrefix () { + const cliPrefix = this[_get]('prefix', 'cli') + if (cliPrefix) { + this.localPrefix = cliPrefix + return + } + + const cliWorkspaces = this[_get]('workspaces', 'cli') + const isGlobal = this[_get]('global') || this[_get]('location') === 'global' + + for (const p of walkUp(this.cwd)) { + const hasNodeModules = await stat(resolve(p, 'node_modules')) + .then((st) => st.isDirectory()) + .catch(() => false) + + const hasPackageJson = await stat(resolve(p, 'package.json')) + .then((st) => st.isFile()) + .catch(() => false) + + if (!this.localPrefix && (hasNodeModules || hasPackageJson)) { + this.localPrefix = p + + // if workspaces are disabled, or we're in global mode, return now + if (cliWorkspaces === false || isGlobal) { + return + } + + // otherwise, continue the loop + continue + } + + 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 + 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 + const hasNpmrc = await stat(resolve(this.localPrefix, '.npmrc')) + .then((st) => st.isFile()) + .catch(() => false) + + if (hasNpmrc) { + 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 + log.info(`found workspace root at ${this.localPrefix}`) + // we found a root, so we return now + return + } + } + } + } + + if (!this.localPrefix) { + this.localPrefix = this.cwd + } + } + + loadUserConfig () { + return this[_loadFile](this[_get]('userconfig'), 'user') + } + + loadGlobalConfig () { + return this[_loadFile](this[_get]('globalconfig'), 'global') + } + + async save (where) { + if (!this.loaded) { + throw new Error('call config.load() before saving') + } + if (!confFileTypes.has(where)) { + throw new Error('invalid config location param: ' + where) + } + + const conf = this.data.get(where) + conf[_raw] = { ...conf.data } + conf[_loadError] = null + + if (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') + } + } + + const iniData = ini.stringify(conf.data).trim() + '\n' + if (!iniData.trim()) { + // ignore the unlink error (eg, if file doesn't exist) + await unlink(conf.source).catch(er => {}) + 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') + } + 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 = {} + + // 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') + } + 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 + } + + // 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] + } + + get valid () { + return this[_valid] + } + + set source (s) { + if (this[_source]) { + throw new Error('cannot set ConfigData source more than once') + } + 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') + } + 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') + } + this[_raw] = r + } + + get raw () { + return this[_raw] + } +} + +module.exports = Config diff --git a/workspaces/config/lib/nerf-dart.js b/workspaces/config/lib/nerf-dart.js new file mode 100644 index 000000000..d6ae4aa2a --- /dev/null +++ b/workspaces/config/lib/nerf-dart.js @@ -0,0 +1,18 @@ +const { URL } = require('url') + +/** + * Maps a URL to an identifier. + * + * Name courtesy schiffertronix media LLC, a New Jersey corporation + * + * @param {String} uri The URL to be nerfed. + * + * @returns {String} A nerfed URL. + */ +module.exports = (url) => { + const parsed = new URL(url) + const from = `${parsed.protocol}//${parsed.host}${parsed.pathname}` + const rel = new URL('.', from) + const res = `//${rel.host}${rel.pathname}` + return res +} diff --git a/workspaces/config/lib/parse-field.js b/workspaces/config/lib/parse-field.js new file mode 100644 index 000000000..0c905bf23 --- /dev/null +++ b/workspaces/config/lib/parse-field.js @@ -0,0 +1,81 @@ +// 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 new file mode 100644 index 000000000..0f5781aaf --- /dev/null +++ b/workspaces/config/lib/set-envs.js @@ -0,0 +1,111 @@ +// 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 + +// 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()}` +} + +const envVal = val => Array.isArray(val) ? val.map(v => envVal(v)).join('\n\n') + : val === null || val === undefined || val === false ? '' + : 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 + } + + for (let i = 0; i < def.length; i++) { + /* istanbul ignore next - there are no array configs where the default + * is not an empty array, so this loop is a no-op, but it's the correct + * thing to do if we ever DO add a config like that. */ + if (def[i] !== val[i]) { + return false + } + } + return true +} + +const setEnv = (env, rawKey, rawVal) => { + const val = envVal(rawVal) + const key = envKey(rawKey, val) + if (key && val !== null) { + env[key] = val + } +} + +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 + } + + // 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 + } + env.NODE = env.npm_node_execpath = config.execPath +} + +module.exports = setEnvs diff --git a/workspaces/config/lib/type-defs.js b/workspaces/config/lib/type-defs.js new file mode 100644 index 000000000..20a827c3d --- /dev/null +++ b/workspaces/config/lib/type-defs.js @@ -0,0 +1,59 @@ +const nopt = require('nopt') + +const { Umask, validate: validateUmask } = require('./umask.js') + +const semver = require('semver') +const validateSemver = (data, k, val) => { + const valid = semver.valid(val) + if (!valid) { + return false + } + data[k] = valid +} + +const noptValidatePath = nopt.typeDefs.path.validate +const validatePath = (data, k, val) => { + if (typeof val !== 'string') { + return false + } + return noptValidatePath(data, k, val) +} + +// add descriptions so we can validate more usefully +module.exports = { + ...nopt.typeDefs, + semver: { + type: semver, + validate: validateSemver, + description: 'full valid SemVer string', + }, + Umask: { + type: Umask, + validate: validateUmask, + description: 'octal number in range 0o000..0o777 (0..511)', + }, + url: { + ...nopt.typeDefs.url, + description: 'full url with "http://"', + }, + path: { + ...nopt.typeDefs.path, + validate: validatePath, + description: 'valid filesystem path', + }, + Number: { + ...nopt.typeDefs.Number, + description: 'numeric value', + }, + Boolean: { + ...nopt.typeDefs.Boolean, + description: 'boolean value (true or false)', + }, + Date: { + ...nopt.typeDefs.Date, + description: 'valid Date string', + }, +} + +// TODO: make nopt less of a global beast so this kludge isn't necessary +nopt.typeDefs = module.exports diff --git a/workspaces/config/lib/type-description.js b/workspaces/config/lib/type-description.js new file mode 100644 index 000000000..f5e0d164f --- /dev/null +++ b/workspaces/config/lib/type-description.js @@ -0,0 +1,21 @@ +// 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 new file mode 100644 index 000000000..195fad238 --- /dev/null +++ b/workspaces/config/lib/umask.js @@ -0,0 +1,31 @@ +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/map.js b/workspaces/config/map.js new file mode 100644 index 000000000..0b263fbec --- /dev/null +++ b/workspaces/config/map.js @@ -0,0 +1 @@ +module.exports = t => t.replace(/^test/, 'lib') diff --git a/workspaces/config/package.json b/workspaces/config/package.json new file mode 100644 index 000000000..b9f41d29b --- /dev/null +++ b/workspaces/config/package.json @@ -0,0 +1,55 @@ +{ + "name": "@npmcli/config", + "version": "6.0.1", + "files": [ + "bin/", + "lib/" + ], + "main": "lib/index.js", + "description": "Configuration management for the npm cli", + "repository": { + "type": "git", + "url": "https://github.com/npm/cli.git", + "directory": "workspaces/config" + }, + "author": "GitHub Inc.", + "license": "ISC", + "scripts": { + "test": "tap", + "snap": "tap", + "lint": "eslint \"**/*.js\"", + "postlint": "template-oss-check", + "lintfix": "node ../.. run lint -- --fix", + "posttest": "node ../.. run lint", + "template-oss-apply": "template-oss-apply --force" + }, + "tap": { + "check-coverage": true, + "coverage-map": "map.js", + "nyc-arg": [ + "--exclude", + "tap-snapshots/**" + ] + }, + "devDependencies": { + "@npmcli/eslint-config": "^4.0.0", + "@npmcli/template-oss": "4.8.0", + "tap": "^16.0.1" + }, + "dependencies": { + "@npmcli/map-workspaces": "^3.0.0", + "ini": "^3.0.0", + "nopt": "^6.0.0", + "proc-log": "^3.0.0", + "read-package-json-fast": "^3.0.0", + "semver": "^7.3.5", + "walk-up-path": "^1.0.0" + }, + "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.8.0" + } +} diff --git a/workspaces/config/scripts/example.js b/workspaces/config/scripts/example.js new file mode 100644 index 000000000..bbb2992a5 --- /dev/null +++ b/workspaces/config/scripts/example.js @@ -0,0 +1,43 @@ +const Config = require('../') + +const shorthands = require('../test/fixtures/shorthands.js') +const types = require('../test/fixtures/types.js') +const defaults = require('../test/fixtures/defaults.js') + +const npmPath = __dirname + +const timers = {} +process.on('time', k => { + if (timers[k]) { + throw new Error('duplicate timer: ' + k) + } + timers[k] = process.hrtime() +}) +process.on('timeEnd', k => { + if (!timers[k]) { + throw new Error('ending unstarted timer: ' + k) + } + const dur = process.hrtime(timers[k]) + delete timers[k] + console.error(`\x1B[2m${k}\x1B[22m`, Math.round(dur[0] * 1e6 + dur[1] / 1e3) / 1e3) + delete timers[k] +}) + +process.on('log', (level, ...message) => + console.log(`\x1B[31m${level}\x1B[39m`, ...message)) + +const priv = /(^|:)_([^=]+)=(.*)\n/g +const ini = require('ini') +const config = new Config({ shorthands, types, defaults, npmPath }) +config.load().then(async () => { + for (const [where, { data, source }] of config.data.entries()) { + console.log(`; ${where} from ${source}`) + if (where === 'default' && !config.get('long')) { + console.log('; not shown, run with -l to show all\n') + } else { + console.log(ini.stringify(data).replace(priv, '$1_$2=******\n')) + } + } + console.log('argv:', { raw: config.argv, parsed: config.parsedArgv }) + return undefined +}).catch(() => {}) diff --git a/workspaces/config/tap-snapshots/test/index.js.test.cjs b/workspaces/config/tap-snapshots/test/index.js.test.cjs new file mode 100644 index 000000000..6680fd237 --- /dev/null +++ b/workspaces/config/tap-snapshots/test/index.js.test.cjs @@ -0,0 +1,240 @@ +/* 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 new file mode 100644 index 000000000..9d80f7e09 --- /dev/null +++ b/workspaces/config/tap-snapshots/test/type-description.js.test.cjs @@ -0,0 +1,449 @@ +/* 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/env-replace.js b/workspaces/config/test/env-replace.js new file mode 100644 index 000000000..c2b570364 --- /dev/null +++ b/workspaces/config/test/env-replace.js @@ -0,0 +1,13 @@ +const envReplace = require('../lib/env-replace.js') +const t = require('tap') + +const env = { + foo: 'bar', + bar: 'baz', +} + +t.equal(envReplace('\\${foo}', env), '${foo}') +t.equal(envReplace('\\\\${foo}', env), '\\bar') +t.equal(envReplace('${baz}', env), '${baz}') +t.equal(envReplace('\\${baz}', env), '${baz}') +t.equal(envReplace('\\\\${baz}', env), '\\${baz}') diff --git a/workspaces/config/test/fixtures/cafile b/workspaces/config/test/fixtures/cafile new file mode 100644 index 000000000..0bc922b25 --- /dev/null +++ b/workspaces/config/test/fixtures/cafile @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIICjTCCAfigAwIBAgIEMaYgRzALBgkqhkiG9w0BAQQwRTELMAkGA1UEBhMCVVMx +NjA0BgNVBAoTLU5hdGlvbmFsIEFlcm9uYXV0aWNzIGFuZCBTcGFjZSBBZG1pbmlz +dHJhdGlvbjAmFxE5NjA1MjgxMzQ5MDUrMDgwMBcROTgwNTI4MTM0OTA1KzA4MDAw +ZzELMAkGA1UEBhMCVVMxNjA0BgNVBAoTLU5hdGlvbmFsIEFlcm9uYXV0aWNzIGFu +ZCBTcGFjZSBBZG1pbmlzdHJhdGlvbjEgMAkGA1UEBRMCMTYwEwYDVQQDEwxTdGV2 +ZSBTY2hvY2gwWDALBgkqhkiG9w0BAQEDSQAwRgJBALrAwyYdgxmzNP/ts0Uyf6Bp +miJYktU/w4NG67ULaN4B5CnEz7k57s9o3YY3LecETgQ5iQHmkwlYDTL2fTgVfw0C +AQOjgaswgagwZAYDVR0ZAQH/BFowWDBWMFQxCzAJBgNVBAYTAlVTMTYwNAYDVQQK +Ey1OYXRpAAAAACBBZXJvbmF1dGljcyBhbmQgU3BhY2UgQWRtaW5pc3RyYXRpb24x +DTALBgNVBAMTBENSTDEwFwYDVR0BAQH/BA0wC4AJODMyOTcwODEwMBgGA1UdAgQR +MA8ECTgzMjk3MDgyM4ACBSAwDQYDVR0KBAYwBAMCBkAwCwYJKoZIhvcNAQEEA4GB +AH2y1VCEw/A4zaXzSYZJTTUi3uawbbFiS2yxHvgf28+8Js0OHXk1H1w2d6qOHH21 +X82tZXd/0JtG0g1T9usFFBDvYK8O0ebgz/P5ELJnBL2+atObEuJy1ZZ0pBDWINR3 +WkDNLCGiTkCKp0F5EWIrVDwh54NNevkCQRZita+z4IBO +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +AAAAAACCAfigAwIBAgIEMaYgRzALBgkqhkiG9w0BAQQwRTELMAkGA1UEBhMCVVMx +NjA0BgNVBAoTLU5hdGlvbmFsIEFlcm9uYXV0aWNzIGFuZCBTcGFjZSBBZG1pbmlz +dHJhdGlvbjAmFxE5NjA1MjgxMzQ5MDUrMDgwMBcROTgwNTI4MTM0OTA1KzA4MDAw +ZzELMAkGA1UEBhMCVVMxNjA0BgNVBAoTLU5hdGlvbmFsIEFlcm9uYXV0aWNzIGFu +ZCBTcGFjZSBBZG1pbmlzdHJhdGlvbjEgMAkGA1UEBRMCMTYwEwYDVQQDEwxTdGV2 +ZSBTY2hvY2gwWDALBgkqhkiG9w0BAQEDSQAwRgJBALrAwyYdgxmzNP/ts0Uyf6Bp +miJYktU/w4NG67ULaN4B5CnEz7k57s9o3YY3LecETgQ5iQHmkwlYDTL2fTgVfw0C +AQOjgaswgagwZAYDVR0ZAQH/BFowWDBWMFQxCzAJBgNVBAYTAlVTMTYwNAYDVQQK +Ey1OYXRpb25hbCBBZXJvbmF1dGljcyBhbmQgU3BhY2UgQWRtaW5pc3RyYXRpb24x +DTALBgNVBAMTBENSTDEwFwYDVR0BAQH/BA0wC4AJODMyOTcwODEwMBgGA1UdAgQR +MA8ECTgzMjk3MDgyM4ACBSAwDQYDVR0KBAYwBAMCBkAwCwYJKoZIhvcNAQEEA4GB +AH2y1VCEw/A4zaXzSYZJTTUi3uawbbFiS2yxHvgf28+8Js0OHXk1H1w2d6qOHH21 +X82tZXd/0JtG0g1T9usFFBDvYK8O0ebgz/P5ELJnBL2+atObEuJy1ZZ0pBDWINR3 +WkDNLCGiTkCKp0F5EWIrVDwh54NNevkCQRZita+z4IBO +-----END CERTIFICATE----- diff --git a/workspaces/config/test/fixtures/defaults.js b/workspaces/config/test/fixtures/defaults.js new file mode 100644 index 000000000..322ceb018 --- /dev/null +++ b/workspaces/config/test/fixtures/defaults.js @@ -0,0 +1,143 @@ +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 new file mode 100644 index 000000000..ce0aff6f3 --- /dev/null +++ b/workspaces/config/test/fixtures/definitions.js @@ -0,0 +1,2609 @@ +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 new file mode 100644 index 000000000..588d05bf0 --- /dev/null +++ b/workspaces/config/test/fixtures/flatten.js @@ -0,0 +1,33 @@ +// 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 new file mode 100644 index 000000000..5c460c661 --- /dev/null +++ b/workspaces/config/test/fixtures/shorthands.js @@ -0,0 +1,41 @@ +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 new file mode 100644 index 000000000..0f8cedfd6 --- /dev/null +++ b/workspaces/config/test/fixtures/types.js @@ -0,0 +1,151 @@ +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/index.js b/workspaces/config/test/index.js new file mode 100644 index 000000000..8dbee0588 --- /dev/null +++ b/workspaces/config/test/index.js @@ -0,0 +1,1295 @@ +const t = require('tap') + +const fs = require('fs') +const { readFileSync } = fs + +// when running with `npm test` it adds environment variables that +// mess with the things we expect here, so delete all of those. +Object.keys(process.env) + .filter(k => /^npm_/.test(k)) + .forEach(k => delete process.env[k]) +delete process.env.PREFIX +delete process.env.DESTDIR + +const definitions = require('./fixtures/definitions.js') +const shorthands = require('./fixtures/shorthands.js') +const flatten = require('./fixtures/flatten.js') +const typeDefs = require('../lib/type-defs.js') + +const { resolve, join, dirname } = require('path') + +const Config = t.mock('../', { + 'fs/promises': { + ...fs.promises, + readFile: async (path, ...args) => { + if (path.includes('WEIRD-ERROR')) { + throw Object.assign(new Error('weird error'), { code: 'EWEIRD' }) + } + + return fs.promises.readFile(path, ...args) + }, + }, +}) + +// because we used t.mock above, the require cache gets blown and we lose our direct equality +// on the typeDefs. to get around that, we require an un-mocked Config and assert against that +const RealConfig = require('../') +t.equal(typeDefs, RealConfig.typeDefs, 'exposes type definitions') + +t.test('construct with no settings, get default values for stuff', t => { + const npmPath = t.testdir() + const c = new Config({ + definitions: {}, + npmPath, + }) + + t.test('default some values from process object', t => { + const { env, execPath, platform } = process + const cwd = process.cwd() + t.equal(c.env, env, 'env') + t.equal(c.execPath, execPath, 'execPath') + t.equal(c.cwd, cwd, 'cwd') + t.equal(c.platform, platform, 'platform') + t.end() + }) + + t.test('not loaded yet', t => { + t.equal(c.loaded, false, 'not loaded yet') + t.throws(() => c.get('foo'), { + message: 'call config.load() before reading values', + }) + t.throws(() => c.find('foo'), { + message: 'call config.load() before reading values', + }) + t.throws(() => c.set('foo', 'bar'), { + message: 'call config.load() before setting values', + }) + t.throws(() => c.delete('foo'), { + message: 'call config.load() before deleting values', + }) + t.rejects(() => c.save('user'), { + message: 'call config.load() before saving', + }) + t.throws(() => c.data.set('user', {}), { + message: 'cannot change internal config data structure', + }) + t.throws(() => c.data.delete('user'), { + message: 'cannot change internal config data structure', + }) + t.end() + }) + + t.test('data structure all wired up properly', t => { + // verify that the proto objects are all wired up properly + c.list.forEach((data, i) => { + t.equal(Object.getPrototypeOf(data), c.list[i + 1] || null) + }) + t.equal(c.data.get('default').data, c.list[c.list.length - 1]) + t.equal(c.data.get('cli').data, c.list[0]) + t.end() + }) + + t.end() +}) + +t.test('load from files and environment variables', t => { + // need to get the dir because we reference it in the contents + const path = t.testdir() + t.testdir({ + npm: { + npmrc: ` +builtin-config = true +foo = from-builtin +userconfig = ${path}/user/.npmrc-from-builtin +`, + }, + global: { + etc: { + npmrc: ` +global-config = true +foo = from-global +userconfig = ${path}/should-not-load-this-file +`, + }, + }, + user: { + '.npmrc': ` +default-user-config-in-home = true +foo = from-default-userconfig +prefix = ${path}/global +`, + '.npmrc-from-builtin': ` +user-config-from-builtin = true +foo = from-custom-userconfig +globalconfig = ${path}/global/etc/npmrc +`, + }, + project: { + node_modules: {}, + '.npmrc': ` +project-config = true +foo = from-project-config +loglevel = yolo +`, + }, + 'project-no-config': { + 'package.json': '{"name":"@scope/project"}', + }, + }) + + const logs = [] + const logHandler = (...args) => logs.push(args) + process.on('log', logHandler) + t.teardown(() => process.off('log', logHandler)) + + const argv = [ + process.execPath, + __filename, + '-v', + '--no-audit', + 'config', + 'get', + 'foo', + '--also=dev', + '--registry=hello', + '--omit=cucumber', + '--access=blueberry', + '--multiple-numbers=what kind of fruit is not a number', + '--multiple-numbers=a baNaNa!!', + '-C', + ] + + t.test('dont let userconfig be the same as builtin config', async t => { + const config = new Config({ + npmPath: `${path}/npm`, + env: {}, + argv: [process.execPath, __filename, '--userconfig', `${path}/npm/npmrc`], + cwd: `${path}/project`, + shorthands, + definitions, + }) + await t.rejects(() => config.load(), { + message: `double-loading config "${resolve(path, 'npm/npmrc')}" as "user",` + + ' previously loaded as "builtin"', + }) + }) + + t.test('dont load project config if global is true', async t => { + const config = new Config({ + npmPath: `${path}/npm`, + env: {}, + argv: [process.execPath, __filename, '--global'], + cwd: `${path}/project`, + shorthands, + definitions, + }) + + await config.load() + const source = config.data.get('project').source + t.equal(source, '(global mode enabled, ignored)', 'data has placeholder') + t.equal(config.sources.get(source), 'project', 'sources has project') + }) + + t.test('dont load project config if location is global', async t => { + const config = new Config({ + npmPath: `${path}/npm`, + env: {}, + argv: [process.execPath, __filename, '--location', 'global'], + cwd: `${path}/project`, + shorthands, + definitions, + }) + + await config.load() + const source = config.data.get('project').source + t.equal(source, '(global mode enabled, ignored)', 'data has placeholder') + t.equal(config.sources.get(source), 'project', 'sources has project') + t.ok(config.localPrefix, 'localPrefix is set') + }) + + t.test('verbose log if config file read is weird error', async t => { + const config = new Config({ + npmPath: path, + env: {}, + argv: [process.execPath, + __filename, + '--userconfig', + `${path}/WEIRD-ERROR`, + '--no-workspaces'], + cwd: path, + shorthands, + definitions, + }) + logs.length = 0 + await config.load() + t.match(logs, [['verbose', 'config', 'error loading user config', { + message: 'weird error', + }]]) + logs.length = 0 + }) + + t.test('load configs from all files, cli, and env', async t => { + const env = { + npm_config_foo: 'from-env', + npm_config_global: '', + npm_config_prefix: '/something', + } + const config = new Config({ + npmPath: `${path}/npm`, + env, + argv, + cwd: `${path}/project`, + + shorthands, + definitions, + }) + + t.equal(config.globalPrefix, null, 'globalPrefix missing before load') + + await config.load() + + t.equal(config.globalPrefix, resolve('/something'), 'env-defined prefix should be loaded') + + t.equal(config.get('global', 'env'), undefined, 'empty env is missing') + t.equal(config.get('global'), false, 'empty env is missing') + + config.set('asdf', 'quux', 'global') + await config.save('global') + const gres = readFileSync(`${path}/global/etc/npmrc`, 'utf8') + t.match(gres, 'asdf=quux') + + const cliData = config.data.get('cli') + t.throws(() => cliData.loadError = true, { + message: 'cannot set ConfigData loadError after load', + }) + t.throws(() => cliData.source = 'foo', { + message: 'cannot set ConfigData source more than once', + }) + t.throws(() => cliData.raw = 1234, { + message: 'cannot set ConfigData raw after load', + }) + + config.argv = [] + + t.throws(() => config.loadCLI(), { + message: 'double-loading "cli" configs from command line options, previously loaded from' + + ' command line options', + }) + t.rejects(() => config.loadUserConfig(), { + message: `double-loading "user" configs from ${resolve(path, 'should-not-load-this-file')}` + + `, previously loaded from ${resolve(path, 'user/.npmrc-from-builtin')}`, + }) + + t.equal(config.loaded, true, 'config is loaded') + + await t.rejects(() => config.load(), { + message: 'attempting to load npm config multiple times', + }) + t.equal(config.find('no config value here'), null) + + t.equal(config.prefix, config.localPrefix, 'prefix is local prefix when not global') + config.set('global', true) + t.equal(config.prefix, config.globalPrefix, 'prefix is global prefix when global') + config.set('global', false) + t.equal(config.find('global'), 'cli') + config.delete('global') + t.equal(config.find('global'), 'default') + + t.throws(() => config.get('foo', 'barbaz'), { + message: 'invalid config location param: barbaz', + }) + t.throws(() => config.set('foo', 1234, 'barbaz'), { + message: 'invalid config location param: barbaz', + }) + t.throws(() => config.delete('foo', 'barbaz'), { + message: 'invalid config location param: barbaz', + }) + + t.match(config.sources, new Map([ + ['default values', 'default'], + [resolve(path, 'npm/npmrc'), 'builtin'], + ['command line options', 'cli'], + ['environment', 'env'], + [resolve(path, 'project/.npmrc'), 'project'], + [resolve(path, 'user/.npmrc-from-builtin'), 'user'], + [resolve(path, 'global/etc/npmrc'), 'global'], + ])) + + t.strictSame({ + version: config.get('version'), + audit: config.get('audit'), + 'project-config': config.get('project-config'), + foo: config.get('foo'), + 'user-config-from-builtin': config.get('user-config-from-builtin'), + 'global-config': config.get('global-config'), + 'builtin-config': config.get('builtin-config'), + all: config.get('all'), + }, { + version: true, + audit: false, + 'project-config': true, + foo: 'from-env', + 'user-config-from-builtin': true, + 'global-config': true, + 'builtin-config': true, + all: config.get('all'), + }) + + t.match(env, { + npm_config_user_config_from_builtin: 'true', + npm_config_audit: '', + npm_config_version: 'true', + npm_config_foo: 'from-env', + npm_config_builtin_config: 'true', + }, 'set env values') + + // warn logs are emitted as a side effect of validate + config.validate() + t.strictSame(logs, [ + ['warn', 'invalid config', 'registry="hello"', 'set in command line options'], + ['warn', 'invalid config', 'Must be', 'full url with "http://"'], + ['warn', 'invalid config', 'omit="cucumber"', 'set in command line options'], + ['warn', 'invalid config', 'Must be one or more of:', 'dev, optional, peer'], + ['warn', 'invalid config', 'access="blueberry"', 'set in command line options'], + ['warn', 'invalid config', 'Must be one of:', 'null, restricted, public'], + ['warn', 'invalid config', 'multiple-numbers="what kind of fruit is not a number"', + 'set in command line options'], + ['warn', 'invalid config', 'Must be one or more', 'numeric value'], + ['warn', 'invalid config', 'multiple-numbers="a baNaNa!!"', 'set in command line options'], + ['warn', 'invalid config', 'Must be one or more', 'numeric value'], + ['warn', 'invalid config', 'prefix=true', 'set in command line options'], + ['warn', 'invalid config', 'Must be', 'valid filesystem path'], + ['warn', 'config', 'also', 'Please use --include=dev instead.'], + ['warn', 'invalid config', 'loglevel="yolo"', + `set in ${resolve(path, 'project/.npmrc')}`], + ['warn', 'invalid config', 'Must be one of:', + ['silent', 'error', 'warn', 'notice', 'http', 'timing', 'info', + 'verbose', 'silly'].join(', '), + ], + ]) + t.equal(config.valid, false) + logs.length = 0 + + // set a new value that defaults to cli source + config.set('cli-config', 1) + + t.ok(config.isDefault('methane'), + 'should return true if value is retrieved from default definitions') + t.notOk(config.isDefault('cli-config'), + 'should return false for a cli-defined value') + t.notOk(config.isDefault('foo'), + 'should return false for a env-defined value') + t.notOk(config.isDefault('project-config'), + 'should return false for a project-defined value') + t.notOk(config.isDefault('default-user-config-in-home'), + 'should return false for a user-defined value') + t.notOk(config.isDefault('global-config'), + 'should return false for a global-defined value') + t.notOk(config.isDefault('builtin-config'), + 'should return false for a builtin-defined value') + + // make sure isDefault still works as intended after + // setting and deleting values in differente sources + config.set('methane', 'H2O', 'cli') + t.notOk(config.isDefault('methane'), + 'should no longer return true now that a cli value was defined') + config.delete('methane', 'cli') + t.ok(config.isDefault('methane'), + 'should return true once again now that values is retrieved from defaults') + }) + + t.test('normalize config env keys', async t => { + const env = { + npm_config_bAr: 'bAr env', + NPM_CONFIG_FOO: 'FOO env', + 'npm_config_//reg.example/UP_CASE/:username': 'ME', + 'npm_config_//reg.example/UP_CASE/:_password': 'Shhhh!', + 'NPM_CONFIG_//reg.example/UP_CASE/:_authToken': 'sEcReT', + } + const config = new Config({ + npmPath: `${path}/npm`, + env, + argv, + cwd: `${path}/project`, + + shorthands, + definitions, + }) + + await config.load() + + t.strictSame({ + bar: config.get('bar'), + foo: config.get('foo'), + '//reg.example/UP_CASE/:username': config.get('//reg.example/UP_CASE/:username'), + '//reg.example/UP_CASE/:_password': config.get('//reg.example/UP_CASE/:_password'), + '//reg.example/UP_CASE/:_authToken': config.get('//reg.example/UP_CASE/:_authToken'), + }, { + bar: 'bAr env', + foo: 'FOO env', + '//reg.example/UP_CASE/:username': 'ME', + '//reg.example/UP_CASE/:_password': 'Shhhh!', + '//reg.example/UP_CASE/:_authToken': 'sEcReT', + }) + }) + + t.test('do not double-load project/user config', async t => { + const env = { + npm_config_foo: 'from-env', + npm_config_globalconfig: '/this/path/does/not/exist', + } + + const config = new Config({ + npmPath: `${path}/npm`, + env, + argv: [process.execPath, __filename, '--userconfig', `${path}/project/.npmrc`], + cwd: `${path}/project`, + + shorthands, + definitions, + }) + await config.load() + + config.argv = [] + t.equal(config.loaded, true, 'config is loaded') + + t.match(config.data.get('global').loadError, { code: 'ENOENT' }) + t.strictSame(config.data.get('env').raw, Object.assign(Object.create(null), { + foo: 'from-env', + globalconfig: '/this/path/does/not/exist', + })) + + t.match(config.sources, new Map([ + ['default values', 'default'], + [resolve(path, 'npm/npmrc'), 'builtin'], + ['command line options', 'cli'], + ['environment', 'env'], + ['(same as "user" config, ignored)', 'project'], + [resolve(path, 'project/.npmrc'), 'user'], + ])) + + t.rejects(() => config.save('yolo'), { + message: 'invalid config location param: yolo', + }) + config.validate() + t.equal(config.valid, false, 'config should not be valid') + logs.length = 0 + }) + + t.test('load configs from files, cli, and env, no builtin or project', async t => { + const env = { + npm_config_foo: 'from-env', + HOME: `${path}/user`, + } + + const config = new Config({ + // no builtin + npmPath: path, + env, + argv, + cwd: `${path}/project-no-config`, + + // should prepend DESTDIR to /global + DESTDIR: path, + PREFIX: '/global', + platform: 'posix', + + shorthands, + definitions, + }) + await config.load() + + t.match(config.sources, new Map([ + ['default values', 'default'], + ['command line options', 'cli'], + ['environment', 'env'], + [resolve(path, 'user/.npmrc'), 'user'], + [resolve(path, 'global/etc/npmrc'), 'global'], + ])) + // no builtin or project config + t.equal(config.sources.get(resolve(path, 'npm/npmrc')), undefined) + t.equal(config.sources.get(resolve(path, 'project/.npmrc')), undefined) + + t.strictSame({ + version: config.get('version'), + audit: config.get('audit'), + 'project-config': config.get('project-config'), + foo: config.get('foo'), + 'user-config-from-builtin': config.get('user-config-from-builtin'), + 'default-user-config-in-home': config.get('default-user-config-in-home'), + 'global-config': config.get('global-config'), + 'builtin-config': config.get('builtin-config'), + all: config.get('all'), + }, { + version: true, + audit: false, + 'project-config': undefined, + foo: 'from-env', + 'user-config-from-builtin': undefined, + 'default-user-config-in-home': true, + 'global-config': true, + 'builtin-config': undefined, + all: config.get('all'), + }) + + t.strictSame(logs, [ + ['warn', 'invalid config', 'registry="hello"', 'set in command line options'], + ['warn', 'invalid config', 'Must be', 'full url with "http://"'], + ['warn', 'invalid config', 'omit="cucumber"', 'set in command line options'], + ['warn', 'invalid config', 'Must be one or more of:', 'dev, optional, peer'], + ['warn', 'invalid config', 'access="blueberry"', 'set in command line options'], + ['warn', 'invalid config', 'Must be one of:', 'null, restricted, public'], + ['warn', 'invalid config', 'multiple-numbers="what kind of fruit is not a number"', + 'set in command line options'], + ['warn', 'invalid config', 'Must be one or more', 'numeric value'], + ['warn', 'invalid config', 'multiple-numbers="a baNaNa!!"', 'set in command line options'], + ['warn', 'invalid config', 'Must be one or more', 'numeric value'], + ['warn', 'invalid config', 'prefix=true', 'set in command line options'], + ['warn', 'invalid config', 'Must be', 'valid filesystem path'], + ['warn', 'config', 'also', 'Please use --include=dev instead.'], + ]) + }) + + t.end() +}) + +t.test('cafile loads as ca (and some saving tests)', async t => { + const cafile = resolve(__dirname, 'fixtures', 'cafile') + const dir = t.testdir({ + '.npmrc': `cafile = ${cafile} +//registry.npmjs.org/:_authToken = deadbeefcafebadfoobarbaz42069 +`, + }) + const expect = `cafile=${cafile} +//registry.npmjs.org/:_authToken=deadbeefcafebadfoobarbaz42069 +` + + const config = new Config({ + shorthands, + definitions, + npmPath: __dirname, + env: { HOME: dir, PREFIX: dir }, + flatten, + }) + await config.load() + t.equal(config.get('ca'), null, 'does not overwrite config.get') + const { flat } = config + t.equal(config.flat, flat, 'getter returns same value again') + const ca = flat.ca + t.equal(ca.join('\n').replace(/\r\n/g, '\n').trim(), readFileSync(cafile, 'utf8') + .replace(/\r\n/g, '\n').trim()) + await config.save('user') + const res = readFileSync(`${dir}/.npmrc`, 'utf8').replace(/\r\n/g, '\n') + t.equal(res, expect, 'did not write back ca, only cafile') + // while we're here, test that saving an empty config file deletes it + config.delete('cafile', 'user') + config.clearCredentialsByURI(config.get('registry')) + await config.save('user') + t.throws(() => readFileSync(`${dir}/.npmrc`, 'utf8'), { code: 'ENOENT' }) + // do it again to verify we ignore the unlink error + await config.save('user') + t.throws(() => readFileSync(`${dir}/.npmrc`, 'utf8'), { code: 'ENOENT' }) + t.equal(config.valid, true) +}) + +t.test('cafile ignored if ca set', async t => { + const cafile = resolve(__dirname, 'fixtures', 'cafile') + const dir = t.testdir({ + '.npmrc': `cafile = ${cafile}`, + }) + const ca = ` +-----BEGIN CERTIFICATE----- +fakey mc fakerson +-----END CERTIFICATE----- +` + const config = new Config({ + shorthands, + definitions, + npmPath: __dirname, + env: { + HOME: dir, + npm_config_ca: ca, + }, + }) + await config.load() + t.strictSame(config.get('ca'), [ca.trim()]) + await config.save('user') + const res = readFileSync(`${dir}/.npmrc`, 'utf8') + t.equal(res.trim(), `cafile=${cafile}`) +}) + +t.test('ignore cafile if it does not load', async t => { + const cafile = resolve(__dirname, 'fixtures', 'cafile-does-not-exist') + const dir = t.testdir({ + '.npmrc': `cafile = ${cafile}`, + }) + const config = new Config({ + shorthands, + definitions, + npmPath: __dirname, + env: { HOME: dir }, + }) + await config.load() + t.equal(config.get('ca'), null) + await config.save('user') + const res = readFileSync(`${dir}/.npmrc`, 'utf8') + t.equal(res.trim(), `cafile=${cafile}`) +}) + +t.test('raise error if reading ca file error other than ENOENT', async t => { + const cafile = resolve(__dirname, 'fixtures', 'WEIRD-ERROR') + const dir = t.testdir({ + '.npmrc': `cafile = ${cafile}`, + }) + const config = new Config({ + shorthands, + definitions, + npmPath: __dirname, + env: { HOME: dir }, + flatten, + }) + await config.load() + t.throws(() => config.flat.ca, { code: 'EWEIRD' }) +}) + +t.test('credentials management', async t => { + const fixtures = { + nerfed_authToken: { '.npmrc': '//registry.example/:_authToken = 0bad1de4' }, + nerfed_userpass: { + '.npmrc': `//registry.example/:username = hello +//registry.example/:_password = ${Buffer.from('world').toString('base64')} +//registry.example/:email = i@izs.me +//registry.example/:always-auth = "false"`, + }, + nerfed_auth: { // note: does not load, because we don't do _auth per reg + '.npmrc': `//registry.example/:_auth = ${Buffer.from('hello:world').toString('base64')}`, + }, + nerfed_mtls: { '.npmrc': `//registry.example/:certfile = /path/to/cert +//registry.example/:keyfile = /path/to/key`, + }, + nerfed_mtlsAuthToken: { '.npmrc': `//registry.example/:_authToken = 0bad1de4 +//registry.example/:certfile = /path/to/cert +//registry.example/:keyfile = /path/to/key`, + }, + nerfed_mtlsUserPass: { '.npmrc': `//registry.example/:username = hello +//registry.example/:_password = ${Buffer.from('world').toString('base64')} +//registry.example/:email = i@izs.me +//registry.example/:always-auth = "false" +//registry.example/:certfile = /path/to/cert +//registry.example/:keyfile = /path/to/key`, + }, + def_userpass: { + '.npmrc': `username = hello +_password = ${Buffer.from('world').toString('base64')} +email = i@izs.me +//registry.example/:always-auth = true +`, + }, + def_userNoPass: { + '.npmrc': `username = hello +email = i@izs.me +//registry.example/:always-auth = true +`, + }, + def_passNoUser: { + '.npmrc': `_password = ${Buffer.from('world').toString('base64')} +email = i@izs.me +//registry.example/:always-auth = true +`, + }, + def_auth: { + '.npmrc': `_auth = ${Buffer.from('hello:world').toString('base64')} +always-auth = true`, + }, + none_authToken: { '.npmrc': '_authToken = 0bad1de4' }, + none_lcAuthToken: { '.npmrc': '_authtoken = 0bad1de4' }, + none_emptyConfig: { '.npmrc': '' }, + none_noConfig: {}, + } + const path = t.testdir(fixtures) + + const defReg = 'https://registry.example/' + const otherReg = 'https://other.registry/' + for (const testCase of Object.keys(fixtures)) { + t.test(testCase, async t => { + const c = new Config({ + npmPath: path, + shorthands, + definitions, + env: { HOME: resolve(path, testCase) }, + argv: ['node', 'file', '--registry', defReg], + }) + await c.load() + + // only have to do this the first time, it's redundant otherwise + if (testCase === 'none_noConfig') { + t.throws(() => c.setCredentialsByURI('http://x.com', { + username: 'foo', + email: 'bar@baz.com', + }), { message: 'must include password' }) + t.throws(() => c.setCredentialsByURI('http://x.com', { + password: 'foo', + email: 'bar@baz.com', + }), { message: 'must include username' }) + c.setCredentialsByURI('http://x.com', { + username: 'foo', + password: 'bar', + email: 'asdf@quux.com', + }) + } + + // the def_ and none_ prefixed cases have unscoped auth values and should throw + if (testCase.startsWith('def_') || + testCase === 'none_authToken' || + testCase === 'none_lcAuthToken') { + try { + c.validate() + // validate should throw, fail the test here if it doesn't + t.fail('validate should have thrown') + } catch (err) { + if (err.code !== 'ERR_INVALID_AUTH') { + throw err + } + + // we got our expected invalid auth error, so now repair it + c.repair(err.problems) + t.ok(c.valid, 'config is valid') + } + } else { + // validate won't throw for these ones, so let's prove it and repair are no-ops + c.validate() + c.repair() + } + + const d = c.getCredentialsByURI(defReg) + const o = c.getCredentialsByURI(otherReg) + + t.matchSnapshot(d, 'default registry') + t.matchSnapshot(o, 'other registry') + + c.clearCredentialsByURI(defReg) + const defAfterDelete = c.getCredentialsByURI(defReg) + { + const expectKeys = [] + if (defAfterDelete.email) { + expectKeys.push('email') + } + t.strictSame(Object.keys(defAfterDelete), expectKeys) + } + + c.clearCredentialsByURI(otherReg) + const otherAfterDelete = c.getCredentialsByURI(otherReg) + { + const expectKeys = [] + if (otherAfterDelete.email) { + expectKeys.push('email') + } + t.strictSame(Object.keys(otherAfterDelete), expectKeys) + } + + // need both or none of user/pass + if (!d.token && (!d.username || !d.password) && (!d.certfile || !d.keyfile)) { + t.throws(() => c.setCredentialsByURI(defReg, d)) + } else { + c.setCredentialsByURI(defReg, d) + t.matchSnapshot(c.getCredentialsByURI(defReg), 'default registry after set') + } + + if (!o.token && (!o.username || !o.password) && (!o.certfile || !o.keyfile)) { + t.throws(() => c.setCredentialsByURI(otherReg, o), {}, { otherReg, o }) + } else { + c.setCredentialsByURI(otherReg, o) + t.matchSnapshot(c.getCredentialsByURI(otherReg), 'other registry after set') + } + }) + } + t.end() +}) + +t.test('finding the global prefix', t => { + const npmPath = __dirname + t.test('load from PREFIX env', t => { + const c = new Config({ + env: { + PREFIX: '/prefix/env', + }, + shorthands, + definitions, + npmPath, + }) + c.loadGlobalPrefix() + t.throws(() => c.loadGlobalPrefix(), { + message: 'cannot load default global prefix more than once', + }) + t.equal(c.globalPrefix, '/prefix/env') + t.end() + }) + t.test('load from execPath, win32', t => { + const c = new Config({ + platform: 'win32', + execPath: '/path/to/nodejs/node.exe', + shorthands, + definitions, + npmPath, + }) + c.loadGlobalPrefix() + t.equal(c.globalPrefix, dirname('/path/to/nodejs/node.exe')) + t.end() + }) + t.test('load from execPath, posix', t => { + const c = new Config({ + platform: 'posix', + execPath: '/path/to/nodejs/bin/node', + shorthands, + definitions, + npmPath, + }) + c.loadGlobalPrefix() + t.equal(c.globalPrefix, dirname(dirname('/path/to/nodejs/bin/node'))) + t.end() + }) + t.test('load from execPath with destdir, posix', t => { + const c = new Config({ + platform: 'posix', + execPath: '/path/to/nodejs/bin/node', + env: { DESTDIR: '/some/dest/dir' }, + shorthands, + definitions, + npmPath, + }) + c.loadGlobalPrefix() + t.equal(c.globalPrefix, join('/some/dest/dir', dirname(dirname('/path/to/nodejs/bin/node')))) + t.end() + }) + t.end() +}) + +t.test('finding the local prefix', t => { + const path = t.testdir({ + hasNM: { + node_modules: {}, + x: { y: { z: {} } }, + }, + hasPJ: { + 'package.json': '{}', + x: { y: { z: {} } }, + }, + }) + t.test('explicit cli prefix', async t => { + const c = new Config({ + argv: [process.execPath, __filename, '-C', path], + shorthands, + definitions, + npmPath: path, + }) + await c.load() + t.equal(c.localPrefix, resolve(path)) + }) + t.test('has node_modules', async t => { + const c = new Config({ + cwd: `${path}/hasNM/x/y/z`, + shorthands, + definitions, + npmPath: path, + }) + await c.load() + t.equal(c.localPrefix, resolve(path, 'hasNM')) + }) + t.test('has package.json', async t => { + const c = new Config({ + cwd: `${path}/hasPJ/x/y/z`, + shorthands, + definitions, + npmPath: path, + }) + await c.load() + t.equal(c.localPrefix, resolve(path, 'hasPJ')) + }) + t.test('nada, just use cwd', async t => { + const c = new Config({ + cwd: '/this/path/does/not/exist/x/y/z', + shorthands, + definitions, + npmPath: path, + }) + await c.load() + t.equal(c.localPrefix, '/this/path/does/not/exist/x/y/z') + }) + t.end() +}) + +t.test('setting basic auth creds and email', async t => { + const registry = 'https://registry.npmjs.org/' + const path = t.testdir() + const _auth = Buffer.from('admin:admin').toString('base64') + const opts = { + shorthands: {}, + argv: ['node', __filename, `--userconfig=${path}/.npmrc`], + definitions: { + registry: { default: registry }, + }, + npmPath: process.cwd(), + } + const c = new Config(opts) + await c.load() + c.set('email', 'name@example.com', 'user') + t.equal(c.get('email', 'user'), 'name@example.com', 'email was set') + await c.save('user') + t.equal(c.get('email', 'user'), 'name@example.com', 'email still top level') + t.strictSame(c.getCredentialsByURI(registry), { email: 'name@example.com' }) + const d = new Config(opts) + await d.load() + t.strictSame(d.getCredentialsByURI(registry), { email: 'name@example.com' }) + d.set('_auth', _auth, 'user') + t.equal(d.get('_auth', 'user'), _auth, '_auth was set') + d.repair() + await d.save('user') + const e = new Config(opts) + await e.load() + t.equal(e.get('_auth', 'user'), undefined, 'un-nerfed _auth deleted') + t.strictSame(e.getCredentialsByURI(registry), { + email: 'name@example.com', + username: 'admin', + password: 'admin', + auth: _auth, + }, 'credentials saved and nerfed') +}) + +t.test('setting username/password/email individually', async t => { + const registry = 'https://registry.npmjs.org/' + const path = t.testdir() + const opts = { + shorthands: {}, + argv: ['node', __filename, `--userconfig=${path}/.npmrc`], + definitions: { + registry: { default: registry }, + }, + npmPath: process.cwd(), + } + const c = new Config(opts) + await c.load() + c.set('email', 'name@example.com', 'user') + t.equal(c.get('email'), 'name@example.com') + c.set('username', 'admin', 'user') + t.equal(c.get('username'), 'admin') + c.set('_password', Buffer.from('admin').toString('base64'), 'user') + t.equal(c.get('_password'), Buffer.from('admin').toString('base64')) + t.equal(c.get('_auth'), undefined) + c.repair() + await c.save('user') + + const d = new Config(opts) + await d.load() + t.equal(d.get('email'), 'name@example.com') + t.equal(d.get('username'), undefined) + t.equal(d.get('_password'), undefined) + t.equal(d.get('_auth'), undefined) + t.strictSame(d.getCredentialsByURI(registry), { + email: 'name@example.com', + username: 'admin', + password: 'admin', + auth: Buffer.from('admin:admin').toString('base64'), + }) +}) + +t.test('nerfdart auths set at the top level into the registry', async t => { + const registry = 'https://registry.npmjs.org/' + const _auth = Buffer.from('admin:admin').toString('base64') + const username = 'admin' + const _password = Buffer.from('admin').toString('base64') + const email = 'i@izs.me' + const _authToken = 'deadbeefblahblah' + + // name: [ini, expect, wontThrow] + const cases = { + '_auth only, no email': [`_auth=${_auth}`, { + '//registry.npmjs.org/:_auth': _auth, + }], + '_auth with email': [`_auth=${_auth}\nemail=${email}`, { + '//registry.npmjs.org/:_auth': _auth, + email, + }], + '_authToken alone': [`_authToken=${_authToken}`, { + '//registry.npmjs.org/:_authToken': _authToken, + }], + '_authToken and email': [`_authToken=${_authToken}\nemail=${email}`, { + '//registry.npmjs.org/:_authToken': _authToken, + email, + }], + 'username and _password': [`username=${username}\n_password=${_password}`, { + '//registry.npmjs.org/:username': username, + '//registry.npmjs.org/:_password': _password, + }], + 'username, password, email': [`username=${username}\n_password=${_password}\nemail=${email}`, { + '//registry.npmjs.org/:username': username, + '//registry.npmjs.org/:_password': _password, + email, + }], + // handled invalid/legacy cases + 'username, no _password': [`username=${username}`, {}], + '_password, no username': [`_password=${_password}`, {}], + '_authtoken instead of _authToken': [`_authtoken=${_authToken}`, {}], + '-authtoken instead of _authToken': [`-authtoken=${_authToken}`, {}], + // de-nerfdart the email, if present in that way + 'nerf-darted email': [`//registry.npmjs.org/:email=${email}`, { + email, + }, true], + } + + const logs = [] + const logHandler = (...args) => logs.push(args) + process.on('log', logHandler) + t.teardown(() => { + process.removeListener('log', logHandler) + }) + const cwd = process.cwd() + for (const [name, [ini, expect, wontThrow]] of Object.entries(cases)) { + t.test(name, async t => { + t.teardown(() => { + process.chdir(cwd) + logs.length = 0 + }) + const path = t.testdir({ + '.npmrc': ini, + 'package.json': JSON.stringify({}), + }) + process.chdir(path) + const argv = [ + 'node', + __filename, + `--prefix=${path}`, + `--userconfig=${path}/.npmrc`, + `--globalconfig=${path}/etc/npmrc`, + ] + const opts = { + shorthands: {}, + argv, + env: {}, + definitions: { + registry: { default: registry }, + }, + npmPath: process.cwd(), + } + + const c = new Config(opts) + await c.load() + + if (!wontThrow) { + t.throws(() => c.validate(), { code: 'ERR_INVALID_AUTH' }) + } + + // now we go ahead and do the repair, and save + c.repair() + await c.save('user') + t.same(c.list[3], expect) + }) + } +}) + +t.test('workspaces', async (t) => { + const path = resolve(t.testdir({ + 'package.json': JSON.stringify({ + name: 'root', + version: '1.0.0', + workspaces: ['./workspaces/*'], + }), + workspaces: { + one: { + 'package.json': JSON.stringify({ + name: 'one', + version: '1.0.0', + }), + }, + two: { + 'package.json': JSON.stringify({ + name: 'two', + version: '1.0.0', + }), + }, + three: { + 'package.json': JSON.stringify({ + name: 'three', + version: '1.0.0', + }), + '.npmrc': 'package-lock=false', + }, + }, + })) + + const logs = [] + const logHandler = (...args) => logs.push(args) + process.on('log', logHandler) + t.teardown(() => process.off('log', logHandler)) + t.afterEach(() => logs.length = 0) + + t.test('finds own parent', async (t) => { + const cwd = process.cwd() + t.teardown(() => process.chdir(cwd)) + process.chdir(`${path}/workspaces/one`) + + const config = new Config({ + npmPath: cwd, + env: {}, + argv: [process.execPath, __filename], + cwd: `${path}/workspaces/one`, + shorthands, + definitions, + }) + + await config.load() + t.equal(config.localPrefix, path, 'localPrefix is the root') + t.same(config.get('workspace'), [join(path, 'workspaces', 'one')], 'set the workspace') + t.equal(logs.length, 1, 'got one log message') + t.match(logs[0], ['info', /^found workspace root at/], 'logged info about workspace root') + }) + + t.test('finds other workspace parent', async (t) => { + const cwd = process.cwd() + t.teardown(() => process.chdir(cwd)) + process.chdir(`${path}/workspaces/one`) + + const config = new Config({ + npmPath: process.cwd(), + env: {}, + argv: [process.execPath, __filename, '--workspace', '../two'], + cwd: `${path}/workspaces/one`, + shorthands, + definitions, + }) + + await config.load() + t.equal(config.localPrefix, path, 'localPrefix is the root') + t.same(config.get('workspace'), ['../two'], 'kept the specified workspace') + t.equal(logs.length, 1, 'got one log message') + t.match(logs[0], ['info', /^found workspace root at/], 'logged info about workspace root') + }) + + t.test('warns when workspace has .npmrc', async (t) => { + const cwd = process.cwd() + t.teardown(() => process.chdir(cwd)) + process.chdir(`${path}/workspaces/three`) + + const config = new Config({ + npmPath: process.cwd(), + env: {}, + argv: [process.execPath, __filename], + cwd: `${path}/workspaces/three`, + shorthands, + definitions, + }) + + await config.load() + t.equal(config.localPrefix, path, 'localPrefix is the root') + t.same(config.get('workspace'), [join(path, 'workspaces', 'three')], 'kept the workspace') + t.equal(logs.length, 2, 'got two log messages') + t.match(logs[0], ['warn', /^ignoring workspace config/], 'warned about ignored config') + t.match(logs[1], ['info', /^found workspace root at/], 'logged info about workspace root') + }) + + t.test('prefix skips auto detect', async (t) => { + const cwd = process.cwd() + t.teardown(() => process.chdir(cwd)) + process.chdir(`${path}/workspaces/one`) + + const config = new Config({ + npmPath: process.cwd(), + env: {}, + argv: [process.execPath, __filename, '--prefix', './'], + cwd: `${path}/workspaces/one`, + shorthands, + definitions, + }) + + await config.load() + t.equal(config.localPrefix, join(path, 'workspaces', 'one'), 'localPrefix is the root') + t.same(config.get('workspace'), [], 'did not set workspace') + t.equal(logs.length, 0, 'got no log messages') + }) + + t.test('no-workspaces skips auto detect', async (t) => { + const cwd = process.cwd() + t.teardown(() => process.chdir(cwd)) + process.chdir(`${path}/workspaces/one`) + + const config = new Config({ + npmPath: process.cwd(), + env: {}, + argv: [process.execPath, __filename, '--no-workspaces'], + cwd: `${path}/workspaces/one`, + shorthands, + definitions, + }) + + await config.load() + t.equal(config.localPrefix, join(path, 'workspaces', 'one'), 'localPrefix is the root') + t.same(config.get('workspace'), [], 'did not set workspace') + t.equal(logs.length, 0, 'got no log messages') + }) + + t.test('global skips auto detect', async (t) => { + const cwd = process.cwd() + t.teardown(() => process.chdir(cwd)) + process.chdir(`${path}/workspaces/one`) + + const config = new Config({ + npmPath: process.cwd(), + env: {}, + argv: [process.execPath, __filename, '--global'], + cwd: `${path}/workspaces/one`, + shorthands, + definitions, + }) + + await config.load() + t.equal(config.localPrefix, join(path, 'workspaces', 'one'), 'localPrefix is the root') + t.same(config.get('workspace'), [], 'did not set workspace') + t.equal(logs.length, 0, 'got no log messages') + }) + + t.test('location=global skips auto detect', async (t) => { + const cwd = process.cwd() + t.teardown(() => process.chdir(cwd)) + process.chdir(`${path}/workspaces/one`) + + const config = new Config({ + npmPath: process.cwd(), + env: {}, + argv: [process.execPath, __filename, '--location=global'], + cwd: `${path}/workspaces/one`, + shorthands, + definitions, + }) + + await config.load() + t.equal(config.localPrefix, join(path, 'workspaces', 'one'), 'localPrefix is the root') + t.same(config.get('workspace'), [], 'did not set workspace') + t.equal(logs.length, 0, 'got no log messages') + }) + + t.test('does not error for invalid package.json', async (t) => { + const invalidPkg = join(path, 'workspaces', 'package.json') + const cwd = process.cwd() + t.teardown(() => { + fs.unlinkSync(invalidPkg) + process.chdir(cwd) + }) + process.chdir(`${path}/workspaces/one`) + + // write some garbage to the file so read-package-json-fast will throw + fs.writeFileSync(invalidPkg, 'not-json') + const config = new Config({ + npmPath: cwd, + env: {}, + argv: [process.execPath, __filename], + cwd: `${path}/workspaces/one`, + shorthands, + definitions, + }) + + await config.load() + t.equal(config.localPrefix, path, 'localPrefix is the root') + t.same(config.get('workspace'), [join(path, 'workspaces', 'one')], 'set the workspace') + t.equal(logs.length, 1, 'got one log message') + t.match(logs[0], ['info', /^found workspace root at/], 'logged info about workspace root') + }) +}) diff --git a/workspaces/config/test/nerf-dart.js b/workspaces/config/test/nerf-dart.js new file mode 100644 index 000000000..8c175a51f --- /dev/null +++ b/workspaces/config/test/nerf-dart.js @@ -0,0 +1,44 @@ +const t = require('tap') +const nerfDart = require('../lib/nerf-dart.js') + +const cases = [ + ['//registry.npmjs.org/', [ + 'https://registry.npmjs.org', + 'https://registry.npmjs.org/package-name', + 'https://registry.npmjs.org/package-name?write=true', + 'https://registry.npmjs.org/@scope%2fpackage-name', + 'https://registry.npmjs.org/@scope%2fpackage-name?write=true', + 'https://username:password@registry.npmjs.org/package-name?write=true', + 'https://registry.npmjs.org/#hash', + 'https://registry.npmjs.org/?write=true#hash', + 'https://registry.npmjs.org/package-name?write=true#hash', + 'https://registry.npmjs.org/package-name#hash', + 'https://registry.npmjs.org/@scope%2fpackage-name?write=true#hash', + 'https://registry.npmjs.org/@scope%2fpackage-name#hash', + ]], + ['//my-couch:5984/registry/_design/app/rewrite/', [ + 'https://my-couch:5984/registry/_design/app/rewrite/', + 'https://my-couch:5984/registry/_design/app/rewrite/package-name', + 'https://my-couch:5984/registry/_design/app/rewrite/package-name?write=true', + 'https://my-couch:5984/registry/_design/app/rewrite/@scope%2fpackage-name', + 'https://my-couch:5984/registry/_design/app/rewrite/@scope%2fpackage-name?write=true', + 'https://username:password@my-couch:5984/registry/_design/app/rewrite/package-name?write=true', + 'https://my-couch:5984/registry/_design/app/rewrite/#hash', + 'https://my-couch:5984/registry/_design/app/rewrite/?write=true#hash', + 'https://my-couch:5984/registry/_design/app/rewrite/package-name?write=true#hash', + 'https://my-couch:5984/registry/_design/app/rewrite/package-name#hash', + 'https://my-couch:5984/registry/_design/app/rewrite/@scope%2fpackage-name?write=true#hash', + 'https://my-couch:5984/registry/_design/app/rewrite/@scope%2fpackage-name#hash', + ]], +] + +for (const [dart, tests] of cases) { + t.test(dart, t => { + t.plan(tests.length) + for (const url of tests) { + t.equal(nerfDart(url), dart, url) + } + }) +} + +t.throws(() => nerfDart('not a valid url')) diff --git a/workspaces/config/test/parse-field.js b/workspaces/config/test/parse-field.js new file mode 100644 index 000000000..1c4193b73 --- /dev/null +++ b/workspaces/config/test/parse-field.js @@ -0,0 +1,36 @@ +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/set-envs.js b/workspaces/config/test/set-envs.js new file mode 100644 index 000000000..c663c2236 --- /dev/null +++ b/workspaces/config/test/set-envs.js @@ -0,0 +1,212 @@ +const setEnvs = require('../lib/set-envs.js') + +const { join } = require('path') +const t = require('tap') +const defaults = require('./fixtures/defaults.js') +const definitions = require('./fixtures/definitions.js') +const { execPath } = process +const cwd = process.cwd() +const globalPrefix = join(cwd, 'global') +const localPrefix = join(cwd, 'local') +const NODE = execPath + +t.test('set envs that are not defaults and not already in env', t => { + const envConf = Object.create(defaults) + const cliConf = Object.create(envConf) + const extras = { + NODE, + INIT_CWD: cwd, + EDITOR: 'vim', + HOME: undefined, + npm_execpath: require.main.filename, + npm_node_execpath: execPath, + npm_config_global_prefix: globalPrefix, + npm_config_local_prefix: localPrefix, + } + + const env = {} + const config = { + list: [cliConf, envConf], + env, + defaults, + definitions, + execPath, + globalPrefix, + localPrefix, + } + + setEnvs(config) + t.strictSame(env, { ...extras }, 'no new environment vars to create') + envConf.call = 'me, maybe' + setEnvs(config) + t.strictSame(env, { ...extras }, 'no new environment vars to create, already in env') + delete envConf.call + cliConf.call = 'me, maybe' + setEnvs(config) + t.strictSame(env, { + ...extras, + npm_config_call: 'me, maybe', + }, 'set in env, because changed from default in cli') + envConf.call = 'me, maybe' + cliConf.call = '' + cliConf['node-options'] = 'some options for node' + setEnvs(config) + t.strictSame(env, { + ...extras, + npm_config_call: '', + npm_config_node_options: 'some options for node', + NODE_OPTIONS: 'some options for node', + }, 'set in env, because changed from default in env, back to default in cli') + t.end() +}) + +t.test('set envs that are not defaults and not already in env, array style', t => { + const envConf = Object.create(defaults) + const cliConf = Object.create(envConf) + const extras = { + NODE, + INIT_CWD: cwd, + EDITOR: 'vim', + HOME: undefined, + npm_execpath: require.main.filename, + npm_node_execpath: execPath, + npm_config_global_prefix: globalPrefix, + npm_config_local_prefix: localPrefix, + } + // make sure it's not sticky + const env = { INIT_CWD: '/some/other/path' } + const config = { + list: [cliConf, envConf], + env, + defaults, + definitions, + execPath, + globalPrefix, + localPrefix, + } + setEnvs(config) + t.strictSame(env, { ...extras }, 'no new environment vars to create') + + envConf.omit = ['dev'] + setEnvs(config) + t.strictSame(env, { ...extras }, 'no new environment vars to create, already in env') + delete envConf.omit + cliConf.omit = ['dev', 'optional'] + setEnvs(config) + t.strictSame(env, { + ...extras, + npm_config_omit: 'dev\n\noptional', + }, 'set in env, because changed from default in cli') + envConf.omit = ['optional', 'peer'] + cliConf.omit = [] + setEnvs(config) + t.strictSame(env, { + ...extras, + npm_config_omit: '', + }, 'set in env, because changed from default in env, back to default in cli') + t.end() +}) + +t.test('set envs that are not defaults and not already in env, boolean edition', t => { + const envConf = Object.create(defaults) + const cliConf = Object.create(envConf) + const extras = { + NODE, + INIT_CWD: cwd, + EDITOR: 'vim', + HOME: undefined, + npm_execpath: require.main.filename, + npm_node_execpath: execPath, + npm_config_global_prefix: globalPrefix, + npm_config_local_prefix: localPrefix, + } + + const env = {} + const config = { + list: [cliConf, envConf], + env, + defaults, + definitions, + execPath, + globalPrefix, + localPrefix, + } + setEnvs(config) + t.strictSame(env, { ...extras }, 'no new environment vars to create') + envConf.audit = false + setEnvs(config) + t.strictSame(env, { ...extras }, 'no new environment vars to create, already in env') + delete envConf.audit + cliConf.audit = false + cliConf.ignoreObjects = { + some: { object: 12345 }, + } + setEnvs(config) + t.strictSame(env, { + ...extras, + npm_config_audit: '', + }, 'set in env, because changed from default in cli') + envConf.audit = false + cliConf.audit = true + setEnvs(config) + t.strictSame(env, { + ...extras, + npm_config_audit: 'true', + }, 'set in env, because changed from default in env, back to default in cli') + t.end() +}) + +t.test('dont set npm_execpath if require.main.filename is not set', t => { + const { filename } = require.main + t.teardown(() => require.main.filename = filename) + require.main.filename = null + // also, don't set editor + const d = { ...defaults, editor: null } + const envConf = Object.create(d) + const cliConf = Object.create(envConf) + const env = { DESTDIR: '/some/dest' } + const config = { + list: [cliConf, envConf], + env, + defaults: d, + definitions, + execPath, + globalPrefix, + localPrefix, + } + setEnvs(config) + t.equal(env.npm_execpath, undefined, 'did not set npm_execpath') + t.end() +}) + +t.test('dont set configs marked as envExport:false', t => { + const envConf = Object.create(defaults) + const cliConf = Object.create(envConf) + const extras = { + NODE, + INIT_CWD: cwd, + EDITOR: 'vim', + HOME: undefined, + npm_execpath: require.main.filename, + npm_node_execpath: execPath, + npm_config_global_prefix: globalPrefix, + npm_config_local_prefix: localPrefix, + } + + const env = {} + const config = { + list: [cliConf, envConf], + env, + defaults, + definitions, + execPath, + globalPrefix, + localPrefix, + } + setEnvs(config) + t.strictSame(env, { ...extras }, 'no new environment vars to create') + cliConf.methane = 'CO2' + setEnvs(config) + t.strictSame(env, { ...extras }, 'not exported, because envExport=false') + t.end() +}) diff --git a/workspaces/config/test/type-defs.js b/workspaces/config/test/type-defs.js new file mode 100644 index 000000000..2ce0ac91d --- /dev/null +++ b/workspaces/config/test/type-defs.js @@ -0,0 +1,22 @@ +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 new file mode 100644 index 000000000..d487c1189 --- /dev/null +++ b/workspaces/config/test/type-description.js @@ -0,0 +1,14 @@ +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) -- cgit v1.2.3