From 6df246fd721c8c6697035ca70ec109c689cafeef Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Sun, 30 Oct 2022 17:42:46 -0700 Subject: [PATCH] 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 + package-lock.json | 41 +- release-please-config.json | 3 +- workspaces/config/.eslintrc.js | 17 + workspaces/config/.gitignore | 21 + workspaces/config/CHANGELOG.md | 133 + .../@npmcli => workspaces}/config/LICENSE | 0 workspaces/config/README.md | 260 ++ .../config/lib/env-replace.js | 0 .../config/lib/errors.js | 0 .../config/lib/index.js | 0 .../config/lib/nerf-dart.js | 0 .../config/lib/parse-field.js | 0 .../config/lib/set-envs.js | 0 .../config/lib/type-defs.js | 0 .../config/lib/type-description.js | 0 .../config/lib/umask.js | 0 workspaces/config/map.js | 1 + .../config/package.json | 11 +- workspaces/config/scripts/example.js | 43 + .../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 + .../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 + 39 files changed, 5945 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/ci-npmcli-config.yml create mode 120000 node_modules/@npmcli/config create mode 100644 workspaces/config/.eslintrc.js create mode 100644 workspaces/config/.gitignore create mode 100644 workspaces/config/CHANGELOG.md rename {node_modules/@npmcli => workspaces}/config/LICENSE (100%) create mode 100644 workspaces/config/README.md rename {node_modules/@npmcli => workspaces}/config/lib/env-replace.js (100%) rename {node_modules/@npmcli => workspaces}/config/lib/errors.js (100%) rename {node_modules/@npmcli => workspaces}/config/lib/index.js (100%) rename {node_modules/@npmcli => workspaces}/config/lib/nerf-dart.js (100%) rename {node_modules/@npmcli => workspaces}/config/lib/parse-field.js (100%) rename {node_modules/@npmcli => workspaces}/config/lib/set-envs.js (100%) rename {node_modules/@npmcli => workspaces}/config/lib/type-defs.js (100%) rename {node_modules/@npmcli => workspaces}/config/lib/type-description.js (100%) rename {node_modules/@npmcli => workspaces}/config/lib/umask.js (100%) create mode 100644 workspaces/config/map.js rename {node_modules/@npmcli => workspaces}/config/package.json (83%) 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 0000000000000..72cc302e721ad --- /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 c857a68a63508..2ab23adf79548 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 60c37e232da3b..ebe46512723ba 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 efe532d542db7..7e9e26ec99102 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 0000000000000..bf09f370d87e3 --- /dev/null +++ b/node_modules/@npmcli/config @@ -0,0 +1 @@ +../../workspaces/config \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b28986fb3cea1..2b468079d68db 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 f5cc462840c51..389df42704f13 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 0000000000000..5db9f815536f1 --- /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 0000000000000..79af2bfcaa4d8 --- /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 0000000000000..b3b30f8630107 --- /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/node_modules/@npmcli/config/LICENSE b/workspaces/config/LICENSE similarity index 100% rename from node_modules/@npmcli/config/LICENSE rename to workspaces/config/LICENSE diff --git a/workspaces/config/README.md b/workspaces/config/README.md new file mode 100644 index 0000000000000..32418381a4c76 --- /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/node_modules/@npmcli/config/lib/env-replace.js b/workspaces/config/lib/env-replace.js similarity index 100% rename from node_modules/@npmcli/config/lib/env-replace.js rename to workspaces/config/lib/env-replace.js diff --git a/node_modules/@npmcli/config/lib/errors.js b/workspaces/config/lib/errors.js similarity index 100% rename from node_modules/@npmcli/config/lib/errors.js rename to workspaces/config/lib/errors.js diff --git a/node_modules/@npmcli/config/lib/index.js b/workspaces/config/lib/index.js similarity index 100% rename from node_modules/@npmcli/config/lib/index.js rename to workspaces/config/lib/index.js diff --git a/node_modules/@npmcli/config/lib/nerf-dart.js b/workspaces/config/lib/nerf-dart.js similarity index 100% rename from node_modules/@npmcli/config/lib/nerf-dart.js rename to workspaces/config/lib/nerf-dart.js diff --git a/node_modules/@npmcli/config/lib/parse-field.js b/workspaces/config/lib/parse-field.js similarity index 100% rename from node_modules/@npmcli/config/lib/parse-field.js rename to workspaces/config/lib/parse-field.js diff --git a/node_modules/@npmcli/config/lib/set-envs.js b/workspaces/config/lib/set-envs.js similarity index 100% rename from node_modules/@npmcli/config/lib/set-envs.js rename to workspaces/config/lib/set-envs.js diff --git a/node_modules/@npmcli/config/lib/type-defs.js b/workspaces/config/lib/type-defs.js similarity index 100% rename from node_modules/@npmcli/config/lib/type-defs.js rename to workspaces/config/lib/type-defs.js diff --git a/node_modules/@npmcli/config/lib/type-description.js b/workspaces/config/lib/type-description.js similarity index 100% rename from node_modules/@npmcli/config/lib/type-description.js rename to workspaces/config/lib/type-description.js diff --git a/node_modules/@npmcli/config/lib/umask.js b/workspaces/config/lib/umask.js similarity index 100% rename from node_modules/@npmcli/config/lib/umask.js rename to workspaces/config/lib/umask.js diff --git a/workspaces/config/map.js b/workspaces/config/map.js new file mode 100644 index 0000000000000..0b263fbecedf1 --- /dev/null +++ b/workspaces/config/map.js @@ -0,0 +1 @@ +module.exports = t => t.replace(/^test/, 'lib') diff --git a/node_modules/@npmcli/config/package.json b/workspaces/config/package.json similarity index 83% rename from node_modules/@npmcli/config/package.json rename to workspaces/config/package.json index 3293ffe5cd7e1..b9f41d29bd3f8 100644 --- a/node_modules/@npmcli/config/package.json +++ b/workspaces/config/package.json @@ -9,7 +9,8 @@ "description": "Configuration management for the npm cli", "repository": { "type": "git", - "url": "https://github.com/npm/config.git" + "url": "https://github.com/npm/cli.git", + "directory": "workspaces/config" }, "author": "GitHub Inc.", "license": "ISC", @@ -18,8 +19,8 @@ "snap": "tap", "lint": "eslint \"**/*.js\"", "postlint": "template-oss-check", - "lintfix": "npm run lint -- --fix", - "posttest": "npm run lint", + "lintfix": "node ../.. run lint -- --fix", + "posttest": "node ../.. run lint", "template-oss-apply": "template-oss-apply --force" }, "tap": { @@ -32,7 +33,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^4.0.0", - "@npmcli/template-oss": "4.5.1", + "@npmcli/template-oss": "4.8.0", "tap": "^16.0.1" }, "dependencies": { @@ -49,6 +50,6 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.5.1" + "version": "4.8.0" } } diff --git a/workspaces/config/scripts/example.js b/workspaces/config/scripts/example.js new file mode 100644 index 0000000000000..bbb2992a5de8a --- /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 0000000000000..6680fd2377960 --- /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 0000000000000..9d80f7e09c315 --- /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 0000000000000..c2b570364de87 --- /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 0000000000000..0bc922b25c59a --- /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 0000000000000..322ceb018bf31 --- /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 0000000000000..ce0aff6f3c457 --- /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 0000000000000..588d05bf0d77d --- /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 0000000000000..5c460c6617175 --- /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 0000000000000..0f8cedfd6e6ac --- /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 0000000000000..8dbee05880619 --- /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 0000000000000..8c175a51f079b --- /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 0000000000000..1c4193b73c986 --- /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 0000000000000..c663c2236d203 --- /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 0000000000000..2ce0ac91dc4cd --- /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 0000000000000..d487c118940ec --- /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)