├── map.js
├── .release-please-manifest.json
├── .github
├── settings.yml
├── CODEOWNERS
├── ISSUE_TEMPLATE
│ ├── config.yml
│ └── bug.yml
├── dependabot.yml
├── matchers
│ └── tap.json
└── workflows
│ ├── codeql-analysis.yml
│ ├── audit.yml
│ ├── pull-request.yml
│ ├── ci.yml
│ ├── post-dependabot.yml
│ ├── ci-release.yml
│ └── release.yml
├── .npmrc
├── SECURITY.md
├── CODE_OF_CONDUCT.md
├── .commitlintrc.js
├── test
├── env-replace.js
├── type-description.js
├── type-defs.js
├── fixtures
│ ├── shorthands.js
│ ├── flatten.js
│ ├── cafile
│ ├── defaults.js
│ └── types.js
├── parse-field.js
├── nerf-dart.js
├── set-envs.js
└── index.js
├── .eslintrc.js
├── lib
├── env-replace.js
├── nerf-dart.js
├── type-description.js
├── errors.js
├── umask.js
├── type-defs.js
├── parse-field.js
├── set-envs.js
└── index.js
├── .gitignore
├── LICENSE
├── release-please-config.json
├── scripts
└── example.js
├── package.json
├── CHANGELOG.md
├── tap-snapshots
└── test
│ ├── index.js.test.cjs
│ └── type-description.js.test.cjs
└── README.md
/map.js:
--------------------------------------------------------------------------------
1 | module.exports = t => t.replace(/^test/, 'lib')
2 |
--------------------------------------------------------------------------------
/.release-please-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | ".": "6.0.1"
3 | }
4 |
--------------------------------------------------------------------------------
/.github/settings.yml:
--------------------------------------------------------------------------------
1 | ---
2 | _extends: '.github:npm-cli/settings.yml'
3 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | ; This file is automatically added by @npmcli/template-oss. Do not edit.
2 |
3 | package-lock=false
4 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # This file is automatically added by @npmcli/template-oss. Do not edit.
2 |
3 | * @npm/cli-team
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | # This file is automatically added by @npmcli/template-oss. Do not edit.
2 |
3 | blank_issues_enabled: true
4 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Please send vulnerability reports through [hackerone](https://hackerone.com/github).
4 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | All interactions in this repo are covered by the [npm Code of
4 | Conduct](https://docs.npmjs.com/policies/conduct)
5 |
6 | The npm cli team may, at its own discretion, moderate, remove, or edit
7 | any interactions such as pull requests, issues, and comments.
8 |
--------------------------------------------------------------------------------
/.commitlintrc.js:
--------------------------------------------------------------------------------
1 | /* This file is automatically added by @npmcli/template-oss. Do not edit. */
2 |
3 | module.exports = {
4 | extends: ['@commitlint/config-conventional'],
5 | rules: {
6 | 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'deps', 'chore']],
7 | 'header-max-length': [2, 'always', 80],
8 | 'subject-case': [0, 'always', ['lower-case', 'sentence-case', 'start-case']],
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/test/env-replace.js:
--------------------------------------------------------------------------------
1 | const envReplace = require('../lib/env-replace.js')
2 | const t = require('tap')
3 |
4 | const env = {
5 | foo: 'bar',
6 | bar: 'baz',
7 | }
8 |
9 | t.equal(envReplace('\\${foo}', env), '${foo}')
10 | t.equal(envReplace('\\\\${foo}', env), '\\bar')
11 | t.equal(envReplace('${baz}', env), '${baz}')
12 | t.equal(envReplace('\\${baz}', env), '${baz}')
13 | t.equal(envReplace('\\\\${baz}', env), '\\${baz}')
14 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /* This file is automatically added by @npmcli/template-oss. Do not edit. */
2 |
3 | 'use strict'
4 |
5 | const { readdirSync: readdir } = require('fs')
6 |
7 | const localConfigs = readdir(__dirname)
8 | .filter((file) => file.startsWith('.eslintrc.local.'))
9 | .map((file) => `./${file}`)
10 |
11 | module.exports = {
12 | root: true,
13 | extends: [
14 | '@npmcli',
15 | ...localConfigs,
16 | ],
17 | }
18 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # This file is automatically added by @npmcli/template-oss. Do not edit.
2 |
3 | version: 2
4 |
5 | updates:
6 | - package-ecosystem: npm
7 | directory: /
8 | schedule:
9 | interval: daily
10 | allow:
11 | - dependency-type: direct
12 | versioning-strategy: increase-if-necessary
13 | commit-message:
14 | prefix: deps
15 | prefix-development: chore
16 | labels:
17 | - "Dependencies"
18 |
--------------------------------------------------------------------------------
/lib/env-replace.js:
--------------------------------------------------------------------------------
1 | // replace any ${ENV} values with the appropriate environ.
2 |
3 | const envExpr = /(? f.replace(envExpr, (orig, esc, name) => {
6 | const val = env[name] !== undefined ? env[name] : `$\{${name}}`
7 |
8 | // consume the escape chars that are relevant.
9 | if (esc.length % 2) {
10 | return orig.slice((esc.length + 1) / 2)
11 | }
12 |
13 | return (esc.slice(esc.length / 2)) + val
14 | })
15 |
--------------------------------------------------------------------------------
/test/type-description.js:
--------------------------------------------------------------------------------
1 | const t = require('tap')
2 | const typeDescription = require('../lib/type-description.js')
3 | const types = require('./fixtures/types.js')
4 | const descriptions = {}
5 | for (const [name, type] of Object.entries(types)) {
6 | const desc = typeDescription(type)
7 | if (name === 'local-address') {
8 | t.strictSame(desc.sort(), type.filter(t => t !== undefined).sort())
9 | } else {
10 | descriptions[name] = desc
11 | }
12 | }
13 |
14 | t.matchSnapshot(descriptions)
15 |
--------------------------------------------------------------------------------
/lib/nerf-dart.js:
--------------------------------------------------------------------------------
1 | const { URL } = require('url')
2 |
3 | /**
4 | * Maps a URL to an identifier.
5 | *
6 | * Name courtesy schiffertronix media LLC, a New Jersey corporation
7 | *
8 | * @param {String} uri The URL to be nerfed.
9 | *
10 | * @returns {String} A nerfed URL.
11 | */
12 | module.exports = (url) => {
13 | const parsed = new URL(url)
14 | const from = `${parsed.protocol}//${parsed.host}${parsed.pathname}`
15 | const rel = new URL('.', from)
16 | const res = `//${rel.host}${rel.pathname}`
17 | return res
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # This file is automatically added by @npmcli/template-oss. Do not edit.
2 |
3 | # ignore everything in the root
4 | /*
5 |
6 | # keep these
7 | !**/.gitignore
8 | !/.commitlintrc.js
9 | !/.eslintrc.js
10 | !/.eslintrc.local.*
11 | !/.github/
12 | !/.gitignore
13 | !/.npmrc
14 | !/.release-please-manifest.json
15 | !/bin/
16 | !/CHANGELOG*
17 | !/CODE_OF_CONDUCT.md
18 | !/docs/
19 | !/lib/
20 | !/LICENSE*
21 | !/map.js
22 | !/package.json
23 | !/README*
24 | !/release-please-config.json
25 | !/scripts/
26 | !/SECURITY.md
27 | !/tap-snapshots/
28 | !/test/
29 |
--------------------------------------------------------------------------------
/lib/type-description.js:
--------------------------------------------------------------------------------
1 | // return the description of the valid values of a field
2 | // returns a string for one thing, or an array of descriptions
3 | const typeDefs = require('./type-defs.js')
4 | const typeDescription = t => {
5 | if (!t || typeof t !== 'function' && typeof t !== 'object') {
6 | return t
7 | }
8 |
9 | if (Array.isArray(t)) {
10 | return t.map(t => typeDescription(t))
11 | }
12 |
13 | for (const { type, description } of Object.values(typeDefs)) {
14 | if (type === t) {
15 | return description || type
16 | }
17 | }
18 |
19 | return t
20 | }
21 | module.exports = t => [].concat(typeDescription(t)).filter(t => t !== undefined)
22 |
--------------------------------------------------------------------------------
/lib/errors.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | class ErrInvalidAuth extends Error {
4 | constructor (problems) {
5 | let message = 'Invalid auth configuration found: '
6 | message += problems.map((problem) => {
7 | if (problem.action === 'delete') {
8 | return `\`${problem.key}\` is not allowed in ${problem.where} config`
9 | } else if (problem.action === 'rename') {
10 | return `\`${problem.from}\` must be renamed to \`${problem.to}\` in ${problem.where} config`
11 | }
12 | }).join(', ')
13 | message += '\nPlease run `npm config fix` to repair your configuration.`'
14 | super(message)
15 | this.code = 'ERR_INVALID_AUTH'
16 | this.problems = problems
17 | }
18 | }
19 |
20 | module.exports = {
21 | ErrInvalidAuth,
22 | }
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The ISC License
2 |
3 | Copyright (c) npm, Inc.
4 |
5 | Permission to use, copy, modify, and/or distribute this software for any
6 | purpose with or without fee is hereby granted, provided that the above
7 | copyright notice and this permission notice appear in all copies.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
15 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16 |
--------------------------------------------------------------------------------
/test/type-defs.js:
--------------------------------------------------------------------------------
1 | const typeDefs = require('../lib/type-defs.js')
2 | const t = require('tap')
3 | const {
4 | semver: {
5 | validate: validateSemver,
6 | },
7 | path: {
8 | validate: validatePath,
9 | },
10 | } = typeDefs
11 | const { resolve } = require('path')
12 |
13 | const d = { semver: 'foobar', somePath: true }
14 | t.equal(validateSemver(d, 'semver', 'foobar'), false)
15 | t.equal(validateSemver(d, 'semver', 'v1.2.3'), undefined)
16 | t.equal(d.semver, '1.2.3')
17 | t.equal(validatePath(d, 'somePath', true), false)
18 | t.equal(validatePath(d, 'somePath', false), false)
19 | t.equal(validatePath(d, 'somePath', null), false)
20 | t.equal(validatePath(d, 'somePath', 1234), false)
21 | t.equal(validatePath(d, 'somePath', 'false'), true)
22 | t.equal(d.somePath, resolve('false'))
23 |
--------------------------------------------------------------------------------
/.github/matchers/tap.json:
--------------------------------------------------------------------------------
1 | {
2 | "//@npmcli/template-oss": "This file is automatically added by @npmcli/template-oss. Do not edit.",
3 | "problemMatcher": [
4 | {
5 | "owner": "tap",
6 | "pattern": [
7 | {
8 | "regexp": "^\\s*not ok \\d+ - (.*)",
9 | "message": 1
10 | },
11 | {
12 | "regexp": "^\\s*---"
13 | },
14 | {
15 | "regexp": "^\\s*at:"
16 | },
17 | {
18 | "regexp": "^\\s*line:\\s*(\\d+)",
19 | "line": 1
20 | },
21 | {
22 | "regexp": "^\\s*column:\\s*(\\d+)",
23 | "column": 1
24 | },
25 | {
26 | "regexp": "^\\s*file:\\s*(.*)",
27 | "file": 1
28 | }
29 | ]
30 | }
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/lib/umask.js:
--------------------------------------------------------------------------------
1 | class Umask {}
2 | const parse = val => {
3 | if (typeof val === 'string') {
4 | if (/^0o?[0-7]+$/.test(val)) {
5 | return parseInt(val.replace(/^0o?/, ''), 8)
6 | } else if (/^[1-9][0-9]*$/.test(val)) {
7 | return parseInt(val, 10)
8 | } else {
9 | throw new Error(`invalid umask value: ${val}`)
10 | }
11 | }
12 | if (typeof val !== 'number') {
13 | throw new Error(`invalid umask value: ${val}`)
14 | }
15 | val = Math.floor(val)
16 | if (val < 0 || val > 511) {
17 | throw new Error(`invalid umask value: ${val}`)
18 | }
19 | return val
20 | }
21 |
22 | const validate = (data, k, val) => {
23 | try {
24 | data[k] = parse(val)
25 | return true
26 | } catch (er) {
27 | return false
28 | }
29 | }
30 |
31 | module.exports = { Umask, parse, validate }
32 |
--------------------------------------------------------------------------------
/release-please-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "exclude-packages-from-root": true,
3 | "group-pull-request-title-pattern": "chore: release ${version}",
4 | "pull-request-title-pattern": "chore: release${component} ${version}",
5 | "changelog-sections": [
6 | {
7 | "type": "feat",
8 | "section": "Features",
9 | "hidden": false
10 | },
11 | {
12 | "type": "fix",
13 | "section": "Bug Fixes",
14 | "hidden": false
15 | },
16 | {
17 | "type": "docs",
18 | "section": "Documentation",
19 | "hidden": false
20 | },
21 | {
22 | "type": "deps",
23 | "section": "Dependencies",
24 | "hidden": false
25 | },
26 | {
27 | "type": "chore",
28 | "hidden": true
29 | }
30 | ],
31 | "packages": {
32 | ".": {
33 | "package-name": ""
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # This file is automatically added by @npmcli/template-oss. Do not edit.
2 |
3 | name: CodeQL
4 |
5 | on:
6 | push:
7 | branches:
8 | - main
9 | - latest
10 | pull_request:
11 | branches:
12 | - main
13 | - latest
14 | schedule:
15 | # "At 10:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1
16 | - cron: "0 10 * * 1"
17 |
18 | jobs:
19 | analyze:
20 | name: Analyze
21 | runs-on: ubuntu-latest
22 | permissions:
23 | actions: read
24 | contents: read
25 | security-events: write
26 | steps:
27 | - name: Checkout
28 | uses: actions/checkout@v3
29 | - name: Setup Git User
30 | run: |
31 | git config --global user.email "npm-cli+bot@github.com"
32 | git config --global user.name "npm CLI robot"
33 | - name: Initialize CodeQL
34 | uses: github/codeql-action/init@v2
35 | with:
36 | languages: javascript
37 | - name: Perform CodeQL Analysis
38 | uses: github/codeql-action/analyze@v2
39 |
--------------------------------------------------------------------------------
/test/fixtures/shorthands.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 'enjoy-by': ['--before'],
3 | a: ['--all'],
4 | c: ['--call'],
5 | s: ['--loglevel', 'silent'],
6 | d: ['--loglevel', 'info'],
7 | dd: ['--loglevel', 'verbose'],
8 | ddd: ['--loglevel', 'silly'],
9 | noreg: ['--no-registry'],
10 | N: ['--no-registry'],
11 | reg: ['--registry'],
12 | 'no-reg': ['--no-registry'],
13 | silent: ['--loglevel', 'silent'],
14 | verbose: ['--loglevel', 'verbose'],
15 | quiet: ['--loglevel', 'warn'],
16 | q: ['--loglevel', 'warn'],
17 | h: ['--usage'],
18 | H: ['--usage'],
19 | '?': ['--usage'],
20 | help: ['--usage'],
21 | v: ['--version'],
22 | f: ['--force'],
23 | desc: ['--description'],
24 | 'no-desc': ['--no-description'],
25 | local: ['--no-global'],
26 | l: ['--long'],
27 | m: ['--message'],
28 | p: ['--parseable'],
29 | porcelain: ['--parseable'],
30 | readonly: ['--read-only'],
31 | g: ['--global'],
32 | S: ['--save'],
33 | D: ['--save-dev'],
34 | E: ['--save-exact'],
35 | O: ['--save-optional'],
36 | P: ['--save-prod'],
37 | y: ['--yes'],
38 | n: ['--no-yes'],
39 | B: ['--save-bundle'],
40 | C: ['--prefix'],
41 | }
42 |
--------------------------------------------------------------------------------
/.github/workflows/audit.yml:
--------------------------------------------------------------------------------
1 | # This file is automatically added by @npmcli/template-oss. Do not edit.
2 |
3 | name: Audit
4 |
5 | on:
6 | workflow_dispatch:
7 | schedule:
8 | # "At 08:00 UTC (01:00 PT) on Monday" https://crontab.guru/#0_8_*_*_1
9 | - cron: "0 8 * * 1"
10 |
11 | jobs:
12 | audit:
13 | name: Audit Dependencies
14 | if: github.repository_owner == 'npm'
15 | runs-on: ubuntu-latest
16 | defaults:
17 | run:
18 | shell: bash
19 | steps:
20 | - name: Checkout
21 | uses: actions/checkout@v3
22 | - name: Setup Git User
23 | run: |
24 | git config --global user.email "npm-cli+bot@github.com"
25 | git config --global user.name "npm CLI robot"
26 | - name: Setup Node
27 | uses: actions/setup-node@v3
28 | with:
29 | node-version: 18.x
30 | - name: Install npm@latest
31 | run: npm i --prefer-online --no-fund --no-audit -g npm@latest
32 | - name: npm Version
33 | run: npm -v
34 | - name: Install Dependencies
35 | run: npm i --ignore-scripts --no-audit --no-fund --package-lock
36 | - name: Run Production Audit
37 | run: npm audit --omit=dev
38 | - name: Run Full Audit
39 | run: npm audit --audit-level=none
40 |
--------------------------------------------------------------------------------
/test/fixtures/flatten.js:
--------------------------------------------------------------------------------
1 | // use the defined flattening function, and copy over any scoped
2 | // registries and registry-specific "nerfdart" configs verbatim
3 | //
4 | // TODO: make these getters so that we only have to make dirty
5 | // the thing that changed, and then flatten the fields that
6 | // could have changed when a config.set is called.
7 | //
8 | // TODO: move nerfdart auth stuff into a nested object that
9 | // is only passed along to paths that end up calling npm-registry-fetch.
10 | const definitions = require('./definitions.js')
11 | const flatten = (obj, flat = {}) => {
12 | for (const [key, val] of Object.entries(obj)) {
13 | const def = definitions[key]
14 | if (def && def.flatten) {
15 | def.flatten(key, obj, flat)
16 | } else if (/@.*:registry$/i.test(key) || /^\/\//.test(key)) {
17 | flat[key] = val
18 | }
19 | }
20 |
21 | // XXX make this the bin/npm-cli.js file explicitly instead
22 | // otherwise using npm programmatically is a bit of a pain.
23 | flat.npmBin = require.main ? require.main.filename
24 | : /* istanbul ignore next - not configurable property */ undefined
25 | flat.nodeBin = process.env.NODE || process.execPath
26 |
27 | // XXX should this be sha512? is it even relevant?
28 | flat.hashAlgorithm = 'sha1'
29 |
30 | return flat
31 | }
32 |
33 | module.exports = flatten
34 |
--------------------------------------------------------------------------------
/scripts/example.js:
--------------------------------------------------------------------------------
1 | const Config = require('../')
2 |
3 | const shorthands = require('../test/fixtures/shorthands.js')
4 | const types = require('../test/fixtures/types.js')
5 | const defaults = require('../test/fixtures/defaults.js')
6 |
7 | const npmPath = __dirname
8 |
9 | const timers = {}
10 | process.on('time', k => {
11 | if (timers[k]) {
12 | throw new Error('duplicate timer: ' + k)
13 | }
14 | timers[k] = process.hrtime()
15 | })
16 | process.on('timeEnd', k => {
17 | if (!timers[k]) {
18 | throw new Error('ending unstarted timer: ' + k)
19 | }
20 | const dur = process.hrtime(timers[k])
21 | delete timers[k]
22 | console.error(`\x1B[2m${k}\x1B[22m`, Math.round(dur[0] * 1e6 + dur[1] / 1e3) / 1e3)
23 | delete timers[k]
24 | })
25 |
26 | process.on('log', (level, ...message) =>
27 | console.log(`\x1B[31m${level}\x1B[39m`, ...message))
28 |
29 | const priv = /(^|:)_([^=]+)=(.*)\n/g
30 | const ini = require('ini')
31 | const config = new Config({ shorthands, types, defaults, npmPath })
32 | config.load().then(async () => {
33 | for (const [where, { data, source }] of config.data.entries()) {
34 | console.log(`; ${where} from ${source}`)
35 | if (where === 'default' && !config.get('long')) {
36 | console.log('; not shown, run with -l to show all\n')
37 | } else {
38 | console.log(ini.stringify(data).replace(priv, '$1_$2=******\n'))
39 | }
40 | }
41 | console.log('argv:', { raw: config.argv, parsed: config.parsedArgv })
42 | return undefined
43 | }).catch(() => {})
44 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@npmcli/config",
3 | "version": "6.0.1",
4 | "files": [
5 | "bin/",
6 | "lib/"
7 | ],
8 | "main": "lib/index.js",
9 | "description": "Configuration management for the npm cli",
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/npm/config.git"
13 | },
14 | "author": "GitHub Inc.",
15 | "license": "ISC",
16 | "scripts": {
17 | "test": "tap",
18 | "snap": "tap",
19 | "lint": "eslint \"**/*.js\"",
20 | "postlint": "template-oss-check",
21 | "lintfix": "npm run lint -- --fix",
22 | "posttest": "npm run lint",
23 | "template-oss-apply": "template-oss-apply --force"
24 | },
25 | "tap": {
26 | "check-coverage": true,
27 | "coverage-map": "map.js",
28 | "nyc-arg": [
29 | "--exclude",
30 | "tap-snapshots/**"
31 | ]
32 | },
33 | "devDependencies": {
34 | "@npmcli/eslint-config": "^4.0.0",
35 | "@npmcli/template-oss": "4.8.0",
36 | "tap": "^16.0.1"
37 | },
38 | "dependencies": {
39 | "@npmcli/map-workspaces": "^3.0.0",
40 | "ini": "^3.0.0",
41 | "nopt": "^7.0.0",
42 | "proc-log": "^3.0.0",
43 | "read-package-json-fast": "^3.0.0",
44 | "semver": "^7.3.5",
45 | "walk-up-path": "^1.0.0"
46 | },
47 | "engines": {
48 | "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
49 | },
50 | "templateOSS": {
51 | "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
52 | "version": "4.8.0"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/.github/workflows/pull-request.yml:
--------------------------------------------------------------------------------
1 | # This file is automatically added by @npmcli/template-oss. Do not edit.
2 |
3 | name: Pull Request
4 |
5 | on:
6 | pull_request:
7 | types:
8 | - opened
9 | - reopened
10 | - edited
11 | - synchronize
12 |
13 | jobs:
14 | commitlint:
15 | name: Lint Commits
16 | if: github.repository_owner == 'npm'
17 | runs-on: ubuntu-latest
18 | defaults:
19 | run:
20 | shell: bash
21 | steps:
22 | - name: Checkout
23 | uses: actions/checkout@v3
24 | with:
25 | fetch-depth: 0
26 | - name: Setup Git User
27 | run: |
28 | git config --global user.email "npm-cli+bot@github.com"
29 | git config --global user.name "npm CLI robot"
30 | - name: Setup Node
31 | uses: actions/setup-node@v3
32 | with:
33 | node-version: 18.x
34 | - name: Install npm@latest
35 | run: npm i --prefer-online --no-fund --no-audit -g npm@latest
36 | - name: npm Version
37 | run: npm -v
38 | - name: Install Dependencies
39 | run: npm i --ignore-scripts --no-audit --no-fund
40 | - name: Run Commitlint on Commits
41 | id: commit
42 | continue-on-error: true
43 | run: |
44 | npx --offline commitlint -V --from origin/${{ github.base_ref }} --to ${{ github.event.pull_request.head.sha }}
45 | - name: Run Commitlint on PR Title
46 | if: steps.commit.outcome == 'failure'
47 | run: |
48 | echo ${{ github.event.pull_request.title }} | npx --offline commitlint -V
49 |
--------------------------------------------------------------------------------
/lib/type-defs.js:
--------------------------------------------------------------------------------
1 | const nopt = require('nopt')
2 |
3 | const { Umask, validate: validateUmask } = require('./umask.js')
4 |
5 | const semver = require('semver')
6 | const validateSemver = (data, k, val) => {
7 | const valid = semver.valid(val)
8 | if (!valid) {
9 | return false
10 | }
11 | data[k] = valid
12 | }
13 |
14 | const noptValidatePath = nopt.typeDefs.path.validate
15 | const validatePath = (data, k, val) => {
16 | if (typeof val !== 'string') {
17 | return false
18 | }
19 | return noptValidatePath(data, k, val)
20 | }
21 |
22 | // add descriptions so we can validate more usefully
23 | module.exports = {
24 | ...nopt.typeDefs,
25 | semver: {
26 | type: semver,
27 | validate: validateSemver,
28 | description: 'full valid SemVer string',
29 | },
30 | Umask: {
31 | type: Umask,
32 | validate: validateUmask,
33 | description: 'octal number in range 0o000..0o777 (0..511)',
34 | },
35 | url: {
36 | ...nopt.typeDefs.url,
37 | description: 'full url with "http://"',
38 | },
39 | path: {
40 | ...nopt.typeDefs.path,
41 | validate: validatePath,
42 | description: 'valid filesystem path',
43 | },
44 | Number: {
45 | ...nopt.typeDefs.Number,
46 | description: 'numeric value',
47 | },
48 | Boolean: {
49 | ...nopt.typeDefs.Boolean,
50 | description: 'boolean value (true or false)',
51 | },
52 | Date: {
53 | ...nopt.typeDefs.Date,
54 | description: 'valid Date string',
55 | },
56 | }
57 |
58 | // TODO: make nopt less of a global beast so this kludge isn't necessary
59 | nopt.typeDefs = module.exports
60 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug.yml:
--------------------------------------------------------------------------------
1 | # This file is automatically added by @npmcli/template-oss. Do not edit.
2 |
3 | name: Bug
4 | description: File a bug/issue
5 | title: "[BUG]
"
6 | labels: [ Bug, Needs Triage ]
7 |
8 | body:
9 | - type: checkboxes
10 | attributes:
11 | label: Is there an existing issue for this?
12 | description: Please [search here](./issues) to see if an issue already exists for your problem.
13 | options:
14 | - label: I have searched the existing issues
15 | required: true
16 | - type: textarea
17 | attributes:
18 | label: Current Behavior
19 | description: A clear & concise description of what you're experiencing.
20 | validations:
21 | required: false
22 | - type: textarea
23 | attributes:
24 | label: Expected Behavior
25 | description: A clear & concise description of what you expected to happen.
26 | validations:
27 | required: false
28 | - type: textarea
29 | attributes:
30 | label: Steps To Reproduce
31 | description: Steps to reproduce the behavior.
32 | value: |
33 | 1. In this environment...
34 | 2. With this config...
35 | 3. Run '...'
36 | 4. See error...
37 | validations:
38 | required: false
39 | - type: textarea
40 | attributes:
41 | label: Environment
42 | description: |
43 | examples:
44 | - **npm**: 7.6.3
45 | - **Node**: 13.14.0
46 | - **OS**: Ubuntu 20.04
47 | - **platform**: Macbook Pro
48 | value: |
49 | - npm:
50 | - Node:
51 | - OS:
52 | - platform:
53 | validations:
54 | required: false
55 |
--------------------------------------------------------------------------------
/test/parse-field.js:
--------------------------------------------------------------------------------
1 | const parseField = require('../lib/parse-field.js')
2 | const t = require('tap')
3 | const { resolve } = require('path')
4 |
5 | t.strictSame(parseField({ a: 1 }, 'a'), { a: 1 })
6 |
7 | const opts = {
8 | platform: 'posix',
9 | types: require('./fixtures/types.js'),
10 | home: '/home/user',
11 | env: { foo: 'bar' },
12 | }
13 |
14 | t.equal(parseField('', 'global', opts), true, 'boolean flag')
15 | t.equal(parseField('true', 'global', opts), true, 'boolean flag "true"')
16 | t.equal(parseField('false', 'global', opts), false, 'boolean flag "false"')
17 | t.equal(parseField('null', 'access', opts), null, '"null" is null')
18 | t.equal(parseField('undefined', 'access', opts), undefined, '"undefined" is undefined')
19 | t.equal(parseField('blerg', 'access', opts), 'blerg', '"blerg" just is a string')
20 | t.equal(parseField('blerg', 'message', opts), 'blerg', '"blerg" just is a string')
21 | t.strictSame(parseField([], 'global', opts), [], 'array passed to non-list type')
22 | t.strictSame(parseField([' dev '], 'omit', opts), ['dev'], 'array to list type')
23 | t.strictSame(parseField('dev\n\noptional', 'omit', opts), ['dev', 'optional'],
24 | 'double-LF delimited list, like we support in env vals')
25 | t.equal(parseField('~/foo', 'userconfig', opts), resolve('/home/user/foo'),
26 | 'path supports ~/')
27 | t.equal(parseField('~\\foo', 'userconfig', { ...opts, platform: 'win32' }),
28 | resolve('/home/user/foo'), 'path supports ~\\ on windows')
29 | t.equal(parseField('foo', 'userconfig', opts), resolve('foo'),
30 | 'path gets resolved')
31 |
32 | t.equal(parseField('1234', 'maxsockets', opts), 1234, 'number is parsed')
33 |
34 | t.equal(parseField('0888', 'umask', opts), '0888',
35 | 'invalid umask is not parsed (will warn later)')
36 | t.equal(parseField('0777', 'umask', opts), 0o777, 'valid umask is parsed')
37 |
--------------------------------------------------------------------------------
/test/fixtures/cafile:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIICjTCCAfigAwIBAgIEMaYgRzALBgkqhkiG9w0BAQQwRTELMAkGA1UEBhMCVVMx
3 | NjA0BgNVBAoTLU5hdGlvbmFsIEFlcm9uYXV0aWNzIGFuZCBTcGFjZSBBZG1pbmlz
4 | dHJhdGlvbjAmFxE5NjA1MjgxMzQ5MDUrMDgwMBcROTgwNTI4MTM0OTA1KzA4MDAw
5 | ZzELMAkGA1UEBhMCVVMxNjA0BgNVBAoTLU5hdGlvbmFsIEFlcm9uYXV0aWNzIGFu
6 | ZCBTcGFjZSBBZG1pbmlzdHJhdGlvbjEgMAkGA1UEBRMCMTYwEwYDVQQDEwxTdGV2
7 | ZSBTY2hvY2gwWDALBgkqhkiG9w0BAQEDSQAwRgJBALrAwyYdgxmzNP/ts0Uyf6Bp
8 | miJYktU/w4NG67ULaN4B5CnEz7k57s9o3YY3LecETgQ5iQHmkwlYDTL2fTgVfw0C
9 | AQOjgaswgagwZAYDVR0ZAQH/BFowWDBWMFQxCzAJBgNVBAYTAlVTMTYwNAYDVQQK
10 | Ey1OYXRpAAAAACBBZXJvbmF1dGljcyBhbmQgU3BhY2UgQWRtaW5pc3RyYXRpb24x
11 | DTALBgNVBAMTBENSTDEwFwYDVR0BAQH/BA0wC4AJODMyOTcwODEwMBgGA1UdAgQR
12 | MA8ECTgzMjk3MDgyM4ACBSAwDQYDVR0KBAYwBAMCBkAwCwYJKoZIhvcNAQEEA4GB
13 | AH2y1VCEw/A4zaXzSYZJTTUi3uawbbFiS2yxHvgf28+8Js0OHXk1H1w2d6qOHH21
14 | X82tZXd/0JtG0g1T9usFFBDvYK8O0ebgz/P5ELJnBL2+atObEuJy1ZZ0pBDWINR3
15 | WkDNLCGiTkCKp0F5EWIrVDwh54NNevkCQRZita+z4IBO
16 | -----END CERTIFICATE-----
17 | -----BEGIN CERTIFICATE-----
18 | AAAAAACCAfigAwIBAgIEMaYgRzALBgkqhkiG9w0BAQQwRTELMAkGA1UEBhMCVVMx
19 | NjA0BgNVBAoTLU5hdGlvbmFsIEFlcm9uYXV0aWNzIGFuZCBTcGFjZSBBZG1pbmlz
20 | dHJhdGlvbjAmFxE5NjA1MjgxMzQ5MDUrMDgwMBcROTgwNTI4MTM0OTA1KzA4MDAw
21 | ZzELMAkGA1UEBhMCVVMxNjA0BgNVBAoTLU5hdGlvbmFsIEFlcm9uYXV0aWNzIGFu
22 | ZCBTcGFjZSBBZG1pbmlzdHJhdGlvbjEgMAkGA1UEBRMCMTYwEwYDVQQDEwxTdGV2
23 | ZSBTY2hvY2gwWDALBgkqhkiG9w0BAQEDSQAwRgJBALrAwyYdgxmzNP/ts0Uyf6Bp
24 | miJYktU/w4NG67ULaN4B5CnEz7k57s9o3YY3LecETgQ5iQHmkwlYDTL2fTgVfw0C
25 | AQOjgaswgagwZAYDVR0ZAQH/BFowWDBWMFQxCzAJBgNVBAYTAlVTMTYwNAYDVQQK
26 | Ey1OYXRpb25hbCBBZXJvbmF1dGljcyBhbmQgU3BhY2UgQWRtaW5pc3RyYXRpb24x
27 | DTALBgNVBAMTBENSTDEwFwYDVR0BAQH/BA0wC4AJODMyOTcwODEwMBgGA1UdAgQR
28 | MA8ECTgzMjk3MDgyM4ACBSAwDQYDVR0KBAYwBAMCBkAwCwYJKoZIhvcNAQEEA4GB
29 | AH2y1VCEw/A4zaXzSYZJTTUi3uawbbFiS2yxHvgf28+8Js0OHXk1H1w2d6qOHH21
30 | X82tZXd/0JtG0g1T9usFFBDvYK8O0ebgz/P5ELJnBL2+atObEuJy1ZZ0pBDWINR3
31 | WkDNLCGiTkCKp0F5EWIrVDwh54NNevkCQRZita+z4IBO
32 | -----END CERTIFICATE-----
33 |
--------------------------------------------------------------------------------
/test/nerf-dart.js:
--------------------------------------------------------------------------------
1 | const t = require('tap')
2 | const nerfDart = require('../lib/nerf-dart.js')
3 |
4 | const cases = [
5 | ['//registry.npmjs.org/', [
6 | 'https://registry.npmjs.org',
7 | 'https://registry.npmjs.org/package-name',
8 | 'https://registry.npmjs.org/package-name?write=true',
9 | 'https://registry.npmjs.org/@scope%2fpackage-name',
10 | 'https://registry.npmjs.org/@scope%2fpackage-name?write=true',
11 | 'https://username:password@registry.npmjs.org/package-name?write=true',
12 | 'https://registry.npmjs.org/#hash',
13 | 'https://registry.npmjs.org/?write=true#hash',
14 | 'https://registry.npmjs.org/package-name?write=true#hash',
15 | 'https://registry.npmjs.org/package-name#hash',
16 | 'https://registry.npmjs.org/@scope%2fpackage-name?write=true#hash',
17 | 'https://registry.npmjs.org/@scope%2fpackage-name#hash',
18 | ]],
19 | ['//my-couch:5984/registry/_design/app/rewrite/', [
20 | 'https://my-couch:5984/registry/_design/app/rewrite/',
21 | 'https://my-couch:5984/registry/_design/app/rewrite/package-name',
22 | 'https://my-couch:5984/registry/_design/app/rewrite/package-name?write=true',
23 | 'https://my-couch:5984/registry/_design/app/rewrite/@scope%2fpackage-name',
24 | 'https://my-couch:5984/registry/_design/app/rewrite/@scope%2fpackage-name?write=true',
25 | 'https://username:password@my-couch:5984/registry/_design/app/rewrite/package-name?write=true',
26 | 'https://my-couch:5984/registry/_design/app/rewrite/#hash',
27 | 'https://my-couch:5984/registry/_design/app/rewrite/?write=true#hash',
28 | 'https://my-couch:5984/registry/_design/app/rewrite/package-name?write=true#hash',
29 | 'https://my-couch:5984/registry/_design/app/rewrite/package-name#hash',
30 | 'https://my-couch:5984/registry/_design/app/rewrite/@scope%2fpackage-name?write=true#hash',
31 | 'https://my-couch:5984/registry/_design/app/rewrite/@scope%2fpackage-name#hash',
32 | ]],
33 | ]
34 |
35 | for (const [dart, tests] of cases) {
36 | t.test(dart, t => {
37 | t.plan(tests.length)
38 | for (const url of tests) {
39 | t.equal(nerfDart(url), dart, url)
40 | }
41 | })
42 | }
43 |
44 | t.throws(() => nerfDart('not a valid url'))
45 |
--------------------------------------------------------------------------------
/lib/parse-field.js:
--------------------------------------------------------------------------------
1 | // Parse a field, coercing it to the best type available.
2 | const typeDefs = require('./type-defs.js')
3 | const envReplace = require('./env-replace.js')
4 | const { resolve } = require('path')
5 |
6 | const { parse: umaskParse } = require('./umask.js')
7 |
8 | const parseField = (f, key, opts, listElement = false) => {
9 | if (typeof f !== 'string' && !Array.isArray(f)) {
10 | return f
11 | }
12 |
13 | const { platform, types, home, env } = opts
14 |
15 | // type can be array or a single thing. coerce to array.
16 | const typeList = new Set([].concat(types[key]))
17 | const isPath = typeList.has(typeDefs.path.type)
18 | const isBool = typeList.has(typeDefs.Boolean.type)
19 | const isString = isPath || typeList.has(typeDefs.String.type)
20 | const isUmask = typeList.has(typeDefs.Umask.type)
21 | const isNumber = typeList.has(typeDefs.Number.type)
22 | const isList = !listElement && typeList.has(Array)
23 |
24 | if (Array.isArray(f)) {
25 | return !isList ? f : f.map(field => parseField(field, key, opts, true))
26 | }
27 |
28 | // now we know it's a string
29 | f = f.trim()
30 |
31 | // list types get put in the environment separated by double-\n
32 | // usually a single \n would suffice, but ca/cert configs can contain
33 | // line breaks and multiple entries.
34 | if (isList) {
35 | return parseField(f.split('\n\n'), key, opts)
36 | }
37 |
38 | // --foo is like --foo=true for boolean types
39 | if (isBool && !isString && f === '') {
40 | return true
41 | }
42 |
43 | // string types can be the string 'true', 'false', etc.
44 | // otherwise, parse these values out
45 | if (!isString && !isPath && !isNumber) {
46 | switch (f) {
47 | case 'true': return true
48 | case 'false': return false
49 | case 'null': return null
50 | case 'undefined': return undefined
51 | }
52 | }
53 |
54 | f = envReplace(f, env)
55 |
56 | if (isPath) {
57 | const homePattern = platform === 'win32' ? /^~(\/|\\)/ : /^~\//
58 | if (homePattern.test(f) && home) {
59 | f = resolve(home, f.slice(2))
60 | } else {
61 | f = resolve(f)
62 | }
63 | }
64 |
65 | if (isUmask) {
66 | try {
67 | return umaskParse(f)
68 | } catch (er) {
69 | // let it warn later when we validate
70 | return f
71 | }
72 | }
73 |
74 | if (isNumber && !isNaN(f)) {
75 | f = +f
76 | }
77 |
78 | return f
79 | }
80 |
81 | module.exports = parseField
82 |
--------------------------------------------------------------------------------
/test/fixtures/defaults.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | methane: 'CH4',
3 | access: null,
4 | all: false,
5 | 'allow-same-version': false,
6 | 'always-auth': false,
7 | also: null,
8 | audit: true,
9 | 'audit-level': null,
10 | 'auth-type': 'legacy',
11 |
12 | before: null,
13 | 'bin-links': true,
14 | browser: null,
15 |
16 | ca: null,
17 | cafile: null,
18 |
19 | cache: '~/.npm',
20 |
21 | 'cache-lock-stale': 60000,
22 | 'cache-lock-retries': 10,
23 | 'cache-lock-wait': 10000,
24 |
25 | 'cache-max': Infinity,
26 | 'cache-min': 10,
27 |
28 | cert: null,
29 |
30 | cidr: null,
31 |
32 | color: true,
33 | call: '',
34 | depth: 0,
35 | description: true,
36 | dev: false,
37 | 'dry-run': false,
38 | editor: 'vim',
39 | 'engine-strict': false,
40 | force: false,
41 | 'format-package-lock': true,
42 |
43 | fund: true,
44 |
45 | 'fetch-retries': 2,
46 | 'fetch-retry-factor': 10,
47 | 'fetch-retry-mintimeout': 10000,
48 | 'fetch-retry-maxtimeout': 60000,
49 |
50 | git: 'git',
51 | 'git-tag-version': true,
52 | 'commit-hooks': true,
53 |
54 | global: false,
55 | 'global-style': false,
56 | heading: 'npm',
57 | 'if-present': false,
58 | include: [],
59 | 'include-staged': false,
60 | 'ignore-prepublish': false,
61 | 'ignore-scripts': false,
62 | 'init-module': '~/.npm-init.js',
63 | 'init-author-name': '',
64 | 'init-author-email': '',
65 | 'init-author-url': '',
66 | 'init-version': '1.0.0',
67 | 'init-license': 'ISC',
68 | json: false,
69 | key: null,
70 | 'legacy-bundling': false,
71 | 'legacy-peer-deps': false,
72 | link: false,
73 | 'local-address': undefined,
74 | loglevel: 'notice',
75 | 'logs-max': 10,
76 | long: false,
77 | maxsockets: 50,
78 | message: '%s',
79 | 'metrics-registry': null,
80 | 'node-options': null,
81 | 'node-version': process.version,
82 | offline: false,
83 | omit: [],
84 | only: null,
85 | optional: true,
86 | otp: null,
87 | package: [],
88 | 'package-lock': true,
89 | 'package-lock-only': false,
90 | parseable: false,
91 | 'prefer-offline': false,
92 | 'prefer-online': false,
93 | preid: '',
94 | production: true,
95 | progress: true,
96 | proxy: null,
97 | 'https-proxy': null,
98 | noproxy: null,
99 | 'user-agent': 'npm/{npm-version} ' +
100 | 'node/{node-version} ' +
101 | '{platform} ' +
102 | '{arch} ' +
103 | '{ci}',
104 | 'read-only': false,
105 | 'rebuild-bundle': true,
106 | registry: 'https://registry.npmjs.org/',
107 | rollback: true,
108 | save: true,
109 | 'save-bundle': false,
110 | 'save-dev': false,
111 | 'save-exact': false,
112 | 'save-optional': false,
113 | 'save-prefix': '^',
114 | 'save-prod': false,
115 | scope: '',
116 | 'script-shell': null,
117 | 'scripts-prepend-node-path': 'warn-only',
118 | searchopts: '',
119 | searchexclude: null,
120 | searchlimit: 20,
121 | searchstaleness: 15 * 60,
122 | 'send-metrics': false,
123 | shell: '/bin/sh',
124 | shrinkwrap: true,
125 | 'sign-git-commit': false,
126 | 'sign-git-tag': false,
127 | 'sso-poll-frequency': 500,
128 | 'sso-type': 'oauth',
129 | 'strict-ssl': true,
130 | tag: 'latest',
131 | 'tag-version-prefix': 'v',
132 | timing: false,
133 | unicode: /UTF-?8$/i.test(
134 | process.env.LC_ALL || process.env.LC_CTYPE || process.env.LANG
135 | ),
136 | 'update-notifier': true,
137 | usage: false,
138 | userconfig: '~/.npmrc',
139 | umask: 0o22,
140 | version: false,
141 | versions: false,
142 | viewer: 'man',
143 | }
144 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | # This file is automatically added by @npmcli/template-oss. Do not edit.
2 |
3 | name: CI
4 |
5 | on:
6 | workflow_dispatch:
7 | pull_request:
8 | push:
9 | branches:
10 | - main
11 | - latest
12 | schedule:
13 | # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1
14 | - cron: "0 9 * * 1"
15 |
16 | jobs:
17 | lint:
18 | name: Lint
19 | if: github.repository_owner == 'npm'
20 | runs-on: ubuntu-latest
21 | defaults:
22 | run:
23 | shell: bash
24 | steps:
25 | - name: Checkout
26 | uses: actions/checkout@v3
27 | - name: Setup Git User
28 | run: |
29 | git config --global user.email "npm-cli+bot@github.com"
30 | git config --global user.name "npm CLI robot"
31 | - name: Setup Node
32 | uses: actions/setup-node@v3
33 | with:
34 | node-version: 18.x
35 | - name: Install npm@latest
36 | run: npm i --prefer-online --no-fund --no-audit -g npm@latest
37 | - name: npm Version
38 | run: npm -v
39 | - name: Install Dependencies
40 | run: npm i --ignore-scripts --no-audit --no-fund
41 | - name: Lint
42 | run: npm run lint --ignore-scripts
43 | - name: Post Lint
44 | run: npm run postlint --ignore-scripts
45 |
46 | test:
47 | name: Test - ${{ matrix.platform.name }} - ${{ matrix.node-version }}
48 | if: github.repository_owner == 'npm'
49 | strategy:
50 | fail-fast: false
51 | matrix:
52 | platform:
53 | - name: Linux
54 | os: ubuntu-latest
55 | shell: bash
56 | - name: macOS
57 | os: macos-latest
58 | shell: bash
59 | - name: Windows
60 | os: windows-latest
61 | shell: cmd
62 | node-version:
63 | - 14.17.0
64 | - 14.x
65 | - 16.13.0
66 | - 16.x
67 | - 18.0.0
68 | - 18.x
69 | runs-on: ${{ matrix.platform.os }}
70 | defaults:
71 | run:
72 | shell: ${{ matrix.platform.shell }}
73 | steps:
74 | - name: Checkout
75 | uses: actions/checkout@v3
76 | - name: Setup Git User
77 | run: |
78 | git config --global user.email "npm-cli+bot@github.com"
79 | git config --global user.name "npm CLI robot"
80 | - name: Setup Node
81 | uses: actions/setup-node@v3
82 | with:
83 | node-version: ${{ matrix.node-version }}
84 | - name: Update Windows npm
85 | # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows
86 | if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.'))
87 | run: |
88 | curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz
89 | tar xf npm-7.5.4.tgz
90 | cd package
91 | node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz
92 | cd ..
93 | rmdir /s /q package
94 | - name: Install npm@7
95 | if: startsWith(matrix.node-version, '10.')
96 | run: npm i --prefer-online --no-fund --no-audit -g npm@7
97 | - name: Install npm@latest
98 | if: ${{ !startsWith(matrix.node-version, '10.') }}
99 | run: npm i --prefer-online --no-fund --no-audit -g npm@latest
100 | - name: npm Version
101 | run: npm -v
102 | - name: Install Dependencies
103 | run: npm i --ignore-scripts --no-audit --no-fund
104 | - name: Add Problem Matcher
105 | run: echo "::add-matcher::.github/matchers/tap.json"
106 | - name: Test
107 | run: npm test --ignore-scripts
108 |
--------------------------------------------------------------------------------
/lib/set-envs.js:
--------------------------------------------------------------------------------
1 | // Set environment variables for any non-default configs,
2 | // so that they're already there when we run lifecycle scripts.
3 | //
4 | // See https://github.com/npm/rfcs/pull/90
5 |
6 | // Return the env key if this is a thing that belongs in the env.
7 | // Ie, if the key isn't a @scope, //nerf.dart, or _private,
8 | // and the value is a string or array. Otherwise return false.
9 | const envKey = (key, val) => {
10 | return !/^[/@_]/.test(key) &&
11 | (typeof envVal(val) === 'string') &&
12 | `npm_config_${key.replace(/-/g, '_').toLowerCase()}`
13 | }
14 |
15 | const envVal = val => Array.isArray(val) ? val.map(v => envVal(v)).join('\n\n')
16 | : val === null || val === undefined || val === false ? ''
17 | : typeof val === 'object' ? null
18 | : String(val)
19 |
20 | const sameConfigValue = (def, val) =>
21 | !Array.isArray(val) || !Array.isArray(def) ? def === val
22 | : sameArrayValue(def, val)
23 |
24 | const sameArrayValue = (def, val) => {
25 | if (def.length !== val.length) {
26 | return false
27 | }
28 |
29 | for (let i = 0; i < def.length; i++) {
30 | /* istanbul ignore next - there are no array configs where the default
31 | * is not an empty array, so this loop is a no-op, but it's the correct
32 | * thing to do if we ever DO add a config like that. */
33 | if (def[i] !== val[i]) {
34 | return false
35 | }
36 | }
37 | return true
38 | }
39 |
40 | const setEnv = (env, rawKey, rawVal) => {
41 | const val = envVal(rawVal)
42 | const key = envKey(rawKey, val)
43 | if (key && val !== null) {
44 | env[key] = val
45 | }
46 | }
47 |
48 | const setEnvs = (config) => {
49 | // This ensures that all npm config values that are not the defaults are
50 | // shared appropriately with child processes, without false positives.
51 | const {
52 | env,
53 | defaults,
54 | definitions,
55 | list: [cliConf, envConf],
56 | } = config
57 |
58 | env.INIT_CWD = process.cwd()
59 |
60 | // if the key is deprecated, skip it always.
61 | // if the key is the default value,
62 | // if the environ is NOT the default value,
63 | // set the environ
64 | // else skip it, it's fine
65 | // if the key is NOT the default value,
66 | // if the env is setting it, then leave it (already set)
67 | // otherwise, set the env
68 | const cliSet = new Set(Object.keys(cliConf))
69 | const envSet = new Set(Object.keys(envConf))
70 | for (const key in cliConf) {
71 | const { deprecated, envExport = true } = definitions[key] || {}
72 | if (deprecated || envExport === false) {
73 | continue
74 | }
75 |
76 | if (sameConfigValue(defaults[key], cliConf[key])) {
77 | // config is the default, if the env thought different, then we
78 | // have to set it BACK to the default in the environment.
79 | if (!sameConfigValue(envConf[key], cliConf[key])) {
80 | setEnv(env, key, cliConf[key])
81 | }
82 | } else {
83 | // config is not the default. if the env wasn't the one to set
84 | // it that way, then we have to put it in the env
85 | if (!(envSet.has(key) && !cliSet.has(key))) {
86 | setEnv(env, key, cliConf[key])
87 | }
88 | }
89 | }
90 |
91 | // also set some other common nice envs that we want to rely on
92 | env.HOME = config.home
93 | env.npm_config_global_prefix = config.globalPrefix
94 | env.npm_config_local_prefix = config.localPrefix
95 | if (cliConf.editor) {
96 | env.EDITOR = cliConf.editor
97 | }
98 |
99 | // note: this doesn't afect the *current* node process, of course, since
100 | // it's already started, but it does affect the options passed to scripts.
101 | if (cliConf['node-options']) {
102 | env.NODE_OPTIONS = cliConf['node-options']
103 | }
104 |
105 | if (require.main && require.main.filename) {
106 | env.npm_execpath = require.main.filename
107 | }
108 | env.NODE = env.npm_node_execpath = config.execPath
109 | }
110 |
111 | module.exports = setEnvs
112 |
--------------------------------------------------------------------------------
/test/fixtures/types.js:
--------------------------------------------------------------------------------
1 | const {
2 | String: { type: String },
3 | Boolean: { type: Boolean },
4 | url: { type: url },
5 | Number: { type: Number },
6 | path: { type: path },
7 | Date: { type: Date },
8 | semver: { type: semver },
9 | Umask: { type: Umask },
10 | } = require('../../lib/type-defs.js')
11 |
12 | const { networkInterfaces } = require('os')
13 | const getLocalAddresses = () => {
14 | try {
15 | return Object.values(networkInterfaces()).map(
16 | int => int.map(({ address }) => address)
17 | ).reduce((set, addrs) => set.concat(addrs), [undefined])
18 | } catch (e) {
19 | return [undefined]
20 | }
21 | }
22 |
23 | module.exports = {
24 | access: [null, 'restricted', 'public'],
25 | all: Boolean,
26 | 'allow-same-version': Boolean,
27 | 'always-auth': Boolean,
28 | also: [null, 'dev', 'development'],
29 | audit: Boolean,
30 | 'audit-level': ['low', 'moderate', 'high', 'critical', 'none', null],
31 | 'auth-type': ['legacy', 'sso', 'saml', 'oauth'],
32 | before: [null, Date],
33 | 'bin-links': Boolean,
34 | browser: [null, Boolean, String],
35 | ca: [null, String, Array],
36 | cafile: path,
37 | cache: path,
38 | 'cache-lock-stale': Number,
39 | 'cache-lock-retries': Number,
40 | 'cache-lock-wait': Number,
41 | 'cache-max': Number,
42 | 'cache-min': Number,
43 | cert: [null, String],
44 | cidr: [null, String, Array],
45 | color: ['always', Boolean],
46 | call: String,
47 | depth: Number,
48 | description: Boolean,
49 | dev: Boolean,
50 | 'dry-run': Boolean,
51 | editor: String,
52 | 'engine-strict': Boolean,
53 | force: Boolean,
54 | fund: Boolean,
55 | 'format-package-lock': Boolean,
56 | 'fetch-retries': Number,
57 | 'fetch-retry-factor': Number,
58 | 'fetch-retry-mintimeout': Number,
59 | 'fetch-retry-maxtimeout': Number,
60 | git: String,
61 | 'git-tag-version': Boolean,
62 | 'commit-hooks': Boolean,
63 | global: Boolean,
64 | globalconfig: path,
65 | 'global-style': Boolean,
66 | 'https-proxy': [null, url],
67 | 'user-agent': String,
68 | heading: String,
69 | 'if-present': Boolean,
70 | include: [Array, 'prod', 'dev', 'optional', 'peer'],
71 | 'include-staged': Boolean,
72 | 'ignore-prepublish': Boolean,
73 | 'ignore-scripts': Boolean,
74 | 'init-module': path,
75 | 'init-author-name': String,
76 | 'init-author-email': String,
77 | 'init-author-url': ['', url],
78 | 'init-license': String,
79 | 'init-version': semver,
80 | json: Boolean,
81 | key: [null, String],
82 | 'legacy-bundling': Boolean,
83 | 'legacy-peer-deps': Boolean,
84 | link: Boolean,
85 | 'local-address': getLocalAddresses(),
86 | loglevel: ['silent', 'error', 'warn', 'notice', 'http', 'timing', 'info', 'verbose', 'silly'],
87 | 'logs-max': Number,
88 | long: Boolean,
89 | 'multiple-numbers': [Array, Number],
90 | maxsockets: Number,
91 | message: String,
92 | 'metrics-registry': [null, String],
93 | 'node-options': [null, String],
94 | 'node-version': [null, semver],
95 | noproxy: [null, String, Array],
96 | offline: Boolean,
97 | omit: [Array, 'dev', 'optional', 'peer'],
98 | only: [null, 'dev', 'development', 'prod', 'production'],
99 | optional: Boolean,
100 | otp: [null, String],
101 | package: [String, Array],
102 | 'package-lock': Boolean,
103 | 'package-lock-only': Boolean,
104 | parseable: Boolean,
105 | 'prefer-offline': Boolean,
106 | 'prefer-online': Boolean,
107 | prefix: path,
108 | preid: String,
109 | production: Boolean,
110 | progress: Boolean,
111 | proxy: [null, false, url], // allow proxy to be disabled explicitly
112 | 'read-only': Boolean,
113 | 'rebuild-bundle': Boolean,
114 | registry: [null, url],
115 | rollback: Boolean,
116 | save: Boolean,
117 | 'save-bundle': Boolean,
118 | 'save-dev': Boolean,
119 | 'save-exact': Boolean,
120 | 'save-optional': Boolean,
121 | 'save-prefix': String,
122 | 'save-prod': Boolean,
123 | scope: String,
124 | 'script-shell': [null, String],
125 | 'scripts-prepend-node-path': [Boolean, 'auto', 'warn-only'],
126 | searchopts: String,
127 | searchexclude: [null, String],
128 | searchlimit: Number,
129 | searchstaleness: Number,
130 | 'send-metrics': Boolean,
131 | shell: String,
132 | shrinkwrap: Boolean,
133 | 'sign-git-commit': Boolean,
134 | 'sign-git-tag': Boolean,
135 | 'sso-poll-frequency': Number,
136 | 'sso-type': [null, 'oauth', 'saml'],
137 | 'strict-ssl': Boolean,
138 | tag: String,
139 | timing: Boolean,
140 | tmp: path,
141 | unicode: Boolean,
142 | 'update-notifier': Boolean,
143 | usage: Boolean,
144 | userconfig: path,
145 | umask: Umask,
146 | version: Boolean,
147 | 'tag-version-prefix': String,
148 | versions: Boolean,
149 | viewer: String,
150 | _exit: Boolean,
151 | }
152 |
--------------------------------------------------------------------------------
/.github/workflows/post-dependabot.yml:
--------------------------------------------------------------------------------
1 | # This file is automatically added by @npmcli/template-oss. Do not edit.
2 |
3 | name: Post Dependabot
4 |
5 | on: pull_request
6 |
7 | permissions:
8 | contents: write
9 |
10 | jobs:
11 | template-oss:
12 | name: template-oss
13 | if: github.repository_owner == 'npm' && github.actor == 'dependabot[bot]'
14 | runs-on: ubuntu-latest
15 | defaults:
16 | run:
17 | shell: bash
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@v3
21 | with:
22 | ref: ${{ github.event.pull_request.head.ref }}
23 | - name: Setup Git User
24 | run: |
25 | git config --global user.email "npm-cli+bot@github.com"
26 | git config --global user.name "npm CLI robot"
27 | - name: Setup Node
28 | uses: actions/setup-node@v3
29 | with:
30 | node-version: 18.x
31 | - name: Install npm@latest
32 | run: npm i --prefer-online --no-fund --no-audit -g npm@latest
33 | - name: npm Version
34 | run: npm -v
35 | - name: Install Dependencies
36 | run: npm i --ignore-scripts --no-audit --no-fund
37 | - name: Fetch Dependabot Metadata
38 | id: metadata
39 | uses: dependabot/fetch-metadata@v1
40 | with:
41 | github-token: ${{ secrets.GITHUB_TOKEN }}
42 |
43 | # Dependabot can update multiple directories so we output which directory
44 | # it is acting on so we can run the command for the correct root or workspace
45 | - name: Get Dependabot Directory
46 | if: contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss')
47 | id: flags
48 | run: |
49 | dependabot_dir="${{ steps.metadata.outputs.directory }}"
50 | if [[ "$dependabot_dir" == "/" ]]; then
51 | echo "::set-output name=workspace::-iwr"
52 | else
53 | # strip leading slash from directory so it works as a
54 | # a path to the workspace flag
55 | echo "::set-output name=workspace::-w ${dependabot_dir#/}"
56 | fi
57 |
58 | - name: Apply Changes
59 | if: steps.flags.outputs.workspace
60 | id: apply
61 | run: |
62 | npm run template-oss-apply ${{ steps.flags.outputs.workspace }}
63 | if [[ `git status --porcelain` ]]; then
64 | echo "::set-output name=changes::true"
65 | fi
66 | # This only sets the conventional commit prefix. This workflow can't reliably determine
67 | # what the breaking change is though. If a BREAKING CHANGE message is required then
68 | # this PR check will fail and the commit will be amended with stafftools
69 | if [[ "${{ steps.metadata.outputs.update-type }}" == "version-update:semver-major" ]]; then
70 | prefix='feat!'
71 | else
72 | prefix='chore'
73 | fi
74 | echo "::set-output name=message::$prefix: postinstall for dependabot template-oss PR"
75 |
76 | # This step will fail if template-oss has made any workflow updates. It is impossible
77 | # for a workflow to update other workflows. In the case it does fail, we continue
78 | # and then try to apply only a portion of the changes in the next step
79 | - name: Push All Changes
80 | if: steps.apply.outputs.changes
81 | id: push
82 | continue-on-error: true
83 | env:
84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
85 | run: |
86 | git commit -am "${{ steps.apply.outputs.message }}"
87 | git push
88 |
89 | # If the previous step failed, then reset the commit and remove any workflow changes
90 | # and attempt to commit and push again. This is helpful because we will have a commit
91 | # with the correct prefix that we can then --amend with @npmcli/stafftools later.
92 | - name: Push All Changes Except Workflows
93 | if: steps.apply.outputs.changes && steps.push.outcome == 'failure'
94 | env:
95 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
96 | run: |
97 | git reset HEAD~
98 | git checkout HEAD -- .github/workflows/
99 | git clean -fd .github/workflows/
100 | git commit -am "${{ steps.apply.outputs.message }}"
101 | git push
102 |
103 | # Check if all the necessary template-oss changes were applied. Since we continued
104 | # on errors in one of the previous steps, this check will fail if our follow up
105 | # only applied a portion of the changes and we need to followup manually.
106 | #
107 | # Note that this used to run `lint` and `postlint` but that will fail this action
108 | # if we've also shipped any linting changes separate from template-oss. We do
109 | # linting in another action, so we want to fail this one only if there are
110 | # template-oss changes that could not be applied.
111 | - name: Check Changes
112 | if: steps.apply.outputs.changes
113 | run: |
114 | npm exec --offline ${{ steps.flags.outputs.workspace }} -- template-oss-check
115 |
116 | - name: Fail on Breaking Change
117 | if: steps.apply.outputs.changes && startsWith(steps.apply.outputs.message, 'feat!')
118 | run: |
119 | echo "This PR has a breaking change. Run 'npx -p @npmcli/stafftools gh template-oss-fix'"
120 | echo "for more information on how to fix this with a BREAKING CHANGE footer."
121 | exit 1
122 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [6.0.1](https://github.com/npm/config/compare/v6.0.0...v6.0.1) (2022-10-17)
4 |
5 | ### Dependencies
6 |
7 | * [`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
8 | * [`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
9 |
10 | ## [6.0.0](https://github.com/npm/config/compare/v5.0.0...v6.0.0) (2022-10-13)
11 |
12 | ### ⚠️ BREAKING CHANGES
13 |
14 | * this module no longer attempts to change file ownership automatically
15 |
16 | ### Features
17 |
18 | * [`805535f`](https://github.com/npm/config/commit/805535ff6b7255a3a2fb5e7da392f53b1c2f3c04) [#96](https://github.com/npm/config/pull/96) do not alter file ownership (#96) (@nlf)
19 |
20 | ### Dependencies
21 |
22 | * [`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
23 |
24 | ## [5.0.0](https://github.com/npm/config/compare/v4.2.2...v5.0.0) (2022-10-06)
25 |
26 | ### ⚠️ BREAKING CHANGES
27 |
28 | * 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.
29 | * `@npmcli/config` is now compatible with the following semver range for node: `^14.17.0 || ^16.13.0 || >=18.0.0`
30 |
31 | ### Features
32 |
33 | * [`344ccd3`](https://github.com/npm/config/commit/344ccd3d07979d0cb36dad8a7fe2e9cbbdbdbc9e) [#92](https://github.com/npm/config/pull/92) throw errors for invalid auth configuration (#92) (@nlf)
34 | * [`aa25682`](https://github.com/npm/config/commit/aa256827d76ec9b1aea06eb3ebdd033067a5e604) [#87](https://github.com/npm/config/pull/87) postinstall for dependabot template-oss PR (@lukekarrys)
35 |
36 | ## [4.2.2](https://github.com/npm/config/compare/v4.2.1...v4.2.2) (2022-08-25)
37 |
38 |
39 | ### Bug Fixes
40 |
41 | * warn on bare auth related configs ([#78](https://github.com/npm/config/issues/78)) ([d4e582a](https://github.com/npm/config/commit/d4e582ab7d8d9f4a8615619bb7d3263df5de66e6))
42 |
43 | ## [4.2.1](https://github.com/npm/config/compare/v4.2.0...v4.2.1) (2022-08-09)
44 |
45 |
46 | ### Bug Fixes
47 |
48 | * correctly handle nerf-darted env vars ([#74](https://github.com/npm/config/issues/74)) ([71f559b](https://github.com/npm/config/commit/71f559b08e01616b53f61e1cf385fc44162e2d66))
49 | * linting ([#75](https://github.com/npm/config/issues/75)) ([deb1001](https://github.com/npm/config/commit/deb10011d1b5e3df84b7d13284ea55b07dd62b63))
50 |
51 |
52 | ### Dependencies
53 |
54 | * 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))
55 |
56 | ## [4.2.0](https://github.com/npm/config/compare/v4.1.0...v4.2.0) (2022-07-18)
57 |
58 |
59 | ### Features
60 |
61 | * detect registry-scoped certfile and keyfile options ([#69](https://github.com/npm/config/issues/69)) ([e58a4f1](https://github.com/npm/config/commit/e58a4f18f0ec0820fe57ccaff34c4135ece12558))
62 |
63 | ## [4.1.0](https://github.com/npm/config/compare/v4.0.2...v4.1.0) (2022-04-13)
64 |
65 |
66 | ### Features
67 |
68 | * warn on deprecated config ([#62](https://github.com/npm/config/issues/62)) ([190065e](https://github.com/npm/config/commit/190065ef53d39a1e09486639c710dabdd73d8a7c))
69 |
70 | ### [4.0.2](https://github.com/npm/config/compare/v4.0.1...v4.0.2) (2022-04-05)
71 |
72 |
73 | ### Bug Fixes
74 |
75 | * replace deprecated String.prototype.substr() ([#59](https://github.com/npm/config/issues/59)) ([43893b6](https://github.com/npm/config/commit/43893b638f82ade945cba27fe9e483b32eea99ae))
76 |
77 |
78 | ### Dependencies
79 |
80 | * 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))
81 | * 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))
82 |
83 | ### [4.0.1](https://www.github.com/npm/config/compare/v4.0.0...v4.0.1) (2022-03-02)
84 |
85 |
86 | ### Bug Fixes
87 |
88 | * 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))
89 |
90 |
91 | ### Dependencies
92 |
93 | * 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))
94 |
95 | ## [4.0.0](https://www.github.com/npm/config/compare/v3.0.1...v4.0.0) (2022-02-14)
96 |
97 |
98 | ### ⚠ BREAKING CHANGES
99 |
100 | * drop support for the `log` option
101 |
102 | ### Features
103 |
104 | * remove `log` option ([#40](https://www.github.com/npm/config/issues/40)) ([bbf5128](https://www.github.com/npm/config/commit/bbf512818f30d0764e3951449c8f07856d70991e))
105 |
106 |
107 | ### Bug Fixes
108 |
109 | * correct a polynomial regex ([#39](https://www.github.com/npm/config/issues/39)) ([9af098f](https://www.github.com/npm/config/commit/9af098fb874c1a8122ab7a5e009235a1f7df72f5))
110 |
111 | ### [3.0.1](https://www.github.com/npm/config/compare/v3.0.0...v3.0.1) (2022-02-10)
112 |
113 |
114 | ### Dependencies
115 |
116 | * update semver requirement from ^7.3.4 to ^7.3.5 ([2cb225a](https://www.github.com/npm/config/commit/2cb225a907180a3b569c8c9baf23da1a989a2f1f))
117 | * use proc-log instead of process.emit ([fd4cd42](https://www.github.com/npm/config/commit/fd4cd429ef875ce68aa0be9bba329cae4e7adfe3))
118 |
119 | ## [3.0.0](https://www.github.com/npm/config/compare/v2.4.0...v3.0.0) (2022-02-01)
120 |
121 |
122 | ### ⚠ BREAKING CHANGES
123 |
124 | * this drops support for node10 and non-LTS versions of node12 and node14
125 |
126 | ### Features
127 |
128 | * automatically detect workspace roots ([#28](https://www.github.com/npm/config/issues/28)) ([a3dc623](https://www.github.com/npm/config/commit/a3dc6234d57c7c80c66a8c33e17cf1d97f86f8d9))
129 |
130 |
131 | ### Bug Fixes
132 |
133 | * template-oss ([#29](https://www.github.com/npm/config/issues/29)) ([6440fba](https://www.github.com/npm/config/commit/6440fba6e04b1f87e57b4c2ccc5ea84d8a69b823))
134 |
--------------------------------------------------------------------------------
/test/set-envs.js:
--------------------------------------------------------------------------------
1 | const setEnvs = require('../lib/set-envs.js')
2 |
3 | const { join } = require('path')
4 | const t = require('tap')
5 | const defaults = require('./fixtures/defaults.js')
6 | const definitions = require('./fixtures/definitions.js')
7 | const { execPath } = process
8 | const cwd = process.cwd()
9 | const globalPrefix = join(cwd, 'global')
10 | const localPrefix = join(cwd, 'local')
11 | const NODE = execPath
12 |
13 | t.test('set envs that are not defaults and not already in env', t => {
14 | const envConf = Object.create(defaults)
15 | const cliConf = Object.create(envConf)
16 | const extras = {
17 | NODE,
18 | INIT_CWD: cwd,
19 | EDITOR: 'vim',
20 | HOME: undefined,
21 | npm_execpath: require.main.filename,
22 | npm_node_execpath: execPath,
23 | npm_config_global_prefix: globalPrefix,
24 | npm_config_local_prefix: localPrefix,
25 | }
26 |
27 | const env = {}
28 | const config = {
29 | list: [cliConf, envConf],
30 | env,
31 | defaults,
32 | definitions,
33 | execPath,
34 | globalPrefix,
35 | localPrefix,
36 | }
37 |
38 | setEnvs(config)
39 | t.strictSame(env, { ...extras }, 'no new environment vars to create')
40 | envConf.call = 'me, maybe'
41 | setEnvs(config)
42 | t.strictSame(env, { ...extras }, 'no new environment vars to create, already in env')
43 | delete envConf.call
44 | cliConf.call = 'me, maybe'
45 | setEnvs(config)
46 | t.strictSame(env, {
47 | ...extras,
48 | npm_config_call: 'me, maybe',
49 | }, 'set in env, because changed from default in cli')
50 | envConf.call = 'me, maybe'
51 | cliConf.call = ''
52 | cliConf['node-options'] = 'some options for node'
53 | setEnvs(config)
54 | t.strictSame(env, {
55 | ...extras,
56 | npm_config_call: '',
57 | npm_config_node_options: 'some options for node',
58 | NODE_OPTIONS: 'some options for node',
59 | }, 'set in env, because changed from default in env, back to default in cli')
60 | t.end()
61 | })
62 |
63 | t.test('set envs that are not defaults and not already in env, array style', t => {
64 | const envConf = Object.create(defaults)
65 | const cliConf = Object.create(envConf)
66 | const extras = {
67 | NODE,
68 | INIT_CWD: cwd,
69 | EDITOR: 'vim',
70 | HOME: undefined,
71 | npm_execpath: require.main.filename,
72 | npm_node_execpath: execPath,
73 | npm_config_global_prefix: globalPrefix,
74 | npm_config_local_prefix: localPrefix,
75 | }
76 | // make sure it's not sticky
77 | const env = { INIT_CWD: '/some/other/path' }
78 | const config = {
79 | list: [cliConf, envConf],
80 | env,
81 | defaults,
82 | definitions,
83 | execPath,
84 | globalPrefix,
85 | localPrefix,
86 | }
87 | setEnvs(config)
88 | t.strictSame(env, { ...extras }, 'no new environment vars to create')
89 |
90 | envConf.omit = ['dev']
91 | setEnvs(config)
92 | t.strictSame(env, { ...extras }, 'no new environment vars to create, already in env')
93 | delete envConf.omit
94 | cliConf.omit = ['dev', 'optional']
95 | setEnvs(config)
96 | t.strictSame(env, {
97 | ...extras,
98 | npm_config_omit: 'dev\n\noptional',
99 | }, 'set in env, because changed from default in cli')
100 | envConf.omit = ['optional', 'peer']
101 | cliConf.omit = []
102 | setEnvs(config)
103 | t.strictSame(env, {
104 | ...extras,
105 | npm_config_omit: '',
106 | }, 'set in env, because changed from default in env, back to default in cli')
107 | t.end()
108 | })
109 |
110 | t.test('set envs that are not defaults and not already in env, boolean edition', t => {
111 | const envConf = Object.create(defaults)
112 | const cliConf = Object.create(envConf)
113 | const extras = {
114 | NODE,
115 | INIT_CWD: cwd,
116 | EDITOR: 'vim',
117 | HOME: undefined,
118 | npm_execpath: require.main.filename,
119 | npm_node_execpath: execPath,
120 | npm_config_global_prefix: globalPrefix,
121 | npm_config_local_prefix: localPrefix,
122 | }
123 |
124 | const env = {}
125 | const config = {
126 | list: [cliConf, envConf],
127 | env,
128 | defaults,
129 | definitions,
130 | execPath,
131 | globalPrefix,
132 | localPrefix,
133 | }
134 | setEnvs(config)
135 | t.strictSame(env, { ...extras }, 'no new environment vars to create')
136 | envConf.audit = false
137 | setEnvs(config)
138 | t.strictSame(env, { ...extras }, 'no new environment vars to create, already in env')
139 | delete envConf.audit
140 | cliConf.audit = false
141 | cliConf.ignoreObjects = {
142 | some: { object: 12345 },
143 | }
144 | setEnvs(config)
145 | t.strictSame(env, {
146 | ...extras,
147 | npm_config_audit: '',
148 | }, 'set in env, because changed from default in cli')
149 | envConf.audit = false
150 | cliConf.audit = true
151 | setEnvs(config)
152 | t.strictSame(env, {
153 | ...extras,
154 | npm_config_audit: 'true',
155 | }, 'set in env, because changed from default in env, back to default in cli')
156 | t.end()
157 | })
158 |
159 | t.test('dont set npm_execpath if require.main.filename is not set', t => {
160 | const { filename } = require.main
161 | t.teardown(() => require.main.filename = filename)
162 | require.main.filename = null
163 | // also, don't set editor
164 | const d = { ...defaults, editor: null }
165 | const envConf = Object.create(d)
166 | const cliConf = Object.create(envConf)
167 | const env = { DESTDIR: '/some/dest' }
168 | const config = {
169 | list: [cliConf, envConf],
170 | env,
171 | defaults: d,
172 | definitions,
173 | execPath,
174 | globalPrefix,
175 | localPrefix,
176 | }
177 | setEnvs(config)
178 | t.equal(env.npm_execpath, undefined, 'did not set npm_execpath')
179 | t.end()
180 | })
181 |
182 | t.test('dont set configs marked as envExport:false', t => {
183 | const envConf = Object.create(defaults)
184 | const cliConf = Object.create(envConf)
185 | const extras = {
186 | NODE,
187 | INIT_CWD: cwd,
188 | EDITOR: 'vim',
189 | HOME: undefined,
190 | npm_execpath: require.main.filename,
191 | npm_node_execpath: execPath,
192 | npm_config_global_prefix: globalPrefix,
193 | npm_config_local_prefix: localPrefix,
194 | }
195 |
196 | const env = {}
197 | const config = {
198 | list: [cliConf, envConf],
199 | env,
200 | defaults,
201 | definitions,
202 | execPath,
203 | globalPrefix,
204 | localPrefix,
205 | }
206 | setEnvs(config)
207 | t.strictSame(env, { ...extras }, 'no new environment vars to create')
208 | cliConf.methane = 'CO2'
209 | setEnvs(config)
210 | t.strictSame(env, { ...extras }, 'not exported, because envExport=false')
211 | t.end()
212 | })
213 |
--------------------------------------------------------------------------------
/tap-snapshots/test/index.js.test.cjs:
--------------------------------------------------------------------------------
1 | /* IMPORTANT
2 | * This snapshot file is auto-generated, but designed for humans.
3 | * It should be checked into source control and tracked carefully.
4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests.
5 | * Make sure to inspect the output below. Do not ignore changes!
6 | */
7 | 'use strict'
8 | exports[`test/index.js TAP credentials management def_auth > default registry 1`] = `
9 | Object {
10 | "auth": "aGVsbG86d29ybGQ=",
11 | "password": "world",
12 | "username": "hello",
13 | }
14 | `
15 |
16 | exports[`test/index.js TAP credentials management def_auth > default registry after set 1`] = `
17 | Object {
18 | "auth": "aGVsbG86d29ybGQ=",
19 | "password": "world",
20 | "username": "hello",
21 | }
22 | `
23 |
24 | exports[`test/index.js TAP credentials management def_auth > other registry 1`] = `
25 | Object {}
26 | `
27 |
28 | exports[`test/index.js TAP credentials management def_passNoUser > default registry 1`] = `
29 | Object {
30 | "email": "i@izs.me",
31 | }
32 | `
33 |
34 | exports[`test/index.js TAP credentials management def_passNoUser > other registry 1`] = `
35 | Object {
36 | "email": "i@izs.me",
37 | }
38 | `
39 |
40 | exports[`test/index.js TAP credentials management def_userNoPass > default registry 1`] = `
41 | Object {
42 | "email": "i@izs.me",
43 | }
44 | `
45 |
46 | exports[`test/index.js TAP credentials management def_userNoPass > other registry 1`] = `
47 | Object {
48 | "email": "i@izs.me",
49 | }
50 | `
51 |
52 | exports[`test/index.js TAP credentials management def_userpass > default registry 1`] = `
53 | Object {
54 | "auth": "aGVsbG86d29ybGQ=",
55 | "email": "i@izs.me",
56 | "password": "world",
57 | "username": "hello",
58 | }
59 | `
60 |
61 | exports[`test/index.js TAP credentials management def_userpass > default registry after set 1`] = `
62 | Object {
63 | "auth": "aGVsbG86d29ybGQ=",
64 | "email": "i@izs.me",
65 | "password": "world",
66 | "username": "hello",
67 | }
68 | `
69 |
70 | exports[`test/index.js TAP credentials management def_userpass > other registry 1`] = `
71 | Object {
72 | "email": "i@izs.me",
73 | }
74 | `
75 |
76 | exports[`test/index.js TAP credentials management nerfed_auth > default registry 1`] = `
77 | Object {
78 | "auth": "aGVsbG86d29ybGQ=",
79 | "password": "world",
80 | "username": "hello",
81 | }
82 | `
83 |
84 | exports[`test/index.js TAP credentials management nerfed_auth > default registry after set 1`] = `
85 | Object {
86 | "auth": "aGVsbG86d29ybGQ=",
87 | "password": "world",
88 | "username": "hello",
89 | }
90 | `
91 |
92 | exports[`test/index.js TAP credentials management nerfed_auth > other registry 1`] = `
93 | Object {}
94 | `
95 |
96 | exports[`test/index.js TAP credentials management nerfed_authToken > default registry 1`] = `
97 | Object {
98 | "token": "0bad1de4",
99 | }
100 | `
101 |
102 | exports[`test/index.js TAP credentials management nerfed_authToken > default registry after set 1`] = `
103 | Object {
104 | "token": "0bad1de4",
105 | }
106 | `
107 |
108 | exports[`test/index.js TAP credentials management nerfed_authToken > other registry 1`] = `
109 | Object {}
110 | `
111 |
112 | exports[`test/index.js TAP credentials management nerfed_mtls > default registry 1`] = `
113 | Object {
114 | "certfile": "/path/to/cert",
115 | "keyfile": "/path/to/key",
116 | }
117 | `
118 |
119 | exports[`test/index.js TAP credentials management nerfed_mtls > default registry after set 1`] = `
120 | Object {
121 | "certfile": "/path/to/cert",
122 | "keyfile": "/path/to/key",
123 | }
124 | `
125 |
126 | exports[`test/index.js TAP credentials management nerfed_mtls > other registry 1`] = `
127 | Object {}
128 | `
129 |
130 | exports[`test/index.js TAP credentials management nerfed_mtlsAuthToken > default registry 1`] = `
131 | Object {
132 | "certfile": "/path/to/cert",
133 | "keyfile": "/path/to/key",
134 | "token": "0bad1de4",
135 | }
136 | `
137 |
138 | exports[`test/index.js TAP credentials management nerfed_mtlsAuthToken > default registry after set 1`] = `
139 | Object {
140 | "certfile": "/path/to/cert",
141 | "keyfile": "/path/to/key",
142 | "token": "0bad1de4",
143 | }
144 | `
145 |
146 | exports[`test/index.js TAP credentials management nerfed_mtlsAuthToken > other registry 1`] = `
147 | Object {}
148 | `
149 |
150 | exports[`test/index.js TAP credentials management nerfed_mtlsUserPass > default registry 1`] = `
151 | Object {
152 | "auth": "aGVsbG86d29ybGQ=",
153 | "certfile": "/path/to/cert",
154 | "email": "i@izs.me",
155 | "keyfile": "/path/to/key",
156 | "password": "world",
157 | "username": "hello",
158 | }
159 | `
160 |
161 | exports[`test/index.js TAP credentials management nerfed_mtlsUserPass > default registry after set 1`] = `
162 | Object {
163 | "auth": "aGVsbG86d29ybGQ=",
164 | "certfile": "/path/to/cert",
165 | "email": "i@izs.me",
166 | "keyfile": "/path/to/key",
167 | "password": "world",
168 | "username": "hello",
169 | }
170 | `
171 |
172 | exports[`test/index.js TAP credentials management nerfed_mtlsUserPass > other registry 1`] = `
173 | Object {
174 | "email": "i@izs.me",
175 | }
176 | `
177 |
178 | exports[`test/index.js TAP credentials management nerfed_userpass > default registry 1`] = `
179 | Object {
180 | "auth": "aGVsbG86d29ybGQ=",
181 | "email": "i@izs.me",
182 | "password": "world",
183 | "username": "hello",
184 | }
185 | `
186 |
187 | exports[`test/index.js TAP credentials management nerfed_userpass > default registry after set 1`] = `
188 | Object {
189 | "auth": "aGVsbG86d29ybGQ=",
190 | "email": "i@izs.me",
191 | "password": "world",
192 | "username": "hello",
193 | }
194 | `
195 |
196 | exports[`test/index.js TAP credentials management nerfed_userpass > other registry 1`] = `
197 | Object {
198 | "email": "i@izs.me",
199 | }
200 | `
201 |
202 | exports[`test/index.js TAP credentials management none_authToken > default registry 1`] = `
203 | Object {
204 | "token": "0bad1de4",
205 | }
206 | `
207 |
208 | exports[`test/index.js TAP credentials management none_authToken > default registry after set 1`] = `
209 | Object {
210 | "token": "0bad1de4",
211 | }
212 | `
213 |
214 | exports[`test/index.js TAP credentials management none_authToken > other registry 1`] = `
215 | Object {}
216 | `
217 |
218 | exports[`test/index.js TAP credentials management none_emptyConfig > default registry 1`] = `
219 | Object {}
220 | `
221 |
222 | exports[`test/index.js TAP credentials management none_emptyConfig > other registry 1`] = `
223 | Object {}
224 | `
225 |
226 | exports[`test/index.js TAP credentials management none_lcAuthToken > default registry 1`] = `
227 | Object {}
228 | `
229 |
230 | exports[`test/index.js TAP credentials management none_lcAuthToken > other registry 1`] = `
231 | Object {}
232 | `
233 |
234 | exports[`test/index.js TAP credentials management none_noConfig > default registry 1`] = `
235 | Object {}
236 | `
237 |
238 | exports[`test/index.js TAP credentials management none_noConfig > other registry 1`] = `
239 | Object {}
240 | `
241 |
--------------------------------------------------------------------------------
/.github/workflows/ci-release.yml:
--------------------------------------------------------------------------------
1 | # This file is automatically added by @npmcli/template-oss. Do not edit.
2 |
3 | name: CI - Release
4 |
5 | on:
6 | workflow_dispatch:
7 | inputs:
8 | ref:
9 | required: true
10 | type: string
11 | default: main
12 | workflow_call:
13 | inputs:
14 | ref:
15 | required: true
16 | type: string
17 | check-sha:
18 | required: true
19 | type: string
20 |
21 | jobs:
22 | lint-all:
23 | name: Lint All
24 | if: github.repository_owner == 'npm'
25 | runs-on: ubuntu-latest
26 | defaults:
27 | run:
28 | shell: bash
29 | steps:
30 | - name: Get Workflow Job
31 | uses: actions/github-script@v6
32 | if: inputs.check-sha
33 | id: check-output
34 | env:
35 | JOB_NAME: "Lint All"
36 | MATRIX_NAME: ""
37 | with:
38 | script: |
39 | const { owner, repo } = context.repo
40 |
41 | const { data } = await github.rest.actions.listJobsForWorkflowRun({
42 | owner,
43 | repo,
44 | run_id: context.runId,
45 | per_page: 100
46 | })
47 |
48 | const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME
49 | const job = data.jobs.find(j => j.name.endsWith(jobName))
50 | const jobUrl = job?.html_url
51 |
52 | const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ inputs.check-sha }}`
53 |
54 | let summary = `This check is assosciated with ${shaUrl}\n\n`
55 |
56 | if (jobUrl) {
57 | summary += `For run logs, click here: ${jobUrl}`
58 | } else {
59 | summary += `Run logs could not be found for a job with name: "${jobName}"`
60 | }
61 |
62 | return { summary }
63 | - name: Create Check
64 | uses: LouisBrunner/checks-action@v1.3.1
65 | id: check
66 | if: inputs.check-sha
67 | with:
68 | token: ${{ secrets.GITHUB_TOKEN }}
69 | status: in_progress
70 | name: Lint All
71 | sha: ${{ inputs.check-sha }}
72 | output: ${{ steps.check-output.outputs.result }}
73 | - name: Checkout
74 | uses: actions/checkout@v3
75 | with:
76 | ref: ${{ inputs.ref }}
77 | - name: Setup Git User
78 | run: |
79 | git config --global user.email "npm-cli+bot@github.com"
80 | git config --global user.name "npm CLI robot"
81 | - name: Setup Node
82 | uses: actions/setup-node@v3
83 | with:
84 | node-version: 18.x
85 | - name: Install npm@latest
86 | run: npm i --prefer-online --no-fund --no-audit -g npm@latest
87 | - name: npm Version
88 | run: npm -v
89 | - name: Install Dependencies
90 | run: npm i --ignore-scripts --no-audit --no-fund
91 | - name: Lint
92 | run: npm run lint --ignore-scripts
93 | - name: Post Lint
94 | run: npm run postlint --ignore-scripts
95 | - name: Conclude Check
96 | uses: LouisBrunner/checks-action@v1.3.1
97 | if: steps.check.outputs.check_id && always()
98 | with:
99 | token: ${{ secrets.GITHUB_TOKEN }}
100 | conclusion: ${{ job.status }}
101 | check_id: ${{ steps.check.outputs.check_id }}
102 |
103 | test-all:
104 | name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }}
105 | if: github.repository_owner == 'npm'
106 | strategy:
107 | fail-fast: false
108 | matrix:
109 | platform:
110 | - name: Linux
111 | os: ubuntu-latest
112 | shell: bash
113 | - name: macOS
114 | os: macos-latest
115 | shell: bash
116 | - name: Windows
117 | os: windows-latest
118 | shell: cmd
119 | node-version:
120 | - 14.17.0
121 | - 14.x
122 | - 16.13.0
123 | - 16.x
124 | - 18.0.0
125 | - 18.x
126 | runs-on: ${{ matrix.platform.os }}
127 | defaults:
128 | run:
129 | shell: ${{ matrix.platform.shell }}
130 | steps:
131 | - name: Get Workflow Job
132 | uses: actions/github-script@v6
133 | if: inputs.check-sha
134 | id: check-output
135 | env:
136 | JOB_NAME: "Test All"
137 | MATRIX_NAME: " - ${{ matrix.platform.name }} - ${{ matrix.node-version }}"
138 | with:
139 | script: |
140 | const { owner, repo } = context.repo
141 |
142 | const { data } = await github.rest.actions.listJobsForWorkflowRun({
143 | owner,
144 | repo,
145 | run_id: context.runId,
146 | per_page: 100
147 | })
148 |
149 | const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME
150 | const job = data.jobs.find(j => j.name.endsWith(jobName))
151 | const jobUrl = job?.html_url
152 |
153 | const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ inputs.check-sha }}`
154 |
155 | let summary = `This check is assosciated with ${shaUrl}\n\n`
156 |
157 | if (jobUrl) {
158 | summary += `For run logs, click here: ${jobUrl}`
159 | } else {
160 | summary += `Run logs could not be found for a job with name: "${jobName}"`
161 | }
162 |
163 | return { summary }
164 | - name: Create Check
165 | uses: LouisBrunner/checks-action@v1.3.1
166 | id: check
167 | if: inputs.check-sha
168 | with:
169 | token: ${{ secrets.GITHUB_TOKEN }}
170 | status: in_progress
171 | name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }}
172 | sha: ${{ inputs.check-sha }}
173 | output: ${{ steps.check-output.outputs.result }}
174 | - name: Checkout
175 | uses: actions/checkout@v3
176 | with:
177 | ref: ${{ inputs.ref }}
178 | - name: Setup Git User
179 | run: |
180 | git config --global user.email "npm-cli+bot@github.com"
181 | git config --global user.name "npm CLI robot"
182 | - name: Setup Node
183 | uses: actions/setup-node@v3
184 | with:
185 | node-version: ${{ matrix.node-version }}
186 | - name: Update Windows npm
187 | # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows
188 | if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.'))
189 | run: |
190 | curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz
191 | tar xf npm-7.5.4.tgz
192 | cd package
193 | node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz
194 | cd ..
195 | rmdir /s /q package
196 | - name: Install npm@7
197 | if: startsWith(matrix.node-version, '10.')
198 | run: npm i --prefer-online --no-fund --no-audit -g npm@7
199 | - name: Install npm@latest
200 | if: ${{ !startsWith(matrix.node-version, '10.') }}
201 | run: npm i --prefer-online --no-fund --no-audit -g npm@latest
202 | - name: npm Version
203 | run: npm -v
204 | - name: Install Dependencies
205 | run: npm i --ignore-scripts --no-audit --no-fund
206 | - name: Add Problem Matcher
207 | run: echo "::add-matcher::.github/matchers/tap.json"
208 | - name: Test
209 | run: npm test --ignore-scripts
210 | - name: Conclude Check
211 | uses: LouisBrunner/checks-action@v1.3.1
212 | if: steps.check.outputs.check_id && always()
213 | with:
214 | token: ${{ secrets.GITHUB_TOKEN }}
215 | conclusion: ${{ job.status }}
216 | check_id: ${{ steps.check.outputs.check_id }}
217 |
--------------------------------------------------------------------------------
/tap-snapshots/test/type-description.js.test.cjs:
--------------------------------------------------------------------------------
1 | /* IMPORTANT
2 | * This snapshot file is auto-generated, but designed for humans.
3 | * It should be checked into source control and tracked carefully.
4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests.
5 | * Make sure to inspect the output below. Do not ignore changes!
6 | */
7 | 'use strict'
8 | exports[`test/type-description.js TAP > must match snapshot 1`] = `
9 | Object {
10 | "_exit": Array [
11 | "boolean value (true or false)",
12 | ],
13 | "access": Array [
14 | null,
15 | "restricted",
16 | "public",
17 | ],
18 | "all": Array [
19 | "boolean value (true or false)",
20 | ],
21 | "allow-same-version": Array [
22 | "boolean value (true or false)",
23 | ],
24 | "also": Array [
25 | null,
26 | "dev",
27 | "development",
28 | ],
29 | "always-auth": Array [
30 | "boolean value (true or false)",
31 | ],
32 | "audit": Array [
33 | "boolean value (true or false)",
34 | ],
35 | "audit-level": Array [
36 | "low",
37 | "moderate",
38 | "high",
39 | "critical",
40 | "none",
41 | null,
42 | ],
43 | "auth-type": Array [
44 | "legacy",
45 | "sso",
46 | "saml",
47 | "oauth",
48 | ],
49 | "before": Array [
50 | null,
51 | "valid Date string",
52 | ],
53 | "bin-links": Array [
54 | "boolean value (true or false)",
55 | ],
56 | "browser": Array [
57 | null,
58 | "boolean value (true or false)",
59 | Function String(),
60 | ],
61 | "ca": Array [
62 | null,
63 | Function String(),
64 | Function Array(),
65 | ],
66 | "cache": Array [
67 | "valid filesystem path",
68 | ],
69 | "cache-lock-retries": Array [
70 | "numeric value",
71 | ],
72 | "cache-lock-stale": Array [
73 | "numeric value",
74 | ],
75 | "cache-lock-wait": Array [
76 | "numeric value",
77 | ],
78 | "cache-max": Array [
79 | "numeric value",
80 | ],
81 | "cache-min": Array [
82 | "numeric value",
83 | ],
84 | "cafile": Array [
85 | "valid filesystem path",
86 | ],
87 | "call": Array [
88 | Function String(),
89 | ],
90 | "cert": Array [
91 | null,
92 | Function String(),
93 | ],
94 | "cidr": Array [
95 | null,
96 | Function String(),
97 | Function Array(),
98 | ],
99 | "color": Array [
100 | "always",
101 | "boolean value (true or false)",
102 | ],
103 | "commit-hooks": Array [
104 | "boolean value (true or false)",
105 | ],
106 | "depth": Array [
107 | "numeric value",
108 | ],
109 | "description": Array [
110 | "boolean value (true or false)",
111 | ],
112 | "dev": Array [
113 | "boolean value (true or false)",
114 | ],
115 | "dry-run": Array [
116 | "boolean value (true or false)",
117 | ],
118 | "editor": Array [
119 | Function String(),
120 | ],
121 | "engine-strict": Array [
122 | "boolean value (true or false)",
123 | ],
124 | "fetch-retries": Array [
125 | "numeric value",
126 | ],
127 | "fetch-retry-factor": Array [
128 | "numeric value",
129 | ],
130 | "fetch-retry-maxtimeout": Array [
131 | "numeric value",
132 | ],
133 | "fetch-retry-mintimeout": Array [
134 | "numeric value",
135 | ],
136 | "force": Array [
137 | "boolean value (true or false)",
138 | ],
139 | "format-package-lock": Array [
140 | "boolean value (true or false)",
141 | ],
142 | "fund": Array [
143 | "boolean value (true or false)",
144 | ],
145 | "git": Array [
146 | Function String(),
147 | ],
148 | "git-tag-version": Array [
149 | "boolean value (true or false)",
150 | ],
151 | "global": Array [
152 | "boolean value (true or false)",
153 | ],
154 | "global-style": Array [
155 | "boolean value (true or false)",
156 | ],
157 | "globalconfig": Array [
158 | "valid filesystem path",
159 | ],
160 | "heading": Array [
161 | Function String(),
162 | ],
163 | "https-proxy": Array [
164 | null,
165 | "full url with \\"http://\\"",
166 | ],
167 | "if-present": Array [
168 | "boolean value (true or false)",
169 | ],
170 | "ignore-prepublish": Array [
171 | "boolean value (true or false)",
172 | ],
173 | "ignore-scripts": Array [
174 | "boolean value (true or false)",
175 | ],
176 | "include": Array [
177 | Function Array(),
178 | "prod",
179 | "dev",
180 | "optional",
181 | "peer",
182 | ],
183 | "include-staged": Array [
184 | "boolean value (true or false)",
185 | ],
186 | "init-author-email": Array [
187 | Function String(),
188 | ],
189 | "init-author-name": Array [
190 | Function String(),
191 | ],
192 | "init-author-url": Array [
193 | "",
194 | "full url with \\"http://\\"",
195 | ],
196 | "init-license": Array [
197 | Function String(),
198 | ],
199 | "init-module": Array [
200 | "valid filesystem path",
201 | ],
202 | "init-version": Array [
203 | "full valid SemVer string",
204 | ],
205 | "json": Array [
206 | "boolean value (true or false)",
207 | ],
208 | "key": Array [
209 | null,
210 | Function String(),
211 | ],
212 | "legacy-bundling": Array [
213 | "boolean value (true or false)",
214 | ],
215 | "legacy-peer-deps": Array [
216 | "boolean value (true or false)",
217 | ],
218 | "link": Array [
219 | "boolean value (true or false)",
220 | ],
221 | "loglevel": Array [
222 | "silent",
223 | "error",
224 | "warn",
225 | "notice",
226 | "http",
227 | "timing",
228 | "info",
229 | "verbose",
230 | "silly",
231 | ],
232 | "logs-max": Array [
233 | "numeric value",
234 | ],
235 | "long": Array [
236 | "boolean value (true or false)",
237 | ],
238 | "maxsockets": Array [
239 | "numeric value",
240 | ],
241 | "message": Array [
242 | Function String(),
243 | ],
244 | "metrics-registry": Array [
245 | null,
246 | Function String(),
247 | ],
248 | "multiple-numbers": Array [
249 | Function Array(),
250 | "numeric value",
251 | ],
252 | "node-options": Array [
253 | null,
254 | Function String(),
255 | ],
256 | "node-version": Array [
257 | null,
258 | "full valid SemVer string",
259 | ],
260 | "noproxy": Array [
261 | null,
262 | Function String(),
263 | Function Array(),
264 | ],
265 | "offline": Array [
266 | "boolean value (true or false)",
267 | ],
268 | "omit": Array [
269 | Function Array(),
270 | "dev",
271 | "optional",
272 | "peer",
273 | ],
274 | "only": Array [
275 | null,
276 | "dev",
277 | "development",
278 | "prod",
279 | "production",
280 | ],
281 | "optional": Array [
282 | "boolean value (true or false)",
283 | ],
284 | "otp": Array [
285 | null,
286 | Function String(),
287 | ],
288 | "package": Array [
289 | Function String(),
290 | Function Array(),
291 | ],
292 | "package-lock": Array [
293 | "boolean value (true or false)",
294 | ],
295 | "package-lock-only": Array [
296 | "boolean value (true or false)",
297 | ],
298 | "parseable": Array [
299 | "boolean value (true or false)",
300 | ],
301 | "prefer-offline": Array [
302 | "boolean value (true or false)",
303 | ],
304 | "prefer-online": Array [
305 | "boolean value (true or false)",
306 | ],
307 | "prefix": Array [
308 | "valid filesystem path",
309 | ],
310 | "preid": Array [
311 | Function String(),
312 | ],
313 | "production": Array [
314 | "boolean value (true or false)",
315 | ],
316 | "progress": Array [
317 | "boolean value (true or false)",
318 | ],
319 | "proxy": Array [
320 | null,
321 | false,
322 | "full url with \\"http://\\"",
323 | ],
324 | "read-only": Array [
325 | "boolean value (true or false)",
326 | ],
327 | "rebuild-bundle": Array [
328 | "boolean value (true or false)",
329 | ],
330 | "registry": Array [
331 | null,
332 | "full url with \\"http://\\"",
333 | ],
334 | "rollback": Array [
335 | "boolean value (true or false)",
336 | ],
337 | "save": Array [
338 | "boolean value (true or false)",
339 | ],
340 | "save-bundle": Array [
341 | "boolean value (true or false)",
342 | ],
343 | "save-dev": Array [
344 | "boolean value (true or false)",
345 | ],
346 | "save-exact": Array [
347 | "boolean value (true or false)",
348 | ],
349 | "save-optional": Array [
350 | "boolean value (true or false)",
351 | ],
352 | "save-prefix": Array [
353 | Function String(),
354 | ],
355 | "save-prod": Array [
356 | "boolean value (true or false)",
357 | ],
358 | "scope": Array [
359 | Function String(),
360 | ],
361 | "script-shell": Array [
362 | null,
363 | Function String(),
364 | ],
365 | "scripts-prepend-node-path": Array [
366 | "boolean value (true or false)",
367 | "auto",
368 | "warn-only",
369 | ],
370 | "searchexclude": Array [
371 | null,
372 | Function String(),
373 | ],
374 | "searchlimit": Array [
375 | "numeric value",
376 | ],
377 | "searchopts": Array [
378 | Function String(),
379 | ],
380 | "searchstaleness": Array [
381 | "numeric value",
382 | ],
383 | "send-metrics": Array [
384 | "boolean value (true or false)",
385 | ],
386 | "shell": Array [
387 | Function String(),
388 | ],
389 | "shrinkwrap": Array [
390 | "boolean value (true or false)",
391 | ],
392 | "sign-git-commit": Array [
393 | "boolean value (true or false)",
394 | ],
395 | "sign-git-tag": Array [
396 | "boolean value (true or false)",
397 | ],
398 | "sso-poll-frequency": Array [
399 | "numeric value",
400 | ],
401 | "sso-type": Array [
402 | null,
403 | "oauth",
404 | "saml",
405 | ],
406 | "strict-ssl": Array [
407 | "boolean value (true or false)",
408 | ],
409 | "tag": Array [
410 | Function String(),
411 | ],
412 | "tag-version-prefix": Array [
413 | Function String(),
414 | ],
415 | "timing": Array [
416 | "boolean value (true or false)",
417 | ],
418 | "tmp": Array [
419 | "valid filesystem path",
420 | ],
421 | "umask": Array [
422 | "octal number in range 0o000..0o777 (0..511)",
423 | ],
424 | "unicode": Array [
425 | "boolean value (true or false)",
426 | ],
427 | "update-notifier": Array [
428 | "boolean value (true or false)",
429 | ],
430 | "usage": Array [
431 | "boolean value (true or false)",
432 | ],
433 | "user-agent": Array [
434 | Function String(),
435 | ],
436 | "userconfig": Array [
437 | "valid filesystem path",
438 | ],
439 | "version": Array [
440 | "boolean value (true or false)",
441 | ],
442 | "versions": Array [
443 | "boolean value (true or false)",
444 | ],
445 | "viewer": Array [
446 | Function String(),
447 | ],
448 | }
449 | `
450 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## NOTICE
2 |
3 | The code for this module is now maintained inside a workspace of the npm cli itself:
4 |
5 | https://github.com/npm/cli/blob/HEAD/workspaces/config
6 |
7 | # `@npmcli/config`
8 |
9 | Configuration management for the npm cli.
10 |
11 | This module is the spiritual descendant of
12 | [`npmconf`](http://npm.im/npmconf), and the code that once lived in npm's
13 | `lib/config/` folder.
14 |
15 | It does the management of configuration files that npm uses, but
16 | importantly, does _not_ define all the configuration defaults or types, as
17 | those parts make more sense to live within the npm CLI itself.
18 |
19 | The only exceptions:
20 |
21 | - The `prefix` config value has some special semantics, setting the local
22 | prefix if specified on the CLI options and not in global mode, or the
23 | global prefix otherwise.
24 | - The `project` config file is loaded based on the local prefix (which can
25 | only be set by the CLI config options, and otherwise defaults to a walk
26 | up the folder tree to the first parent containing a `node_modules`
27 | folder, `package.json` file, or `package-lock.json` file.)
28 | - The `userconfig` value, as set by the environment and CLI (defaulting to
29 | `~/.npmrc`, is used to load user configs.
30 | - The `globalconfig` value, as set by the environment, CLI, and
31 | `userconfig` file (defaulting to `$PREFIX/etc/npmrc`) is used to load
32 | global configs.
33 | - A `builtin` config, read from a `npmrc` file in the root of the npm
34 | project itself, overrides all defaults.
35 |
36 | The resulting hierarchy of configs:
37 |
38 | - CLI switches. eg `--some-key=some-value` on the command line. These are
39 | parsed by [`nopt`](http://npm.im/nopt), which is not a great choice, but
40 | it's the one that npm has used forever, and changing it will be
41 | difficult.
42 | - Environment variables. eg `npm_config_some_key=some_value` in the
43 | environment. There is no way at this time to modify this prefix.
44 | - INI-formatted project configs. eg `some-key = some-value` in the
45 | `localPrefix` folder (ie, the `cwd`, or its nearest parent that contains
46 | either a `node_modules` folder or `package.json` file.)
47 | - INI-formatted userconfig file. eg `some-key = some-value` in `~/.npmrc`.
48 | The `userconfig` config value can be overridden by the `cli`, `env`, or
49 | `project` configs to change this value.
50 | - INI-formatted globalconfig file. eg `some-key = some-value` in
51 | the `globalPrefix` folder, which is inferred by looking at the location
52 | of the node executable, or the `prefix` setting in the `cli`, `env`,
53 | `project`, or `userconfig`. The `globalconfig` value at any of those
54 | levels can override this.
55 | - INI-formatted builtin config file. eg `some-key = some-value` in
56 | `/usr/local/lib/node_modules/npm/npmrc`. This is not configurable, and
57 | is determined by looking in the `npmPath` folder.
58 | - Default values (passed in by npm when it loads this module).
59 |
60 | ## USAGE
61 |
62 | ```js
63 | const Config = require('@npmcli/config')
64 | // the types of all the configs we know about
65 | const types = require('./config/types.js')
66 | // default values for all the configs we know about
67 | const defaults = require('./config/defaults.js')
68 | // if you want -c to be short for --call and so on, define it here
69 | const shorthands = require('./config/shorthands.js')
70 |
71 | const conf = new Config({
72 | // path to the npm module being run
73 | npmPath: resolve(__dirname, '..'),
74 | types,
75 | shorthands,
76 | defaults,
77 | // optional, defaults to process.argv
78 | argv: process.argv,
79 | // optional, defaults to process.env
80 | env: process.env,
81 | // optional, defaults to process.execPath
82 | execPath: process.execPath,
83 | // optional, defaults to process.platform
84 | platform: process.platform,
85 | // optional, defaults to process.cwd()
86 | cwd: process.cwd(),
87 | })
88 |
89 | // emits log events on the process object
90 | // see `proc-log` for more info
91 | process.on('log', (level, ...args) => {
92 | console.log(level, ...args)
93 | })
94 |
95 | // returns a promise that fails if config loading fails, and
96 | // resolves when the config object is ready for action
97 | conf.load().then(() => {
98 | conf.validate()
99 | console.log('loaded ok! some-key = ' + conf.get('some-key'))
100 | }).catch(er => {
101 | console.error('error loading configs!', er)
102 | })
103 | ```
104 |
105 | ## API
106 |
107 | The `Config` class is the sole export.
108 |
109 | ```js
110 | const Config = require('@npmcli/config')
111 | ```
112 |
113 | ### static `Config.typeDefs`
114 |
115 | The type definitions passed to `nopt` for CLI option parsing and known
116 | configuration validation.
117 |
118 | ### constructor `new Config(options)`
119 |
120 | Options:
121 |
122 | - `types` Types of all known config values. Note that some are effectively
123 | given semantic value in the config loading process itself.
124 | - `shorthands` An object mapping a shorthand value to an array of CLI
125 | arguments that replace it.
126 | - `defaults` Default values for each of the known configuration keys.
127 | These should be defined for all configs given a type, and must be valid.
128 | - `npmPath` The path to the `npm` module, for loading the `builtin` config
129 | file.
130 | - `cwd` Optional, defaults to `process.cwd()`, used for inferring the
131 | `localPrefix` and loading the `project` config.
132 | - `platform` Optional, defaults to `process.platform`. Used when inferring
133 | the `globalPrefix` from the `execPath`, since this is done diferently on
134 | Windows.
135 | - `execPath` Optional, defaults to `process.execPath`. Used to infer the
136 | `globalPrefix`.
137 | - `env` Optional, defaults to `process.env`. Source of the environment
138 | variables for configuration.
139 | - `argv` Optional, defaults to `process.argv`. Source of the CLI options
140 | used for configuration.
141 |
142 | Returns a `config` object, which is not yet loaded.
143 |
144 | Fields:
145 |
146 | - `config.globalPrefix` The prefix for `global` operations. Set by the
147 | `prefix` config value, or defaults based on the location of the
148 | `execPath` option.
149 | - `config.localPrefix` The prefix for `local` operations. Set by the
150 | `prefix` config value on the CLI only, or defaults to either the `cwd` or
151 | its nearest ancestor containing a `node_modules` folder or `package.json`
152 | file.
153 | - `config.sources` A read-only `Map` of the file (or a comment, if no file
154 | found, or relevant) to the config level loaded from that source.
155 | - `config.data` A `Map` of config level to `ConfigData` objects. These
156 | objects should not be modified directly under any circumstances.
157 | - `source` The source where this data was loaded from.
158 | - `raw` The raw data used to generate this config data, as it was parsed
159 | initially from the environment, config file, or CLI options.
160 | - `data` The data object reflecting the inheritance of configs up to this
161 | point in the chain.
162 | - `loadError` Any errors encountered that prevented the loading of this
163 | config data.
164 | - `config.list` A list sorted in priority of all the config data objects in
165 | the prototype chain. `config.list[0]` is the `cli` level,
166 | `config.list[1]` is the `env` level, and so on.
167 | - `cwd` The `cwd` param
168 | - `env` The `env` param
169 | - `argv` The `argv` param
170 | - `execPath` The `execPath` param
171 | - `platform` The `platform` param
172 | - `defaults` The `defaults` param
173 | - `shorthands` The `shorthands` param
174 | - `types` The `types` param
175 | - `npmPath` The `npmPath` param
176 | - `globalPrefix` The effective `globalPrefix`
177 | - `localPrefix` The effective `localPrefix`
178 | - `prefix` If `config.get('global')` is true, then `globalPrefix`,
179 | otherwise `localPrefix`
180 | - `home` The user's home directory, found by looking at `env.HOME` or
181 | calling `os.homedir()`.
182 | - `loaded` A boolean indicating whether or not configs are loaded
183 | - `valid` A getter that returns `true` if all the config objects are valid.
184 | Any data objects that have been modified with `config.set(...)` will be
185 | re-evaluated when `config.valid` is read.
186 |
187 | ### `config.load()`
188 |
189 | Load configuration from the various sources of information.
190 |
191 | Returns a `Promise` that resolves when configuration is loaded, and fails
192 | if a fatal error is encountered.
193 |
194 | ### `config.find(key)`
195 |
196 | Find the effective place in the configuration levels a given key is set.
197 | Returns one of: `cli`, `env`, `project`, `user`, `global`, `builtin`, or
198 | `default`.
199 |
200 | Returns `null` if the key is not set.
201 |
202 | ### `config.get(key, where = 'cli')`
203 |
204 | Load the given key from the config stack.
205 |
206 | ### `config.set(key, value, where = 'cli')`
207 |
208 | Set the key to the specified value, at the specified level in the config
209 | stack.
210 |
211 | ### `config.delete(key, where = 'cli')`
212 |
213 | Delete the configuration key from the specified level in the config stack.
214 |
215 | ### `config.validate(where)`
216 |
217 | Verify that all known configuration options are set to valid values, and
218 | log a warning if they are invalid.
219 |
220 | Invalid auth options will cause this method to throw an error with a `code`
221 | property of `ERR_INVALID_AUTH`, and a `problems` property listing the specific
222 | concerns with the current configuration.
223 |
224 | If `where` is not set, then all config objects are validated.
225 |
226 | Returns `true` if all configs are valid.
227 |
228 | Note that it's usually enough (and more efficient) to just check
229 | `config.valid`, since each data object is marked for re-evaluation on every
230 | `config.set()` operation.
231 |
232 | ### `config.repair(problems)`
233 |
234 | Accept an optional array of problems (as thrown by `config.validate()`) and
235 | perform the necessary steps to resolve them. If no problems are provided,
236 | this method will call `config.validate()` internally to retrieve them.
237 |
238 | Note that you must `await config.save('user')` in order to persist the changes.
239 |
240 | ### `config.isDefault(key)`
241 |
242 | Returns `true` if the value is coming directly from the
243 | default definitions, if the current value for the key config is
244 | coming from any other source, returns `false`.
245 |
246 | This method can be used for avoiding or tweaking default values, e.g:
247 |
248 | > Given a global default definition of foo='foo' it's possible to read that
249 | > value such as:
250 | >
251 | > ```js
252 | > const save = config.get('foo')
253 | > ```
254 | >
255 | > Now in a different place of your app it's possible to avoid using the `foo`
256 | > default value, by checking to see if the current config value is currently
257 | > one that was defined by the default definitions:
258 | >
259 | > ```js
260 | > const save = config.isDefault('foo') ? 'bar' : config.get('foo')
261 | > ```
262 |
263 | ### `config.save(where)`
264 |
265 | Save the config file specified by the `where` param. Must be one of
266 | `project`, `user`, `global`, `builtin`.
267 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | # This file is automatically added by @npmcli/template-oss. Do not edit.
2 |
3 | name: Release
4 |
5 | on:
6 | workflow_dispatch:
7 | push:
8 | branches:
9 | - main
10 | - latest
11 | - release/v*
12 |
13 | permissions:
14 | contents: write
15 | pull-requests: write
16 | checks: write
17 |
18 | jobs:
19 | release:
20 | outputs:
21 | pr: ${{ steps.release.outputs.pr }}
22 | releases: ${{ steps.release.outputs.releases }}
23 | release-flags: ${{ steps.release.outputs.release-flags }}
24 | branch: ${{ steps.release.outputs.pr-branch }}
25 | pr-number: ${{ steps.release.outputs.pr-number }}
26 | comment-id: ${{ steps.pr-comment.outputs.result }}
27 | check-id: ${{ steps.check.outputs.check_id }}
28 | name: Release
29 | if: github.repository_owner == 'npm'
30 | runs-on: ubuntu-latest
31 | defaults:
32 | run:
33 | shell: bash
34 | steps:
35 | - name: Checkout
36 | uses: actions/checkout@v3
37 | - name: Setup Git User
38 | run: |
39 | git config --global user.email "npm-cli+bot@github.com"
40 | git config --global user.name "npm CLI robot"
41 | - name: Setup Node
42 | uses: actions/setup-node@v3
43 | with:
44 | node-version: 18.x
45 | - name: Install npm@latest
46 | run: npm i --prefer-online --no-fund --no-audit -g npm@latest
47 | - name: npm Version
48 | run: npm -v
49 | - name: Install Dependencies
50 | run: npm i --ignore-scripts --no-audit --no-fund
51 | - name: Release Please
52 | id: release
53 | env:
54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55 | run: |
56 | npx --offline template-oss-release-please ${{ github.ref_name }} ${{ github.event_name }}
57 | - name: Post Pull Request Comment
58 | if: steps.release.outputs.pr-number
59 | uses: actions/github-script@v6
60 | id: pr-comment
61 | env:
62 | PR_NUMBER: ${{ steps.release.outputs.pr-number }}
63 | REF_NAME: ${{ github.ref_name }}
64 | with:
65 | script: |
66 | const { REF_NAME, PR_NUMBER } = process.env
67 | const repo = { owner: context.repo.owner, repo: context.repo.repo }
68 | const issue = { ...repo, issue_number: PR_NUMBER }
69 |
70 | const { data: workflow } = await github.rest.actions.getWorkflowRun({ ...repo, run_id: context.runId })
71 |
72 | let body = '## Release Manager\n\n'
73 |
74 | const comments = await github.paginate(github.rest.issues.listComments, issue)
75 | let commentId = comments?.find(c => c.user.login === 'github-actions[bot]' && c.body.startsWith(body))?.id
76 |
77 | body += `Release workflow run: ${workflow.html_url}\n\n#### Force CI to Rerun for This Release\n\n`
78 | body += `This PR will be updated and CI will run for every non-\`chore:\` commit that is pushed to \`main\`. `
79 | body += `To force CI to rerun, run this command:\n\n`
80 | body += `\`\`\`\ngh workflow run release.yml -r ${REF_NAME}\n\`\`\``
81 |
82 | if (commentId) {
83 | await github.rest.issues.updateComment({ ...repo, comment_id: commentId, body })
84 | } else {
85 | const { data: comment } = await github.rest.issues.createComment({ ...issue, body })
86 | commentId = comment?.id
87 | }
88 |
89 | return commentId
90 | - name: Get Workflow Job
91 | uses: actions/github-script@v6
92 | if: steps.release.outputs.pr-sha
93 | id: check-output
94 | env:
95 | JOB_NAME: "Release"
96 | MATRIX_NAME: ""
97 | with:
98 | script: |
99 | const { owner, repo } = context.repo
100 |
101 | const { data } = await github.rest.actions.listJobsForWorkflowRun({
102 | owner,
103 | repo,
104 | run_id: context.runId,
105 | per_page: 100
106 | })
107 |
108 | const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME
109 | const job = data.jobs.find(j => j.name.endsWith(jobName))
110 | const jobUrl = job?.html_url
111 |
112 | const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ steps.release.outputs.pr-sha }}`
113 |
114 | let summary = `This check is assosciated with ${shaUrl}\n\n`
115 |
116 | if (jobUrl) {
117 | summary += `For run logs, click here: ${jobUrl}`
118 | } else {
119 | summary += `Run logs could not be found for a job with name: "${jobName}"`
120 | }
121 |
122 | return { summary }
123 | - name: Create Check
124 | uses: LouisBrunner/checks-action@v1.3.1
125 | id: check
126 | if: steps.release.outputs.pr-sha
127 | with:
128 | token: ${{ secrets.GITHUB_TOKEN }}
129 | status: in_progress
130 | name: Release
131 | sha: ${{ steps.release.outputs.pr-sha }}
132 | output: ${{ steps.check-output.outputs.result }}
133 |
134 | update:
135 | needs: release
136 | outputs:
137 | sha: ${{ steps.commit.outputs.sha }}
138 | check-id: ${{ steps.check.outputs.check_id }}
139 | name: Update - Release
140 | if: github.repository_owner == 'npm' && needs.release.outputs.pr
141 | runs-on: ubuntu-latest
142 | defaults:
143 | run:
144 | shell: bash
145 | steps:
146 | - name: Checkout
147 | uses: actions/checkout@v3
148 | with:
149 | fetch-depth: 0
150 | ref: ${{ needs.release.outputs.branch }}
151 | - name: Setup Git User
152 | run: |
153 | git config --global user.email "npm-cli+bot@github.com"
154 | git config --global user.name "npm CLI robot"
155 | - name: Setup Node
156 | uses: actions/setup-node@v3
157 | with:
158 | node-version: 18.x
159 | - name: Install npm@latest
160 | run: npm i --prefer-online --no-fund --no-audit -g npm@latest
161 | - name: npm Version
162 | run: npm -v
163 | - name: Install Dependencies
164 | run: npm i --ignore-scripts --no-audit --no-fund
165 | - name: Run Post Pull Request Actions
166 | env:
167 | RELEASE_PR_NUMBER: ${{ needs.release.outputs.pr-number }}
168 | RELEASE_COMMENT_ID: ${{ needs.release.outputs.comment-id }}
169 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
170 | run: |
171 | npm exec --offline -- template-oss-release-manager
172 | npm run rp-pull-request --ignore-scripts --if-present
173 | - name: Commit
174 | id: commit
175 | env:
176 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
177 | run: |
178 | git commit --all --amend --no-edit || true
179 | git push --force-with-lease
180 | echo "::set-output name=sha::$(git rev-parse HEAD)"
181 | - name: Get Workflow Job
182 | uses: actions/github-script@v6
183 | if: steps.commit.outputs.sha
184 | id: check-output
185 | env:
186 | JOB_NAME: "Update - Release"
187 | MATRIX_NAME: ""
188 | with:
189 | script: |
190 | const { owner, repo } = context.repo
191 |
192 | const { data } = await github.rest.actions.listJobsForWorkflowRun({
193 | owner,
194 | repo,
195 | run_id: context.runId,
196 | per_page: 100
197 | })
198 |
199 | const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME
200 | const job = data.jobs.find(j => j.name.endsWith(jobName))
201 | const jobUrl = job?.html_url
202 |
203 | const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ steps.commit.outputs.sha }}`
204 |
205 | let summary = `This check is assosciated with ${shaUrl}\n\n`
206 |
207 | if (jobUrl) {
208 | summary += `For run logs, click here: ${jobUrl}`
209 | } else {
210 | summary += `Run logs could not be found for a job with name: "${jobName}"`
211 | }
212 |
213 | return { summary }
214 | - name: Create Check
215 | uses: LouisBrunner/checks-action@v1.3.1
216 | id: check
217 | if: steps.commit.outputs.sha
218 | with:
219 | token: ${{ secrets.GITHUB_TOKEN }}
220 | status: in_progress
221 | name: Release
222 | sha: ${{ steps.commit.outputs.sha }}
223 | output: ${{ steps.check-output.outputs.result }}
224 | - name: Conclude Check
225 | uses: LouisBrunner/checks-action@v1.3.1
226 | if: needs.release.outputs.check-id && always()
227 | with:
228 | token: ${{ secrets.GITHUB_TOKEN }}
229 | conclusion: ${{ job.status }}
230 | check_id: ${{ needs.release.outputs.check-id }}
231 |
232 | ci:
233 | name: CI - Release
234 | needs: [ release, update ]
235 | if: needs.release.outputs.pr
236 | uses: ./.github/workflows/ci-release.yml
237 | with:
238 | ref: ${{ needs.release.outputs.branch }}
239 | check-sha: ${{ needs.update.outputs.sha }}
240 |
241 | post-ci:
242 | needs: [ release, update, ci ]
243 | name: Post CI - Release
244 | if: github.repository_owner == 'npm' && needs.release.outputs.pr && always()
245 | runs-on: ubuntu-latest
246 | defaults:
247 | run:
248 | shell: bash
249 | steps:
250 | - name: Get Needs Result
251 | id: needs-result
252 | run: |
253 | result=""
254 | if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
255 | result="failure"
256 | elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
257 | result="cancelled"
258 | else
259 | result="success"
260 | fi
261 | echo "::set-output name=result::$result"
262 | - name: Conclude Check
263 | uses: LouisBrunner/checks-action@v1.3.1
264 | if: needs.update.outputs.check-id && always()
265 | with:
266 | token: ${{ secrets.GITHUB_TOKEN }}
267 | conclusion: ${{ steps.needs-result.outputs.result }}
268 | check_id: ${{ needs.update.outputs.check-id }}
269 |
270 | post-release:
271 | needs: release
272 | name: Post Release - Release
273 | if: github.repository_owner == 'npm' && needs.release.outputs.releases
274 | runs-on: ubuntu-latest
275 | defaults:
276 | run:
277 | shell: bash
278 | steps:
279 | - name: Checkout
280 | uses: actions/checkout@v3
281 | - name: Setup Git User
282 | run: |
283 | git config --global user.email "npm-cli+bot@github.com"
284 | git config --global user.name "npm CLI robot"
285 | - name: Setup Node
286 | uses: actions/setup-node@v3
287 | with:
288 | node-version: 18.x
289 | - name: Install npm@latest
290 | run: npm i --prefer-online --no-fund --no-audit -g npm@latest
291 | - name: npm Version
292 | run: npm -v
293 | - name: Install Dependencies
294 | run: npm i --ignore-scripts --no-audit --no-fund
295 | - name: Run Post Release Actions
296 | env:
297 | RELEASES: ${{ needs.release.outputs.releases }}
298 | run: |
299 | npm run rp-release --ignore-scripts --if-present ${{ join(fromJSON(needs.release.outputs.release-flags), ' ') }}
300 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | // TODO: set the scope config from package.json or explicit cli config
2 | const walkUp = require('walk-up-path')
3 | const ini = require('ini')
4 | const nopt = require('nopt')
5 | const mapWorkspaces = require('@npmcli/map-workspaces')
6 | const rpj = require('read-package-json-fast')
7 | const log = require('proc-log')
8 |
9 | const { resolve, dirname, join } = require('path')
10 | const { homedir } = require('os')
11 | const {
12 | readFile,
13 | writeFile,
14 | chmod,
15 | unlink,
16 | stat,
17 | mkdir,
18 | } = require('fs/promises')
19 |
20 | const hasOwnProperty = (obj, key) =>
21 | Object.prototype.hasOwnProperty.call(obj, key)
22 |
23 | // define a custom getter, but turn into a normal prop
24 | // if we set it. otherwise it can't be set on child objects
25 | const settableGetter = (obj, key, get) => {
26 | Object.defineProperty(obj, key, {
27 | get,
28 | set (value) {
29 | Object.defineProperty(obj, key, {
30 | value,
31 | configurable: true,
32 | writable: true,
33 | enumerable: true,
34 | })
35 | },
36 | configurable: true,
37 | enumerable: true,
38 | })
39 | }
40 |
41 | const typeDefs = require('./type-defs.js')
42 | const nerfDart = require('./nerf-dart.js')
43 | const envReplace = require('./env-replace.js')
44 | const parseField = require('./parse-field.js')
45 | const typeDescription = require('./type-description.js')
46 | const setEnvs = require('./set-envs.js')
47 |
48 | const {
49 | ErrInvalidAuth,
50 | } = require('./errors.js')
51 |
52 | // types that can be saved back to
53 | const confFileTypes = new Set([
54 | 'global',
55 | 'user',
56 | 'project',
57 | ])
58 |
59 | const confTypes = new Set([
60 | 'default',
61 | 'builtin',
62 | ...confFileTypes,
63 | 'env',
64 | 'cli',
65 | ])
66 |
67 | const _loaded = Symbol('loaded')
68 | const _get = Symbol('get')
69 | const _find = Symbol('find')
70 | const _loadObject = Symbol('loadObject')
71 | const _loadFile = Symbol('loadFile')
72 | const _checkDeprecated = Symbol('checkDeprecated')
73 | const _flatten = Symbol('flatten')
74 | const _flatOptions = Symbol('flatOptions')
75 |
76 | class Config {
77 | static get typeDefs () {
78 | return typeDefs
79 | }
80 |
81 | constructor ({
82 | definitions,
83 | shorthands,
84 | flatten,
85 | npmPath,
86 |
87 | // options just to override in tests, mostly
88 | env = process.env,
89 | argv = process.argv,
90 | platform = process.platform,
91 | execPath = process.execPath,
92 | cwd = process.cwd(),
93 | }) {
94 | // turn the definitions into nopt's weirdo syntax
95 | this.definitions = definitions
96 | const types = {}
97 | const defaults = {}
98 | this.deprecated = {}
99 | for (const [key, def] of Object.entries(definitions)) {
100 | defaults[key] = def.default
101 | types[key] = def.type
102 | if (def.deprecated) {
103 | this.deprecated[key] = def.deprecated.trim().replace(/\n +/, '\n')
104 | }
105 | }
106 |
107 | // populated the first time we flatten the object
108 | this[_flatOptions] = null
109 | this[_flatten] = flatten
110 | this.types = types
111 | this.shorthands = shorthands
112 | this.defaults = defaults
113 |
114 | this.npmPath = npmPath
115 | this.argv = argv
116 | this.env = env
117 | this.execPath = execPath
118 | this.platform = platform
119 | this.cwd = cwd
120 |
121 | // set when we load configs
122 | this.globalPrefix = null
123 | this.localPrefix = null
124 |
125 | // defaults to env.HOME, but will always be *something*
126 | this.home = null
127 |
128 | // set up the prototype chain of config objects
129 | const wheres = [...confTypes]
130 | this.data = new Map()
131 | let parent = null
132 | for (const where of wheres) {
133 | this.data.set(where, parent = new ConfigData(parent))
134 | }
135 |
136 | this.data.set = () => {
137 | throw new Error('cannot change internal config data structure')
138 | }
139 | this.data.delete = () => {
140 | throw new Error('cannot change internal config data structure')
141 | }
142 |
143 | this.sources = new Map([])
144 |
145 | this.list = []
146 | for (const { data } of this.data.values()) {
147 | this.list.unshift(data)
148 | }
149 | Object.freeze(this.list)
150 |
151 | this[_loaded] = false
152 | }
153 |
154 | get loaded () {
155 | return this[_loaded]
156 | }
157 |
158 | get prefix () {
159 | return this[_get]('global') ? this.globalPrefix : this.localPrefix
160 | }
161 |
162 | // return the location where key is found.
163 | find (key) {
164 | if (!this.loaded) {
165 | throw new Error('call config.load() before reading values')
166 | }
167 | return this[_find](key)
168 | }
169 |
170 | [_find] (key) {
171 | // have to look in reverse order
172 | const entries = [...this.data.entries()]
173 | for (let i = entries.length - 1; i > -1; i--) {
174 | const [where, { data }] = entries[i]
175 | if (hasOwnProperty(data, key)) {
176 | return where
177 | }
178 | }
179 | return null
180 | }
181 |
182 | get (key, where) {
183 | if (!this.loaded) {
184 | throw new Error('call config.load() before reading values')
185 | }
186 | return this[_get](key, where)
187 | }
188 |
189 | // we need to get values sometimes, so use this internal one to do so
190 | // while in the process of loading.
191 | [_get] (key, where = null) {
192 | if (where !== null && !confTypes.has(where)) {
193 | throw new Error('invalid config location param: ' + where)
194 | }
195 | const { data } = this.data.get(where || 'cli')
196 | return where === null || hasOwnProperty(data, key) ? data[key] : undefined
197 | }
198 |
199 | set (key, val, where = 'cli') {
200 | if (!this.loaded) {
201 | throw new Error('call config.load() before setting values')
202 | }
203 | if (!confTypes.has(where)) {
204 | throw new Error('invalid config location param: ' + where)
205 | }
206 | this[_checkDeprecated](key)
207 | const { data } = this.data.get(where)
208 | data[key] = val
209 |
210 | // this is now dirty, the next call to this.valid will have to check it
211 | this.data.get(where)[_valid] = null
212 |
213 | // the flat options are invalidated, regenerate next time they're needed
214 | this[_flatOptions] = null
215 | }
216 |
217 | get flat () {
218 | if (this[_flatOptions]) {
219 | return this[_flatOptions]
220 | }
221 |
222 | // create the object for flat options passed to deps
223 | process.emit('time', 'config:load:flatten')
224 | this[_flatOptions] = {}
225 | // walk from least priority to highest
226 | for (const { data } of this.data.values()) {
227 | this[_flatten](data, this[_flatOptions])
228 | }
229 | process.emit('timeEnd', 'config:load:flatten')
230 |
231 | return this[_flatOptions]
232 | }
233 |
234 | delete (key, where = 'cli') {
235 | if (!this.loaded) {
236 | throw new Error('call config.load() before deleting values')
237 | }
238 | if (!confTypes.has(where)) {
239 | throw new Error('invalid config location param: ' + where)
240 | }
241 | delete this.data.get(where).data[key]
242 | }
243 |
244 | async load () {
245 | if (this.loaded) {
246 | throw new Error('attempting to load npm config multiple times')
247 | }
248 |
249 | process.emit('time', 'config:load')
250 | // first load the defaults, which sets the global prefix
251 | process.emit('time', 'config:load:defaults')
252 | this.loadDefaults()
253 | process.emit('timeEnd', 'config:load:defaults')
254 |
255 | // next load the builtin config, as this sets new effective defaults
256 | process.emit('time', 'config:load:builtin')
257 | await this.loadBuiltinConfig()
258 | process.emit('timeEnd', 'config:load:builtin')
259 |
260 | // cli and env are not async, and can set the prefix, relevant to project
261 | process.emit('time', 'config:load:cli')
262 | this.loadCLI()
263 | process.emit('timeEnd', 'config:load:cli')
264 | process.emit('time', 'config:load:env')
265 | this.loadEnv()
266 | process.emit('timeEnd', 'config:load:env')
267 |
268 | // next project config, which can affect userconfig location
269 | process.emit('time', 'config:load:project')
270 | await this.loadProjectConfig()
271 | process.emit('timeEnd', 'config:load:project')
272 | // then user config, which can affect globalconfig location
273 | process.emit('time', 'config:load:user')
274 | await this.loadUserConfig()
275 | process.emit('timeEnd', 'config:load:user')
276 | // last but not least, global config file
277 | process.emit('time', 'config:load:global')
278 | await this.loadGlobalConfig()
279 | process.emit('timeEnd', 'config:load:global')
280 |
281 | // set this before calling setEnvs, so that we don't have to share
282 | // symbols, as that module also does a bunch of get operations
283 | this[_loaded] = true
284 |
285 | // set proper globalPrefix now that everything is loaded
286 | this.globalPrefix = this.get('prefix')
287 |
288 | process.emit('time', 'config:load:setEnvs')
289 | this.setEnvs()
290 | process.emit('timeEnd', 'config:load:setEnvs')
291 |
292 | process.emit('timeEnd', 'config:load')
293 | }
294 |
295 | loadDefaults () {
296 | this.loadGlobalPrefix()
297 | this.loadHome()
298 |
299 | this[_loadObject]({
300 | ...this.defaults,
301 | prefix: this.globalPrefix,
302 | }, 'default', 'default values')
303 |
304 | const { data } = this.data.get('default')
305 |
306 | // the metrics-registry defaults to the current resolved value of
307 | // the registry, unless overridden somewhere else.
308 | settableGetter(data, 'metrics-registry', () => this[_get]('registry'))
309 |
310 | // if the prefix is set on cli, env, or userconfig, then we need to
311 | // default the globalconfig file to that location, instead of the default
312 | // global prefix. It's weird that `npm get globalconfig --prefix=/foo`
313 | // returns `/foo/etc/npmrc`, but better to not change it at this point.
314 | settableGetter(data, 'globalconfig', () =>
315 | resolve(this[_get]('prefix'), 'etc/npmrc'))
316 | }
317 |
318 | loadHome () {
319 | if (this.env.HOME) {
320 | return this.home = this.env.HOME
321 | }
322 | this.home = homedir()
323 | }
324 |
325 | loadGlobalPrefix () {
326 | if (this.globalPrefix) {
327 | throw new Error('cannot load default global prefix more than once')
328 | }
329 |
330 | if (this.env.PREFIX) {
331 | this.globalPrefix = this.env.PREFIX
332 | } else if (this.platform === 'win32') {
333 | // c:\node\node.exe --> prefix=c:\node\
334 | this.globalPrefix = dirname(this.execPath)
335 | } else {
336 | // /usr/local/bin/node --> prefix=/usr/local
337 | this.globalPrefix = dirname(dirname(this.execPath))
338 |
339 | // destdir only is respected on Unix
340 | if (this.env.DESTDIR) {
341 | this.globalPrefix = join(this.env.DESTDIR, this.globalPrefix)
342 | }
343 | }
344 | }
345 |
346 | loadEnv () {
347 | const conf = Object.create(null)
348 | for (const [envKey, envVal] of Object.entries(this.env)) {
349 | if (!/^npm_config_/i.test(envKey) || envVal === '') {
350 | continue
351 | }
352 | let key = envKey.slice('npm_config_'.length)
353 | if (!key.startsWith('//')) { // don't normalize nerf-darted keys
354 | key = key.replace(/(?!^)_/g, '-') // don't replace _ at the start of the key
355 | .toLowerCase()
356 | }
357 | conf[key] = envVal
358 | }
359 | this[_loadObject](conf, 'env', 'environment')
360 | }
361 |
362 | loadCLI () {
363 | nopt.invalidHandler = (k, val, type) =>
364 | this.invalidHandler(k, val, type, 'command line options', 'cli')
365 | const conf = nopt(this.types, this.shorthands, this.argv)
366 | nopt.invalidHandler = null
367 | this.parsedArgv = conf.argv
368 | delete conf.argv
369 | this[_loadObject](conf, 'cli', 'command line options')
370 | }
371 |
372 | get valid () {
373 | for (const [where, { valid }] of this.data.entries()) {
374 | if (valid === false || valid === null && !this.validate(where)) {
375 | return false
376 | }
377 | }
378 | return true
379 | }
380 |
381 | validate (where) {
382 | if (!where) {
383 | let valid = true
384 | const authProblems = []
385 |
386 | for (const entryWhere of this.data.keys()) {
387 | // no need to validate our defaults, we know they're fine
388 | // cli was already validated when parsed the first time
389 | if (entryWhere === 'default' || entryWhere === 'builtin' || entryWhere === 'cli') {
390 | continue
391 | }
392 | const ret = this.validate(entryWhere)
393 | valid = valid && ret
394 |
395 | if (['global', 'user', 'project'].includes(entryWhere)) {
396 | // after validating everything else, we look for old auth configs we no longer support
397 | // if these keys are found, we build up a list of them and the appropriate action and
398 | // attach it as context on the thrown error
399 |
400 | // first, keys that should be removed
401 | for (const key of ['_authtoken', '-authtoken']) {
402 | if (this.get(key, entryWhere)) {
403 | authProblems.push({ action: 'delete', key, where: entryWhere })
404 | }
405 | }
406 |
407 | // NOTE we pull registry without restricting to the current 'where' because we want to
408 | // suggest scoping things to the registry they would be applied to, which is the default
409 | // regardless of where it was defined
410 | const nerfedReg = nerfDart(this.get('registry'))
411 | // keys that should be nerfed but currently are not
412 | for (const key of ['_auth', '_authToken', 'username', '_password']) {
413 | if (this.get(key, entryWhere)) {
414 | // username and _password must both exist in the same file to be recognized correctly
415 | if (key === 'username' && !this.get('_password', entryWhere)) {
416 | authProblems.push({ action: 'delete', key, where: entryWhere })
417 | } else if (key === '_password' && !this.get('username', entryWhere)) {
418 | authProblems.push({ action: 'delete', key, where: entryWhere })
419 | } else {
420 | authProblems.push({
421 | action: 'rename',
422 | from: key,
423 | to: `${nerfedReg}:${key}`,
424 | where: entryWhere,
425 | })
426 | }
427 | }
428 | }
429 | }
430 | }
431 |
432 | if (authProblems.length) {
433 | throw new ErrInvalidAuth(authProblems)
434 | }
435 |
436 | return valid
437 | } else {
438 | const obj = this.data.get(where)
439 | obj[_valid] = true
440 |
441 | nopt.invalidHandler = (k, val, type) =>
442 | this.invalidHandler(k, val, type, obj.source, where)
443 |
444 | nopt.clean(obj.data, this.types, this.typeDefs)
445 |
446 | nopt.invalidHandler = null
447 | return obj[_valid]
448 | }
449 | }
450 |
451 | // fixes problems identified by validate(), accepts the 'problems' property from a thrown
452 | // ErrInvalidAuth to avoid having to check everything again
453 | repair (problems) {
454 | if (!problems) {
455 | try {
456 | this.validate()
457 | } catch (err) {
458 | // coverage skipped here because we don't need to test re-throwing an error
459 | // istanbul ignore next
460 | if (err.code !== 'ERR_INVALID_AUTH') {
461 | throw err
462 | }
463 |
464 | problems = err.problems
465 | } finally {
466 | if (!problems) {
467 | problems = []
468 | }
469 | }
470 | }
471 |
472 | for (const problem of problems) {
473 | // coverage disabled for else branch because it doesn't do anything and shouldn't
474 | // istanbul ignore else
475 | if (problem.action === 'delete') {
476 | this.delete(problem.key, problem.where)
477 | } else if (problem.action === 'rename') {
478 | const old = this.get(problem.from, problem.where)
479 | this.set(problem.to, old, problem.where)
480 | this.delete(problem.from, problem.where)
481 | }
482 | }
483 | }
484 |
485 | // Returns true if the value is coming directly from the source defined
486 | // in default definitions, if the current value for the key config is
487 | // coming from any other different source, returns false
488 | isDefault (key) {
489 | const [defaultType, ...types] = [...confTypes]
490 | const defaultData = this.data.get(defaultType).data
491 |
492 | return hasOwnProperty(defaultData, key)
493 | && types.every(type => {
494 | const typeData = this.data.get(type).data
495 | return !hasOwnProperty(typeData, key)
496 | })
497 | }
498 |
499 | invalidHandler (k, val, type, source, where) {
500 | log.warn(
501 | 'invalid config',
502 | k + '=' + JSON.stringify(val),
503 | `set in ${source}`
504 | )
505 | this.data.get(where)[_valid] = false
506 |
507 | if (Array.isArray(type)) {
508 | if (type.includes(typeDefs.url.type)) {
509 | type = typeDefs.url.type
510 | } else {
511 | /* istanbul ignore if - no actual configs matching this, but
512 | * path types SHOULD be handled this way, like URLs, for the
513 | * same reason */
514 | if (type.includes(typeDefs.path.type)) {
515 | type = typeDefs.path.type
516 | }
517 | }
518 | }
519 |
520 | const typeDesc = typeDescription(type)
521 | const oneOrMore = typeDesc.indexOf(Array) !== -1
522 | const mustBe = typeDesc
523 | .filter(m => m !== undefined && m !== Array)
524 | const oneOf = mustBe.length === 1 && oneOrMore ? ' one or more'
525 | : mustBe.length > 1 && oneOrMore ? ' one or more of:'
526 | : mustBe.length > 1 ? ' one of:'
527 | : ''
528 | const msg = 'Must be' + oneOf
529 | const desc = mustBe.length === 1 ? mustBe[0]
530 | : mustBe.filter(m => m !== Array)
531 | .map(n => typeof n === 'string' ? n : JSON.stringify(n))
532 | .join(', ')
533 | log.warn('invalid config', msg, desc)
534 | }
535 |
536 | [_loadObject] (obj, where, source, er = null) {
537 | const conf = this.data.get(where)
538 | if (conf.source) {
539 | const m = `double-loading "${where}" configs from ${source}, ` +
540 | `previously loaded from ${conf.source}`
541 | throw new Error(m)
542 | }
543 |
544 | if (this.sources.has(source)) {
545 | const m = `double-loading config "${source}" as "${where}", ` +
546 | `previously loaded as "${this.sources.get(source)}"`
547 | throw new Error(m)
548 | }
549 |
550 | conf.source = source
551 | this.sources.set(source, where)
552 | if (er) {
553 | conf.loadError = er
554 | if (er.code !== 'ENOENT') {
555 | log.verbose('config', `error loading ${where} config`, er)
556 | }
557 | } else {
558 | conf.raw = obj
559 | for (const [key, value] of Object.entries(obj)) {
560 | const k = envReplace(key, this.env)
561 | const v = this.parseField(value, k)
562 | if (where !== 'default') {
563 | this[_checkDeprecated](k, where, obj, [key, value])
564 | }
565 | conf.data[k] = v
566 | }
567 | }
568 | }
569 |
570 | [_checkDeprecated] (key, where, obj, kv) {
571 | // XXX(npm9+) make this throw an error
572 | if (this.deprecated[key]) {
573 | log.warn('config', key, this.deprecated[key])
574 | }
575 | }
576 |
577 | // Parse a field, coercing it to the best type available.
578 | parseField (f, key, listElement = false) {
579 | return parseField(f, key, this, listElement)
580 | }
581 |
582 | async [_loadFile] (file, type) {
583 | process.emit('time', 'config:load:file:' + file)
584 | // only catch the error from readFile, not from the loadObject call
585 | await readFile(file, 'utf8').then(
586 | data => this[_loadObject](ini.parse(data), type, file),
587 | er => this[_loadObject](null, type, file, er)
588 | )
589 | process.emit('timeEnd', 'config:load:file:' + file)
590 | }
591 |
592 | loadBuiltinConfig () {
593 | return this[_loadFile](resolve(this.npmPath, 'npmrc'), 'builtin')
594 | }
595 |
596 | async loadProjectConfig () {
597 | // the localPrefix can be set by the CLI config, but otherwise is
598 | // found by walking up the folder tree. either way, we load it before
599 | // we return to make sure localPrefix is set
600 | await this.loadLocalPrefix()
601 |
602 | if (this[_get]('global') === true || this[_get]('location') === 'global') {
603 | this.data.get('project').source = '(global mode enabled, ignored)'
604 | this.sources.set(this.data.get('project').source, 'project')
605 | return
606 | }
607 |
608 | const projectFile = resolve(this.localPrefix, '.npmrc')
609 | // if we're in the ~ directory, and there happens to be a node_modules
610 | // folder (which is not TOO uncommon, it turns out), then we can end
611 | // up loading the "project" config where the "userconfig" will be,
612 | // which causes some calamaties. So, we only load project config if
613 | // it doesn't match what the userconfig will be.
614 | if (projectFile !== this[_get]('userconfig')) {
615 | return this[_loadFile](projectFile, 'project')
616 | } else {
617 | this.data.get('project').source = '(same as "user" config, ignored)'
618 | this.sources.set(this.data.get('project').source, 'project')
619 | }
620 | }
621 |
622 | async loadLocalPrefix () {
623 | const cliPrefix = this[_get]('prefix', 'cli')
624 | if (cliPrefix) {
625 | this.localPrefix = cliPrefix
626 | return
627 | }
628 |
629 | const cliWorkspaces = this[_get]('workspaces', 'cli')
630 | const isGlobal = this[_get]('global') || this[_get]('location') === 'global'
631 |
632 | for (const p of walkUp(this.cwd)) {
633 | const hasNodeModules = await stat(resolve(p, 'node_modules'))
634 | .then((st) => st.isDirectory())
635 | .catch(() => false)
636 |
637 | const hasPackageJson = await stat(resolve(p, 'package.json'))
638 | .then((st) => st.isFile())
639 | .catch(() => false)
640 |
641 | if (!this.localPrefix && (hasNodeModules || hasPackageJson)) {
642 | this.localPrefix = p
643 |
644 | // if workspaces are disabled, or we're in global mode, return now
645 | if (cliWorkspaces === false || isGlobal) {
646 | return
647 | }
648 |
649 | // otherwise, continue the loop
650 | continue
651 | }
652 |
653 | if (this.localPrefix && hasPackageJson) {
654 | // if we already set localPrefix but this dir has a package.json
655 | // then we need to see if `p` is a workspace root by reading its package.json
656 | // however, if reading it fails then we should just move on
657 | const pkg = await rpj(resolve(p, 'package.json')).catch(() => false)
658 | if (!pkg) {
659 | continue
660 | }
661 |
662 | const workspaces = await mapWorkspaces({ cwd: p, pkg })
663 | for (const w of workspaces.values()) {
664 | if (w === this.localPrefix) {
665 | // see if there's a .npmrc file in the workspace, if so log a warning
666 | const hasNpmrc = await stat(resolve(this.localPrefix, '.npmrc'))
667 | .then((st) => st.isFile())
668 | .catch(() => false)
669 |
670 | if (hasNpmrc) {
671 | log.warn(`ignoring workspace config at ${this.localPrefix}/.npmrc`)
672 | }
673 |
674 | // set the workspace in the default layer, which allows it to be overridden easily
675 | const { data } = this.data.get('default')
676 | data.workspace = [this.localPrefix]
677 | this.localPrefix = p
678 | log.info(`found workspace root at ${this.localPrefix}`)
679 | // we found a root, so we return now
680 | return
681 | }
682 | }
683 | }
684 | }
685 |
686 | if (!this.localPrefix) {
687 | this.localPrefix = this.cwd
688 | }
689 | }
690 |
691 | loadUserConfig () {
692 | return this[_loadFile](this[_get]('userconfig'), 'user')
693 | }
694 |
695 | loadGlobalConfig () {
696 | return this[_loadFile](this[_get]('globalconfig'), 'global')
697 | }
698 |
699 | async save (where) {
700 | if (!this.loaded) {
701 | throw new Error('call config.load() before saving')
702 | }
703 | if (!confFileTypes.has(where)) {
704 | throw new Error('invalid config location param: ' + where)
705 | }
706 |
707 | const conf = this.data.get(where)
708 | conf[_raw] = { ...conf.data }
709 | conf[_loadError] = null
710 |
711 | if (where === 'user') {
712 | // if email is nerfed, then we want to de-nerf it
713 | const nerfed = nerfDart(this.get('registry'))
714 | const email = this.get(`${nerfed}:email`, 'user')
715 | if (email) {
716 | this.delete(`${nerfed}:email`, 'user')
717 | this.set('email', email, 'user')
718 | }
719 | }
720 |
721 | const iniData = ini.stringify(conf.data).trim() + '\n'
722 | if (!iniData.trim()) {
723 | // ignore the unlink error (eg, if file doesn't exist)
724 | await unlink(conf.source).catch(er => {})
725 | return
726 | }
727 | const dir = dirname(conf.source)
728 | await mkdir(dir, { recursive: true })
729 | await writeFile(conf.source, iniData, 'utf8')
730 | const mode = where === 'user' ? 0o600 : 0o666
731 | await chmod(conf.source, mode)
732 | }
733 |
734 | clearCredentialsByURI (uri) {
735 | const nerfed = nerfDart(uri)
736 | const def = nerfDart(this.get('registry'))
737 | if (def === nerfed) {
738 | this.delete(`-authtoken`, 'user')
739 | this.delete(`_authToken`, 'user')
740 | this.delete(`_authtoken`, 'user')
741 | this.delete(`_auth`, 'user')
742 | this.delete(`_password`, 'user')
743 | this.delete(`username`, 'user')
744 | // de-nerf email if it's nerfed to the default registry
745 | const email = this.get(`${nerfed}:email`, 'user')
746 | if (email) {
747 | this.set('email', email, 'user')
748 | }
749 | }
750 | this.delete(`${nerfed}:_authToken`, 'user')
751 | this.delete(`${nerfed}:_auth`, 'user')
752 | this.delete(`${nerfed}:_password`, 'user')
753 | this.delete(`${nerfed}:username`, 'user')
754 | this.delete(`${nerfed}:email`, 'user')
755 | this.delete(`${nerfed}:certfile`, 'user')
756 | this.delete(`${nerfed}:keyfile`, 'user')
757 | }
758 |
759 | setCredentialsByURI (uri, { token, username, password, email, certfile, keyfile }) {
760 | const nerfed = nerfDart(uri)
761 |
762 | // email is either provided, a top level key, or nothing
763 | email = email || this.get('email', 'user')
764 |
765 | // field that hasn't been used as documented for a LONG time,
766 | // and as of npm 7.10.0, isn't used at all. We just always
767 | // send auth if we have it, only to the URIs under the nerf dart.
768 | this.delete(`${nerfed}:always-auth`, 'user')
769 |
770 | this.delete(`${nerfed}:email`, 'user')
771 | if (certfile && keyfile) {
772 | this.set(`${nerfed}:certfile`, certfile, 'user')
773 | this.set(`${nerfed}:keyfile`, keyfile, 'user')
774 | // cert/key may be used in conjunction with other credentials, thus no `else`
775 | }
776 | if (token) {
777 | this.set(`${nerfed}:_authToken`, token, 'user')
778 | this.delete(`${nerfed}:_password`, 'user')
779 | this.delete(`${nerfed}:username`, 'user')
780 | } else if (username || password) {
781 | if (!username) {
782 | throw new Error('must include username')
783 | }
784 | if (!password) {
785 | throw new Error('must include password')
786 | }
787 | this.delete(`${nerfed}:_authToken`, 'user')
788 | this.set(`${nerfed}:username`, username, 'user')
789 | // note: not encrypted, no idea why we bothered to do this, but oh well
790 | // protects against shoulder-hacks if password is memorable, I guess?
791 | const encoded = Buffer.from(password, 'utf8').toString('base64')
792 | this.set(`${nerfed}:_password`, encoded, 'user')
793 | } else if (!certfile || !keyfile) {
794 | throw new Error('No credentials to set.')
795 | }
796 | }
797 |
798 | // this has to be a bit more complicated to support legacy data of all forms
799 | getCredentialsByURI (uri) {
800 | const nerfed = nerfDart(uri)
801 | const def = nerfDart(this.get('registry'))
802 | const creds = {}
803 |
804 | // email is handled differently, it used to always be nerfed and now it never should be
805 | // if it's set nerfed to the default registry, then we copy it to the unnerfed key
806 | // TODO: evaluate removing 'email' from the credentials object returned here
807 | const email = this.get(`${nerfed}:email`) || this.get('email')
808 | if (email) {
809 | if (nerfed === def) {
810 | this.set('email', email, 'user')
811 | }
812 | creds.email = email
813 | }
814 |
815 | const certfileReg = this.get(`${nerfed}:certfile`)
816 | const keyfileReg = this.get(`${nerfed}:keyfile`)
817 | if (certfileReg && keyfileReg) {
818 | creds.certfile = certfileReg
819 | creds.keyfile = keyfileReg
820 | // cert/key may be used in conjunction with other credentials, thus no `return`
821 | }
822 |
823 | const tokenReg = this.get(`${nerfed}:_authToken`)
824 | if (tokenReg) {
825 | creds.token = tokenReg
826 | return creds
827 | }
828 |
829 | const userReg = this.get(`${nerfed}:username`)
830 | const passReg = this.get(`${nerfed}:_password`)
831 | if (userReg && passReg) {
832 | creds.username = userReg
833 | creds.password = Buffer.from(passReg, 'base64').toString('utf8')
834 | const auth = `${creds.username}:${creds.password}`
835 | creds.auth = Buffer.from(auth, 'utf8').toString('base64')
836 | return creds
837 | }
838 |
839 | const authReg = this.get(`${nerfed}:_auth`)
840 | if (authReg) {
841 | const authDecode = Buffer.from(authReg, 'base64').toString('utf8')
842 | const authSplit = authDecode.split(':')
843 | creds.username = authSplit.shift()
844 | creds.password = authSplit.join(':')
845 | creds.auth = authReg
846 | return creds
847 | }
848 |
849 | // at this point, nothing else is usable so just return what we do have
850 | return creds
851 | }
852 |
853 | // set up the environment object we have with npm_config_* environs
854 | // for all configs that are different from their default values, and
855 | // set EDITOR and HOME.
856 | setEnvs () {
857 | setEnvs(this)
858 | }
859 | }
860 |
861 | const _data = Symbol('data')
862 | const _raw = Symbol('raw')
863 | const _loadError = Symbol('loadError')
864 | const _source = Symbol('source')
865 | const _valid = Symbol('valid')
866 | class ConfigData {
867 | constructor (parent) {
868 | this[_data] = Object.create(parent && parent.data)
869 | this[_source] = null
870 | this[_loadError] = null
871 | this[_raw] = null
872 | this[_valid] = true
873 | }
874 |
875 | get data () {
876 | return this[_data]
877 | }
878 |
879 | get valid () {
880 | return this[_valid]
881 | }
882 |
883 | set source (s) {
884 | if (this[_source]) {
885 | throw new Error('cannot set ConfigData source more than once')
886 | }
887 | this[_source] = s
888 | }
889 |
890 | get source () {
891 | return this[_source]
892 | }
893 |
894 | set loadError (e) {
895 | if (this[_loadError] || this[_raw]) {
896 | throw new Error('cannot set ConfigData loadError after load')
897 | }
898 | this[_loadError] = e
899 | }
900 |
901 | get loadError () {
902 | return this[_loadError]
903 | }
904 |
905 | set raw (r) {
906 | if (this[_raw] || this[_loadError]) {
907 | throw new Error('cannot set ConfigData raw after load')
908 | }
909 | this[_raw] = r
910 | }
911 |
912 | get raw () {
913 | return this[_raw]
914 | }
915 | }
916 |
917 | module.exports = Config
918 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | const t = require('tap')
2 |
3 | const fs = require('fs')
4 | const { readFileSync } = fs
5 |
6 | // when running with `npm test` it adds environment variables that
7 | // mess with the things we expect here, so delete all of those.
8 | Object.keys(process.env)
9 | .filter(k => /^npm_/.test(k))
10 | .forEach(k => delete process.env[k])
11 | delete process.env.PREFIX
12 | delete process.env.DESTDIR
13 |
14 | const definitions = require('./fixtures/definitions.js')
15 | const shorthands = require('./fixtures/shorthands.js')
16 | const flatten = require('./fixtures/flatten.js')
17 | const typeDefs = require('../lib/type-defs.js')
18 |
19 | const { resolve, join, dirname } = require('path')
20 |
21 | const Config = t.mock('../', {
22 | 'fs/promises': {
23 | ...fs.promises,
24 | readFile: async (path, ...args) => {
25 | if (path.includes('WEIRD-ERROR')) {
26 | throw Object.assign(new Error('weird error'), { code: 'EWEIRD' })
27 | }
28 |
29 | return fs.promises.readFile(path, ...args)
30 | },
31 | },
32 | })
33 |
34 | // because we used t.mock above, the require cache gets blown and we lose our direct equality
35 | // on the typeDefs. to get around that, we require an un-mocked Config and assert against that
36 | const RealConfig = require('../')
37 | t.equal(typeDefs, RealConfig.typeDefs, 'exposes type definitions')
38 |
39 | t.test('construct with no settings, get default values for stuff', t => {
40 | const npmPath = t.testdir()
41 | const c = new Config({
42 | definitions: {},
43 | npmPath,
44 | })
45 |
46 | t.test('default some values from process object', t => {
47 | const { env, execPath, platform } = process
48 | const cwd = process.cwd()
49 | t.equal(c.env, env, 'env')
50 | t.equal(c.execPath, execPath, 'execPath')
51 | t.equal(c.cwd, cwd, 'cwd')
52 | t.equal(c.platform, platform, 'platform')
53 | t.end()
54 | })
55 |
56 | t.test('not loaded yet', t => {
57 | t.equal(c.loaded, false, 'not loaded yet')
58 | t.throws(() => c.get('foo'), {
59 | message: 'call config.load() before reading values',
60 | })
61 | t.throws(() => c.find('foo'), {
62 | message: 'call config.load() before reading values',
63 | })
64 | t.throws(() => c.set('foo', 'bar'), {
65 | message: 'call config.load() before setting values',
66 | })
67 | t.throws(() => c.delete('foo'), {
68 | message: 'call config.load() before deleting values',
69 | })
70 | t.rejects(() => c.save('user'), {
71 | message: 'call config.load() before saving',
72 | })
73 | t.throws(() => c.data.set('user', {}), {
74 | message: 'cannot change internal config data structure',
75 | })
76 | t.throws(() => c.data.delete('user'), {
77 | message: 'cannot change internal config data structure',
78 | })
79 | t.end()
80 | })
81 |
82 | t.test('data structure all wired up properly', t => {
83 | // verify that the proto objects are all wired up properly
84 | c.list.forEach((data, i) => {
85 | t.equal(Object.getPrototypeOf(data), c.list[i + 1] || null)
86 | })
87 | t.equal(c.data.get('default').data, c.list[c.list.length - 1])
88 | t.equal(c.data.get('cli').data, c.list[0])
89 | t.end()
90 | })
91 |
92 | t.end()
93 | })
94 |
95 | t.test('load from files and environment variables', t => {
96 | // need to get the dir because we reference it in the contents
97 | const path = t.testdir()
98 | t.testdir({
99 | npm: {
100 | npmrc: `
101 | builtin-config = true
102 | foo = from-builtin
103 | userconfig = ${path}/user/.npmrc-from-builtin
104 | `,
105 | },
106 | global: {
107 | etc: {
108 | npmrc: `
109 | global-config = true
110 | foo = from-global
111 | userconfig = ${path}/should-not-load-this-file
112 | `,
113 | },
114 | },
115 | user: {
116 | '.npmrc': `
117 | default-user-config-in-home = true
118 | foo = from-default-userconfig
119 | prefix = ${path}/global
120 | `,
121 | '.npmrc-from-builtin': `
122 | user-config-from-builtin = true
123 | foo = from-custom-userconfig
124 | globalconfig = ${path}/global/etc/npmrc
125 | `,
126 | },
127 | project: {
128 | node_modules: {},
129 | '.npmrc': `
130 | project-config = true
131 | foo = from-project-config
132 | loglevel = yolo
133 | `,
134 | },
135 | 'project-no-config': {
136 | 'package.json': '{"name":"@scope/project"}',
137 | },
138 | })
139 |
140 | const logs = []
141 | const logHandler = (...args) => logs.push(args)
142 | process.on('log', logHandler)
143 | t.teardown(() => process.off('log', logHandler))
144 |
145 | const argv = [
146 | process.execPath,
147 | __filename,
148 | '-v',
149 | '--no-audit',
150 | 'config',
151 | 'get',
152 | 'foo',
153 | '--also=dev',
154 | '--registry=hello',
155 | '--omit=cucumber',
156 | '--access=blueberry',
157 | '--multiple-numbers=what kind of fruit is not a number',
158 | '--multiple-numbers=a baNaNa!!',
159 | '-C',
160 | ]
161 |
162 | t.test('dont let userconfig be the same as builtin config', async t => {
163 | const config = new Config({
164 | npmPath: `${path}/npm`,
165 | env: {},
166 | argv: [process.execPath, __filename, '--userconfig', `${path}/npm/npmrc`],
167 | cwd: `${path}/project`,
168 | shorthands,
169 | definitions,
170 | })
171 | await t.rejects(() => config.load(), {
172 | message: `double-loading config "${resolve(path, 'npm/npmrc')}" as "user",` +
173 | ' previously loaded as "builtin"',
174 | })
175 | })
176 |
177 | t.test('dont load project config if global is true', async t => {
178 | const config = new Config({
179 | npmPath: `${path}/npm`,
180 | env: {},
181 | argv: [process.execPath, __filename, '--global'],
182 | cwd: `${path}/project`,
183 | shorthands,
184 | definitions,
185 | })
186 |
187 | await config.load()
188 | const source = config.data.get('project').source
189 | t.equal(source, '(global mode enabled, ignored)', 'data has placeholder')
190 | t.equal(config.sources.get(source), 'project', 'sources has project')
191 | })
192 |
193 | t.test('dont load project config if location is global', async t => {
194 | const config = new Config({
195 | npmPath: `${path}/npm`,
196 | env: {},
197 | argv: [process.execPath, __filename, '--location', 'global'],
198 | cwd: `${path}/project`,
199 | shorthands,
200 | definitions,
201 | })
202 |
203 | await config.load()
204 | const source = config.data.get('project').source
205 | t.equal(source, '(global mode enabled, ignored)', 'data has placeholder')
206 | t.equal(config.sources.get(source), 'project', 'sources has project')
207 | t.ok(config.localPrefix, 'localPrefix is set')
208 | })
209 |
210 | t.test('verbose log if config file read is weird error', async t => {
211 | const config = new Config({
212 | npmPath: path,
213 | env: {},
214 | argv: [process.execPath, __filename, '--userconfig', `${path}/WEIRD-ERROR`],
215 | cwd: path,
216 | shorthands,
217 | definitions,
218 | })
219 | logs.length = 0
220 | await config.load()
221 | t.match(logs, [['verbose', 'config', 'error loading user config', {
222 | message: 'weird error',
223 | }]])
224 | logs.length = 0
225 | })
226 |
227 | t.test('load configs from all files, cli, and env', async t => {
228 | const env = {
229 | npm_config_foo: 'from-env',
230 | npm_config_global: '',
231 | npm_config_prefix: '/something',
232 | }
233 | const config = new Config({
234 | npmPath: `${path}/npm`,
235 | env,
236 | argv,
237 | cwd: `${path}/project`,
238 |
239 | shorthands,
240 | definitions,
241 | })
242 |
243 | t.equal(config.globalPrefix, null, 'globalPrefix missing before load')
244 |
245 | await config.load()
246 |
247 | t.equal(config.globalPrefix, resolve('/something'), 'env-defined prefix should be loaded')
248 |
249 | t.equal(config.get('global', 'env'), undefined, 'empty env is missing')
250 | t.equal(config.get('global'), false, 'empty env is missing')
251 |
252 | config.set('asdf', 'quux', 'global')
253 | await config.save('global')
254 | const gres = readFileSync(`${path}/global/etc/npmrc`, 'utf8')
255 | t.match(gres, 'asdf=quux')
256 |
257 | const cliData = config.data.get('cli')
258 | t.throws(() => cliData.loadError = true, {
259 | message: 'cannot set ConfigData loadError after load',
260 | })
261 | t.throws(() => cliData.source = 'foo', {
262 | message: 'cannot set ConfigData source more than once',
263 | })
264 | t.throws(() => cliData.raw = 1234, {
265 | message: 'cannot set ConfigData raw after load',
266 | })
267 |
268 | config.argv = []
269 |
270 | t.throws(() => config.loadCLI(), {
271 | message: 'double-loading "cli" configs from command line options, previously loaded from' +
272 | ' command line options',
273 | })
274 | t.rejects(() => config.loadUserConfig(), {
275 | message: `double-loading "user" configs from ${resolve(path, 'should-not-load-this-file')}` +
276 | `, previously loaded from ${resolve(path, 'user/.npmrc-from-builtin')}`,
277 | })
278 |
279 | t.equal(config.loaded, true, 'config is loaded')
280 |
281 | await t.rejects(() => config.load(), {
282 | message: 'attempting to load npm config multiple times',
283 | })
284 | t.equal(config.find('no config value here'), null)
285 |
286 | t.equal(config.prefix, config.localPrefix, 'prefix is local prefix when not global')
287 | config.set('global', true)
288 | t.equal(config.prefix, config.globalPrefix, 'prefix is global prefix when global')
289 | config.set('global', false)
290 | t.equal(config.find('global'), 'cli')
291 | config.delete('global')
292 | t.equal(config.find('global'), 'default')
293 |
294 | t.throws(() => config.get('foo', 'barbaz'), {
295 | message: 'invalid config location param: barbaz',
296 | })
297 | t.throws(() => config.set('foo', 1234, 'barbaz'), {
298 | message: 'invalid config location param: barbaz',
299 | })
300 | t.throws(() => config.delete('foo', 'barbaz'), {
301 | message: 'invalid config location param: barbaz',
302 | })
303 |
304 | t.match(config.sources, new Map([
305 | ['default values', 'default'],
306 | [resolve(path, 'npm/npmrc'), 'builtin'],
307 | ['command line options', 'cli'],
308 | ['environment', 'env'],
309 | [resolve(path, 'project/.npmrc'), 'project'],
310 | [resolve(path, 'user/.npmrc-from-builtin'), 'user'],
311 | [resolve(path, 'global/etc/npmrc'), 'global'],
312 | ]))
313 |
314 | t.strictSame({
315 | version: config.get('version'),
316 | audit: config.get('audit'),
317 | 'project-config': config.get('project-config'),
318 | foo: config.get('foo'),
319 | 'user-config-from-builtin': config.get('user-config-from-builtin'),
320 | 'global-config': config.get('global-config'),
321 | 'builtin-config': config.get('builtin-config'),
322 | all: config.get('all'),
323 | }, {
324 | version: true,
325 | audit: false,
326 | 'project-config': true,
327 | foo: 'from-env',
328 | 'user-config-from-builtin': true,
329 | 'global-config': true,
330 | 'builtin-config': true,
331 | all: config.get('all'),
332 | })
333 |
334 | t.match(env, {
335 | npm_config_user_config_from_builtin: 'true',
336 | npm_config_audit: '',
337 | npm_config_version: 'true',
338 | npm_config_foo: 'from-env',
339 | npm_config_builtin_config: 'true',
340 | }, 'set env values')
341 |
342 | // warn logs are emitted as a side effect of validate
343 | config.validate()
344 | t.strictSame(logs, [
345 | ['warn', 'invalid config', 'registry="hello"', 'set in command line options'],
346 | ['warn', 'invalid config', 'Must be', 'full url with "http://"'],
347 | ['warn', 'invalid config', 'omit="cucumber"', 'set in command line options'],
348 | ['warn', 'invalid config', 'Must be one or more of:', 'dev, optional, peer'],
349 | ['warn', 'invalid config', 'access="blueberry"', 'set in command line options'],
350 | ['warn', 'invalid config', 'Must be one of:', 'null, restricted, public'],
351 | ['warn', 'invalid config', 'multiple-numbers="what kind of fruit is not a number"',
352 | 'set in command line options'],
353 | ['warn', 'invalid config', 'Must be one or more', 'numeric value'],
354 | ['warn', 'invalid config', 'multiple-numbers="a baNaNa!!"', 'set in command line options'],
355 | ['warn', 'invalid config', 'Must be one or more', 'numeric value'],
356 | ['warn', 'invalid config', 'prefix=true', 'set in command line options'],
357 | ['warn', 'invalid config', 'Must be', 'valid filesystem path'],
358 | ['warn', 'config', 'also', 'Please use --include=dev instead.'],
359 | ['warn', 'invalid config', 'loglevel="yolo"',
360 | `set in ${resolve(path, 'project/.npmrc')}`],
361 | ['warn', 'invalid config', 'Must be one of:',
362 | ['silent', 'error', 'warn', 'notice', 'http', 'timing', 'info',
363 | 'verbose', 'silly'].join(', '),
364 | ],
365 | ])
366 | t.equal(config.valid, false)
367 | logs.length = 0
368 |
369 | // set a new value that defaults to cli source
370 | config.set('cli-config', 1)
371 |
372 | t.ok(config.isDefault('methane'),
373 | 'should return true if value is retrieved from default definitions')
374 | t.notOk(config.isDefault('cli-config'),
375 | 'should return false for a cli-defined value')
376 | t.notOk(config.isDefault('foo'),
377 | 'should return false for a env-defined value')
378 | t.notOk(config.isDefault('project-config'),
379 | 'should return false for a project-defined value')
380 | t.notOk(config.isDefault('default-user-config-in-home'),
381 | 'should return false for a user-defined value')
382 | t.notOk(config.isDefault('global-config'),
383 | 'should return false for a global-defined value')
384 | t.notOk(config.isDefault('builtin-config'),
385 | 'should return false for a builtin-defined value')
386 |
387 | // make sure isDefault still works as intended after
388 | // setting and deleting values in differente sources
389 | config.set('methane', 'H2O', 'cli')
390 | t.notOk(config.isDefault('methane'),
391 | 'should no longer return true now that a cli value was defined')
392 | config.delete('methane', 'cli')
393 | t.ok(config.isDefault('methane'),
394 | 'should return true once again now that values is retrieved from defaults')
395 | })
396 |
397 | t.test('normalize config env keys', async t => {
398 | const env = {
399 | npm_config_bAr: 'bAr env',
400 | NPM_CONFIG_FOO: 'FOO env',
401 | 'npm_config_//reg.example/UP_CASE/:username': 'ME',
402 | 'npm_config_//reg.example/UP_CASE/:_password': 'Shhhh!',
403 | 'NPM_CONFIG_//reg.example/UP_CASE/:_authToken': 'sEcReT',
404 | }
405 | const config = new Config({
406 | npmPath: `${path}/npm`,
407 | env,
408 | argv,
409 | cwd: `${path}/project`,
410 |
411 | shorthands,
412 | definitions,
413 | })
414 |
415 | await config.load()
416 |
417 | t.strictSame({
418 | bar: config.get('bar'),
419 | foo: config.get('foo'),
420 | '//reg.example/UP_CASE/:username': config.get('//reg.example/UP_CASE/:username'),
421 | '//reg.example/UP_CASE/:_password': config.get('//reg.example/UP_CASE/:_password'),
422 | '//reg.example/UP_CASE/:_authToken': config.get('//reg.example/UP_CASE/:_authToken'),
423 | }, {
424 | bar: 'bAr env',
425 | foo: 'FOO env',
426 | '//reg.example/UP_CASE/:username': 'ME',
427 | '//reg.example/UP_CASE/:_password': 'Shhhh!',
428 | '//reg.example/UP_CASE/:_authToken': 'sEcReT',
429 | })
430 | })
431 |
432 | t.test('do not double-load project/user config', async t => {
433 | const env = {
434 | npm_config_foo: 'from-env',
435 | npm_config_globalconfig: '/this/path/does/not/exist',
436 | }
437 |
438 | const config = new Config({
439 | npmPath: `${path}/npm`,
440 | env,
441 | argv: [process.execPath, __filename, '--userconfig', `${path}/project/.npmrc`],
442 | cwd: `${path}/project`,
443 |
444 | shorthands,
445 | definitions,
446 | })
447 | await config.load()
448 |
449 | config.argv = []
450 | t.equal(config.loaded, true, 'config is loaded')
451 |
452 | t.match(config.data.get('global').loadError, { code: 'ENOENT' })
453 | t.strictSame(config.data.get('env').raw, Object.assign(Object.create(null), {
454 | foo: 'from-env',
455 | globalconfig: '/this/path/does/not/exist',
456 | }))
457 |
458 | t.match(config.sources, new Map([
459 | ['default values', 'default'],
460 | [resolve(path, 'npm/npmrc'), 'builtin'],
461 | ['command line options', 'cli'],
462 | ['environment', 'env'],
463 | ['(same as "user" config, ignored)', 'project'],
464 | [resolve(path, 'project/.npmrc'), 'user'],
465 | ]))
466 |
467 | t.rejects(() => config.save('yolo'), {
468 | message: 'invalid config location param: yolo',
469 | })
470 | config.validate()
471 | t.equal(config.valid, false, 'config should not be valid')
472 | logs.length = 0
473 | })
474 |
475 | t.test('load configs from files, cli, and env, no builtin or project', async t => {
476 | const env = {
477 | npm_config_foo: 'from-env',
478 | HOME: `${path}/user`,
479 | }
480 |
481 | const config = new Config({
482 | // no builtin
483 | npmPath: path,
484 | env,
485 | argv,
486 | cwd: `${path}/project-no-config`,
487 |
488 | // should prepend DESTDIR to /global
489 | DESTDIR: path,
490 | PREFIX: '/global',
491 | platform: 'posix',
492 |
493 | shorthands,
494 | definitions,
495 | })
496 | await config.load()
497 |
498 | t.match(config.sources, new Map([
499 | ['default values', 'default'],
500 | ['command line options', 'cli'],
501 | ['environment', 'env'],
502 | [resolve(path, 'user/.npmrc'), 'user'],
503 | [resolve(path, 'global/etc/npmrc'), 'global'],
504 | ]))
505 | // no builtin or project config
506 | t.equal(config.sources.get(resolve(path, 'npm/npmrc')), undefined)
507 | t.equal(config.sources.get(resolve(path, 'project/.npmrc')), undefined)
508 |
509 | t.strictSame({
510 | version: config.get('version'),
511 | audit: config.get('audit'),
512 | 'project-config': config.get('project-config'),
513 | foo: config.get('foo'),
514 | 'user-config-from-builtin': config.get('user-config-from-builtin'),
515 | 'default-user-config-in-home': config.get('default-user-config-in-home'),
516 | 'global-config': config.get('global-config'),
517 | 'builtin-config': config.get('builtin-config'),
518 | all: config.get('all'),
519 | }, {
520 | version: true,
521 | audit: false,
522 | 'project-config': undefined,
523 | foo: 'from-env',
524 | 'user-config-from-builtin': undefined,
525 | 'default-user-config-in-home': true,
526 | 'global-config': true,
527 | 'builtin-config': undefined,
528 | all: config.get('all'),
529 | })
530 |
531 | t.strictSame(logs, [
532 | ['warn', 'invalid config', 'registry="hello"', 'set in command line options'],
533 | ['warn', 'invalid config', 'Must be', 'full url with "http://"'],
534 | ['warn', 'invalid config', 'omit="cucumber"', 'set in command line options'],
535 | ['warn', 'invalid config', 'Must be one or more of:', 'dev, optional, peer'],
536 | ['warn', 'invalid config', 'access="blueberry"', 'set in command line options'],
537 | ['warn', 'invalid config', 'Must be one of:', 'null, restricted, public'],
538 | ['warn', 'invalid config', 'multiple-numbers="what kind of fruit is not a number"',
539 | 'set in command line options'],
540 | ['warn', 'invalid config', 'Must be one or more', 'numeric value'],
541 | ['warn', 'invalid config', 'multiple-numbers="a baNaNa!!"', 'set in command line options'],
542 | ['warn', 'invalid config', 'Must be one or more', 'numeric value'],
543 | ['warn', 'invalid config', 'prefix=true', 'set in command line options'],
544 | ['warn', 'invalid config', 'Must be', 'valid filesystem path'],
545 | ['warn', 'config', 'also', 'Please use --include=dev instead.'],
546 | ])
547 | })
548 |
549 | t.end()
550 | })
551 |
552 | t.test('cafile loads as ca (and some saving tests)', async t => {
553 | const cafile = resolve(__dirname, 'fixtures', 'cafile')
554 | const dir = t.testdir({
555 | '.npmrc': `cafile = ${cafile}
556 | //registry.npmjs.org/:_authToken = deadbeefcafebadfoobarbaz42069
557 | `,
558 | })
559 | const expect = `cafile=${cafile}
560 | //registry.npmjs.org/:_authToken=deadbeefcafebadfoobarbaz42069
561 | `
562 |
563 | const config = new Config({
564 | shorthands,
565 | definitions,
566 | npmPath: __dirname,
567 | env: { HOME: dir, PREFIX: dir },
568 | flatten,
569 | })
570 | await config.load()
571 | t.equal(config.get('ca'), null, 'does not overwrite config.get')
572 | const { flat } = config
573 | t.equal(config.flat, flat, 'getter returns same value again')
574 | const ca = flat.ca
575 | t.equal(ca.join('\n').replace(/\r\n/g, '\n').trim(), readFileSync(cafile, 'utf8')
576 | .replace(/\r\n/g, '\n').trim())
577 | await config.save('user')
578 | const res = readFileSync(`${dir}/.npmrc`, 'utf8').replace(/\r\n/g, '\n')
579 | t.equal(res, expect, 'did not write back ca, only cafile')
580 | // while we're here, test that saving an empty config file deletes it
581 | config.delete('cafile', 'user')
582 | config.clearCredentialsByURI(config.get('registry'))
583 | await config.save('user')
584 | t.throws(() => readFileSync(`${dir}/.npmrc`, 'utf8'), { code: 'ENOENT' })
585 | // do it again to verify we ignore the unlink error
586 | await config.save('user')
587 | t.throws(() => readFileSync(`${dir}/.npmrc`, 'utf8'), { code: 'ENOENT' })
588 | t.equal(config.valid, true)
589 | })
590 |
591 | t.test('cafile ignored if ca set', async t => {
592 | const cafile = resolve(__dirname, 'fixtures', 'cafile')
593 | const dir = t.testdir({
594 | '.npmrc': `cafile = ${cafile}`,
595 | })
596 | const ca = `
597 | -----BEGIN CERTIFICATE-----
598 | fakey mc fakerson
599 | -----END CERTIFICATE-----
600 | `
601 | const config = new Config({
602 | shorthands,
603 | definitions,
604 | npmPath: __dirname,
605 | env: {
606 | HOME: dir,
607 | npm_config_ca: ca,
608 | },
609 | })
610 | await config.load()
611 | t.strictSame(config.get('ca'), [ca.trim()])
612 | await config.save('user')
613 | const res = readFileSync(`${dir}/.npmrc`, 'utf8')
614 | t.equal(res.trim(), `cafile=${cafile}`)
615 | })
616 |
617 | t.test('ignore cafile if it does not load', async t => {
618 | const cafile = resolve(__dirname, 'fixtures', 'cafile-does-not-exist')
619 | const dir = t.testdir({
620 | '.npmrc': `cafile = ${cafile}`,
621 | })
622 | const config = new Config({
623 | shorthands,
624 | definitions,
625 | npmPath: __dirname,
626 | env: { HOME: dir },
627 | })
628 | await config.load()
629 | t.equal(config.get('ca'), null)
630 | await config.save('user')
631 | const res = readFileSync(`${dir}/.npmrc`, 'utf8')
632 | t.equal(res.trim(), `cafile=${cafile}`)
633 | })
634 |
635 | t.test('raise error if reading ca file error other than ENOENT', async t => {
636 | const cafile = resolve(__dirname, 'fixtures', 'WEIRD-ERROR')
637 | const dir = t.testdir({
638 | '.npmrc': `cafile = ${cafile}`,
639 | })
640 | const config = new Config({
641 | shorthands,
642 | definitions,
643 | npmPath: __dirname,
644 | env: { HOME: dir },
645 | flatten,
646 | })
647 | await config.load()
648 | t.throws(() => config.flat.ca, { code: 'EWEIRD' })
649 | })
650 |
651 | t.test('credentials management', async t => {
652 | const fixtures = {
653 | nerfed_authToken: { '.npmrc': '//registry.example/:_authToken = 0bad1de4' },
654 | nerfed_userpass: {
655 | '.npmrc': `//registry.example/:username = hello
656 | //registry.example/:_password = ${Buffer.from('world').toString('base64')}
657 | //registry.example/:email = i@izs.me
658 | //registry.example/:always-auth = "false"`,
659 | },
660 | nerfed_auth: { // note: does not load, because we don't do _auth per reg
661 | '.npmrc': `//registry.example/:_auth = ${Buffer.from('hello:world').toString('base64')}`,
662 | },
663 | nerfed_mtls: { '.npmrc': `//registry.example/:certfile = /path/to/cert
664 | //registry.example/:keyfile = /path/to/key`,
665 | },
666 | nerfed_mtlsAuthToken: { '.npmrc': `//registry.example/:_authToken = 0bad1de4
667 | //registry.example/:certfile = /path/to/cert
668 | //registry.example/:keyfile = /path/to/key`,
669 | },
670 | nerfed_mtlsUserPass: { '.npmrc': `//registry.example/:username = hello
671 | //registry.example/:_password = ${Buffer.from('world').toString('base64')}
672 | //registry.example/:email = i@izs.me
673 | //registry.example/:always-auth = "false"
674 | //registry.example/:certfile = /path/to/cert
675 | //registry.example/:keyfile = /path/to/key`,
676 | },
677 | def_userpass: {
678 | '.npmrc': `username = hello
679 | _password = ${Buffer.from('world').toString('base64')}
680 | email = i@izs.me
681 | //registry.example/:always-auth = true
682 | `,
683 | },
684 | def_userNoPass: {
685 | '.npmrc': `username = hello
686 | email = i@izs.me
687 | //registry.example/:always-auth = true
688 | `,
689 | },
690 | def_passNoUser: {
691 | '.npmrc': `_password = ${Buffer.from('world').toString('base64')}
692 | email = i@izs.me
693 | //registry.example/:always-auth = true
694 | `,
695 | },
696 | def_auth: {
697 | '.npmrc': `_auth = ${Buffer.from('hello:world').toString('base64')}
698 | always-auth = true`,
699 | },
700 | none_authToken: { '.npmrc': '_authToken = 0bad1de4' },
701 | none_lcAuthToken: { '.npmrc': '_authtoken = 0bad1de4' },
702 | none_emptyConfig: { '.npmrc': '' },
703 | none_noConfig: {},
704 | }
705 | const path = t.testdir(fixtures)
706 |
707 | const defReg = 'https://registry.example/'
708 | const otherReg = 'https://other.registry/'
709 | for (const testCase of Object.keys(fixtures)) {
710 | t.test(testCase, async t => {
711 | const c = new Config({
712 | npmPath: path,
713 | shorthands,
714 | definitions,
715 | env: { HOME: resolve(path, testCase) },
716 | argv: ['node', 'file', '--registry', defReg],
717 | })
718 | await c.load()
719 |
720 | // only have to do this the first time, it's redundant otherwise
721 | if (testCase === 'none_noConfig') {
722 | t.throws(() => c.setCredentialsByURI('http://x.com', {
723 | username: 'foo',
724 | email: 'bar@baz.com',
725 | }), { message: 'must include password' })
726 | t.throws(() => c.setCredentialsByURI('http://x.com', {
727 | password: 'foo',
728 | email: 'bar@baz.com',
729 | }), { message: 'must include username' })
730 | c.setCredentialsByURI('http://x.com', {
731 | username: 'foo',
732 | password: 'bar',
733 | email: 'asdf@quux.com',
734 | })
735 | }
736 |
737 | // the def_ and none_ prefixed cases have unscoped auth values and should throw
738 | if (testCase.startsWith('def_') ||
739 | testCase === 'none_authToken' ||
740 | testCase === 'none_lcAuthToken') {
741 | try {
742 | c.validate()
743 | // validate should throw, fail the test here if it doesn't
744 | t.fail('validate should have thrown')
745 | } catch (err) {
746 | if (err.code !== 'ERR_INVALID_AUTH') {
747 | throw err
748 | }
749 |
750 | // we got our expected invalid auth error, so now repair it
751 | c.repair(err.problems)
752 | t.ok(c.valid, 'config is valid')
753 | }
754 | } else {
755 | // validate won't throw for these ones, so let's prove it and repair are no-ops
756 | c.validate()
757 | c.repair()
758 | }
759 |
760 | const d = c.getCredentialsByURI(defReg)
761 | const o = c.getCredentialsByURI(otherReg)
762 |
763 | t.matchSnapshot(d, 'default registry')
764 | t.matchSnapshot(o, 'other registry')
765 |
766 | c.clearCredentialsByURI(defReg)
767 | const defAfterDelete = c.getCredentialsByURI(defReg)
768 | {
769 | const expectKeys = []
770 | if (defAfterDelete.email) {
771 | expectKeys.push('email')
772 | }
773 | t.strictSame(Object.keys(defAfterDelete), expectKeys)
774 | }
775 |
776 | c.clearCredentialsByURI(otherReg)
777 | const otherAfterDelete = c.getCredentialsByURI(otherReg)
778 | {
779 | const expectKeys = []
780 | if (otherAfterDelete.email) {
781 | expectKeys.push('email')
782 | }
783 | t.strictSame(Object.keys(otherAfterDelete), expectKeys)
784 | }
785 |
786 | // need both or none of user/pass
787 | if (!d.token && (!d.username || !d.password) && (!d.certfile || !d.keyfile)) {
788 | t.throws(() => c.setCredentialsByURI(defReg, d))
789 | } else {
790 | c.setCredentialsByURI(defReg, d)
791 | t.matchSnapshot(c.getCredentialsByURI(defReg), 'default registry after set')
792 | }
793 |
794 | if (!o.token && (!o.username || !o.password) && (!o.certfile || !o.keyfile)) {
795 | t.throws(() => c.setCredentialsByURI(otherReg, o), {}, { otherReg, o })
796 | } else {
797 | c.setCredentialsByURI(otherReg, o)
798 | t.matchSnapshot(c.getCredentialsByURI(otherReg), 'other registry after set')
799 | }
800 | })
801 | }
802 | t.end()
803 | })
804 |
805 | t.test('finding the global prefix', t => {
806 | const npmPath = __dirname
807 | t.test('load from PREFIX env', t => {
808 | const c = new Config({
809 | env: {
810 | PREFIX: '/prefix/env',
811 | },
812 | shorthands,
813 | definitions,
814 | npmPath,
815 | })
816 | c.loadGlobalPrefix()
817 | t.throws(() => c.loadGlobalPrefix(), {
818 | message: 'cannot load default global prefix more than once',
819 | })
820 | t.equal(c.globalPrefix, '/prefix/env')
821 | t.end()
822 | })
823 | t.test('load from execPath, win32', t => {
824 | const c = new Config({
825 | platform: 'win32',
826 | execPath: '/path/to/nodejs/node.exe',
827 | shorthands,
828 | definitions,
829 | npmPath,
830 | })
831 | c.loadGlobalPrefix()
832 | t.equal(c.globalPrefix, dirname('/path/to/nodejs/node.exe'))
833 | t.end()
834 | })
835 | t.test('load from execPath, posix', t => {
836 | const c = new Config({
837 | platform: 'posix',
838 | execPath: '/path/to/nodejs/bin/node',
839 | shorthands,
840 | definitions,
841 | npmPath,
842 | })
843 | c.loadGlobalPrefix()
844 | t.equal(c.globalPrefix, dirname(dirname('/path/to/nodejs/bin/node')))
845 | t.end()
846 | })
847 | t.test('load from execPath with destdir, posix', t => {
848 | const c = new Config({
849 | platform: 'posix',
850 | execPath: '/path/to/nodejs/bin/node',
851 | env: { DESTDIR: '/some/dest/dir' },
852 | shorthands,
853 | definitions,
854 | npmPath,
855 | })
856 | c.loadGlobalPrefix()
857 | t.equal(c.globalPrefix, join('/some/dest/dir', dirname(dirname('/path/to/nodejs/bin/node'))))
858 | t.end()
859 | })
860 | t.end()
861 | })
862 |
863 | t.test('finding the local prefix', t => {
864 | const path = t.testdir({
865 | hasNM: {
866 | node_modules: {},
867 | x: { y: { z: {} } },
868 | },
869 | hasPJ: {
870 | 'package.json': '{}',
871 | x: { y: { z: {} } },
872 | },
873 | })
874 | t.test('explicit cli prefix', async t => {
875 | const c = new Config({
876 | argv: [process.execPath, __filename, '-C', path],
877 | shorthands,
878 | definitions,
879 | npmPath: path,
880 | })
881 | await c.load()
882 | t.equal(c.localPrefix, resolve(path))
883 | })
884 | t.test('has node_modules', async t => {
885 | const c = new Config({
886 | cwd: `${path}/hasNM/x/y/z`,
887 | shorthands,
888 | definitions,
889 | npmPath: path,
890 | })
891 | await c.load()
892 | t.equal(c.localPrefix, resolve(path, 'hasNM'))
893 | })
894 | t.test('has package.json', async t => {
895 | const c = new Config({
896 | cwd: `${path}/hasPJ/x/y/z`,
897 | shorthands,
898 | definitions,
899 | npmPath: path,
900 | })
901 | await c.load()
902 | t.equal(c.localPrefix, resolve(path, 'hasPJ'))
903 | })
904 | t.test('nada, just use cwd', async t => {
905 | const c = new Config({
906 | cwd: '/this/path/does/not/exist/x/y/z',
907 | shorthands,
908 | definitions,
909 | npmPath: path,
910 | })
911 | await c.load()
912 | t.equal(c.localPrefix, '/this/path/does/not/exist/x/y/z')
913 | })
914 | t.end()
915 | })
916 |
917 | t.test('setting basic auth creds and email', async t => {
918 | const registry = 'https://registry.npmjs.org/'
919 | const path = t.testdir()
920 | const _auth = Buffer.from('admin:admin').toString('base64')
921 | const opts = {
922 | shorthands: {},
923 | argv: ['node', __filename, `--userconfig=${path}/.npmrc`],
924 | definitions: {
925 | registry: { default: registry },
926 | },
927 | npmPath: process.cwd(),
928 | }
929 | const c = new Config(opts)
930 | await c.load()
931 | c.set('email', 'name@example.com', 'user')
932 | t.equal(c.get('email', 'user'), 'name@example.com', 'email was set')
933 | await c.save('user')
934 | t.equal(c.get('email', 'user'), 'name@example.com', 'email still top level')
935 | t.strictSame(c.getCredentialsByURI(registry), { email: 'name@example.com' })
936 | const d = new Config(opts)
937 | await d.load()
938 | t.strictSame(d.getCredentialsByURI(registry), { email: 'name@example.com' })
939 | d.set('_auth', _auth, 'user')
940 | t.equal(d.get('_auth', 'user'), _auth, '_auth was set')
941 | d.repair()
942 | await d.save('user')
943 | const e = new Config(opts)
944 | await e.load()
945 | t.equal(e.get('_auth', 'user'), undefined, 'un-nerfed _auth deleted')
946 | t.strictSame(e.getCredentialsByURI(registry), {
947 | email: 'name@example.com',
948 | username: 'admin',
949 | password: 'admin',
950 | auth: _auth,
951 | }, 'credentials saved and nerfed')
952 | })
953 |
954 | t.test('setting username/password/email individually', async t => {
955 | const registry = 'https://registry.npmjs.org/'
956 | const path = t.testdir()
957 | const opts = {
958 | shorthands: {},
959 | argv: ['node', __filename, `--userconfig=${path}/.npmrc`],
960 | definitions: {
961 | registry: { default: registry },
962 | },
963 | npmPath: process.cwd(),
964 | }
965 | const c = new Config(opts)
966 | await c.load()
967 | c.set('email', 'name@example.com', 'user')
968 | t.equal(c.get('email'), 'name@example.com')
969 | c.set('username', 'admin', 'user')
970 | t.equal(c.get('username'), 'admin')
971 | c.set('_password', Buffer.from('admin').toString('base64'), 'user')
972 | t.equal(c.get('_password'), Buffer.from('admin').toString('base64'))
973 | t.equal(c.get('_auth'), undefined)
974 | c.repair()
975 | await c.save('user')
976 |
977 | const d = new Config(opts)
978 | await d.load()
979 | t.equal(d.get('email'), 'name@example.com')
980 | t.equal(d.get('username'), undefined)
981 | t.equal(d.get('_password'), undefined)
982 | t.equal(d.get('_auth'), undefined)
983 | t.strictSame(d.getCredentialsByURI(registry), {
984 | email: 'name@example.com',
985 | username: 'admin',
986 | password: 'admin',
987 | auth: Buffer.from('admin:admin').toString('base64'),
988 | })
989 | })
990 |
991 | t.test('nerfdart auths set at the top level into the registry', async t => {
992 | const registry = 'https://registry.npmjs.org/'
993 | const _auth = Buffer.from('admin:admin').toString('base64')
994 | const username = 'admin'
995 | const _password = Buffer.from('admin').toString('base64')
996 | const email = 'i@izs.me'
997 | const _authToken = 'deadbeefblahblah'
998 |
999 | // name: [ini, expect, wontThrow]
1000 | const cases = {
1001 | '_auth only, no email': [`_auth=${_auth}`, {
1002 | '//registry.npmjs.org/:_auth': _auth,
1003 | }],
1004 | '_auth with email': [`_auth=${_auth}\nemail=${email}`, {
1005 | '//registry.npmjs.org/:_auth': _auth,
1006 | email,
1007 | }],
1008 | '_authToken alone': [`_authToken=${_authToken}`, {
1009 | '//registry.npmjs.org/:_authToken': _authToken,
1010 | }],
1011 | '_authToken and email': [`_authToken=${_authToken}\nemail=${email}`, {
1012 | '//registry.npmjs.org/:_authToken': _authToken,
1013 | email,
1014 | }],
1015 | 'username and _password': [`username=${username}\n_password=${_password}`, {
1016 | '//registry.npmjs.org/:username': username,
1017 | '//registry.npmjs.org/:_password': _password,
1018 | }],
1019 | 'username, password, email': [`username=${username}\n_password=${_password}\nemail=${email}`, {
1020 | '//registry.npmjs.org/:username': username,
1021 | '//registry.npmjs.org/:_password': _password,
1022 | email,
1023 | }],
1024 | // handled invalid/legacy cases
1025 | 'username, no _password': [`username=${username}`, {}],
1026 | '_password, no username': [`_password=${_password}`, {}],
1027 | '_authtoken instead of _authToken': [`_authtoken=${_authToken}`, {}],
1028 | '-authtoken instead of _authToken': [`-authtoken=${_authToken}`, {}],
1029 | // de-nerfdart the email, if present in that way
1030 | 'nerf-darted email': [`//registry.npmjs.org/:email=${email}`, {
1031 | email,
1032 | }, true],
1033 | }
1034 |
1035 | const logs = []
1036 | const logHandler = (...args) => logs.push(args)
1037 | process.on('log', logHandler)
1038 | t.teardown(() => {
1039 | process.removeListener('log', logHandler)
1040 | })
1041 | const cwd = process.cwd()
1042 | for (const [name, [ini, expect, wontThrow]] of Object.entries(cases)) {
1043 | t.test(name, async t => {
1044 | t.teardown(() => {
1045 | process.chdir(cwd)
1046 | logs.length = 0
1047 | })
1048 | const path = t.testdir({
1049 | '.npmrc': ini,
1050 | 'package.json': JSON.stringify({}),
1051 | })
1052 | process.chdir(path)
1053 | const argv = [
1054 | 'node',
1055 | __filename,
1056 | `--prefix=${path}`,
1057 | `--userconfig=${path}/.npmrc`,
1058 | `--globalconfig=${path}/etc/npmrc`,
1059 | ]
1060 | const opts = {
1061 | shorthands: {},
1062 | argv,
1063 | env: {},
1064 | definitions: {
1065 | registry: { default: registry },
1066 | },
1067 | npmPath: process.cwd(),
1068 | }
1069 |
1070 | const c = new Config(opts)
1071 | await c.load()
1072 |
1073 | if (!wontThrow) {
1074 | t.throws(() => c.validate(), { code: 'ERR_INVALID_AUTH' })
1075 | }
1076 |
1077 | // now we go ahead and do the repair, and save
1078 | c.repair()
1079 | await c.save('user')
1080 | t.same(c.list[3], expect)
1081 | })
1082 | }
1083 | })
1084 |
1085 | t.test('workspaces', async (t) => {
1086 | const path = resolve(t.testdir({
1087 | 'package.json': JSON.stringify({
1088 | name: 'root',
1089 | version: '1.0.0',
1090 | workspaces: ['./workspaces/*'],
1091 | }),
1092 | workspaces: {
1093 | one: {
1094 | 'package.json': JSON.stringify({
1095 | name: 'one',
1096 | version: '1.0.0',
1097 | }),
1098 | },
1099 | two: {
1100 | 'package.json': JSON.stringify({
1101 | name: 'two',
1102 | version: '1.0.0',
1103 | }),
1104 | },
1105 | three: {
1106 | 'package.json': JSON.stringify({
1107 | name: 'three',
1108 | version: '1.0.0',
1109 | }),
1110 | '.npmrc': 'package-lock=false',
1111 | },
1112 | },
1113 | }))
1114 |
1115 | const logs = []
1116 | const logHandler = (...args) => logs.push(args)
1117 | process.on('log', logHandler)
1118 | t.teardown(() => process.off('log', logHandler))
1119 | t.afterEach(() => logs.length = 0)
1120 |
1121 | t.test('finds own parent', async (t) => {
1122 | const cwd = process.cwd()
1123 | t.teardown(() => process.chdir(cwd))
1124 | process.chdir(`${path}/workspaces/one`)
1125 |
1126 | const config = new Config({
1127 | npmPath: cwd,
1128 | env: {},
1129 | argv: [process.execPath, __filename],
1130 | cwd: `${path}/workspaces/one`,
1131 | shorthands,
1132 | definitions,
1133 | })
1134 |
1135 | await config.load()
1136 | t.equal(config.localPrefix, path, 'localPrefix is the root')
1137 | t.same(config.get('workspace'), [join(path, 'workspaces', 'one')], 'set the workspace')
1138 | t.equal(logs.length, 1, 'got one log message')
1139 | t.match(logs[0], ['info', /^found workspace root at/], 'logged info about workspace root')
1140 | })
1141 |
1142 | t.test('finds other workspace parent', async (t) => {
1143 | const cwd = process.cwd()
1144 | t.teardown(() => process.chdir(cwd))
1145 | process.chdir(`${path}/workspaces/one`)
1146 |
1147 | const config = new Config({
1148 | npmPath: process.cwd(),
1149 | env: {},
1150 | argv: [process.execPath, __filename, '--workspace', '../two'],
1151 | cwd: `${path}/workspaces/one`,
1152 | shorthands,
1153 | definitions,
1154 | })
1155 |
1156 | await config.load()
1157 | t.equal(config.localPrefix, path, 'localPrefix is the root')
1158 | t.same(config.get('workspace'), ['../two'], 'kept the specified workspace')
1159 | t.equal(logs.length, 1, 'got one log message')
1160 | t.match(logs[0], ['info', /^found workspace root at/], 'logged info about workspace root')
1161 | })
1162 |
1163 | t.test('warns when workspace has .npmrc', async (t) => {
1164 | const cwd = process.cwd()
1165 | t.teardown(() => process.chdir(cwd))
1166 | process.chdir(`${path}/workspaces/three`)
1167 |
1168 | const config = new Config({
1169 | npmPath: process.cwd(),
1170 | env: {},
1171 | argv: [process.execPath, __filename],
1172 | cwd: `${path}/workspaces/three`,
1173 | shorthands,
1174 | definitions,
1175 | })
1176 |
1177 | await config.load()
1178 | t.equal(config.localPrefix, path, 'localPrefix is the root')
1179 | t.same(config.get('workspace'), [join(path, 'workspaces', 'three')], 'kept the workspace')
1180 | t.equal(logs.length, 2, 'got two log messages')
1181 | t.match(logs[0], ['warn', /^ignoring workspace config/], 'warned about ignored config')
1182 | t.match(logs[1], ['info', /^found workspace root at/], 'logged info about workspace root')
1183 | })
1184 |
1185 | t.test('prefix skips auto detect', async (t) => {
1186 | const cwd = process.cwd()
1187 | t.teardown(() => process.chdir(cwd))
1188 | process.chdir(`${path}/workspaces/one`)
1189 |
1190 | const config = new Config({
1191 | npmPath: process.cwd(),
1192 | env: {},
1193 | argv: [process.execPath, __filename, '--prefix', './'],
1194 | cwd: `${path}/workspaces/one`,
1195 | shorthands,
1196 | definitions,
1197 | })
1198 |
1199 | await config.load()
1200 | t.equal(config.localPrefix, join(path, 'workspaces', 'one'), 'localPrefix is the root')
1201 | t.same(config.get('workspace'), [], 'did not set workspace')
1202 | t.equal(logs.length, 0, 'got no log messages')
1203 | })
1204 |
1205 | t.test('no-workspaces skips auto detect', async (t) => {
1206 | const cwd = process.cwd()
1207 | t.teardown(() => process.chdir(cwd))
1208 | process.chdir(`${path}/workspaces/one`)
1209 |
1210 | const config = new Config({
1211 | npmPath: process.cwd(),
1212 | env: {},
1213 | argv: [process.execPath, __filename, '--no-workspaces'],
1214 | cwd: `${path}/workspaces/one`,
1215 | shorthands,
1216 | definitions,
1217 | })
1218 |
1219 | await config.load()
1220 | t.equal(config.localPrefix, join(path, 'workspaces', 'one'), 'localPrefix is the root')
1221 | t.same(config.get('workspace'), [], 'did not set workspace')
1222 | t.equal(logs.length, 0, 'got no log messages')
1223 | })
1224 |
1225 | t.test('global skips auto detect', async (t) => {
1226 | const cwd = process.cwd()
1227 | t.teardown(() => process.chdir(cwd))
1228 | process.chdir(`${path}/workspaces/one`)
1229 |
1230 | const config = new Config({
1231 | npmPath: process.cwd(),
1232 | env: {},
1233 | argv: [process.execPath, __filename, '--global'],
1234 | cwd: `${path}/workspaces/one`,
1235 | shorthands,
1236 | definitions,
1237 | })
1238 |
1239 | await config.load()
1240 | t.equal(config.localPrefix, join(path, 'workspaces', 'one'), 'localPrefix is the root')
1241 | t.same(config.get('workspace'), [], 'did not set workspace')
1242 | t.equal(logs.length, 0, 'got no log messages')
1243 | })
1244 |
1245 | t.test('location=global skips auto detect', async (t) => {
1246 | const cwd = process.cwd()
1247 | t.teardown(() => process.chdir(cwd))
1248 | process.chdir(`${path}/workspaces/one`)
1249 |
1250 | const config = new Config({
1251 | npmPath: process.cwd(),
1252 | env: {},
1253 | argv: [process.execPath, __filename, '--location=global'],
1254 | cwd: `${path}/workspaces/one`,
1255 | shorthands,
1256 | definitions,
1257 | })
1258 |
1259 | await config.load()
1260 | t.equal(config.localPrefix, join(path, 'workspaces', 'one'), 'localPrefix is the root')
1261 | t.same(config.get('workspace'), [], 'did not set workspace')
1262 | t.equal(logs.length, 0, 'got no log messages')
1263 | })
1264 |
1265 | t.test('does not error for invalid package.json', async (t) => {
1266 | const invalidPkg = join(path, 'workspaces', 'package.json')
1267 | const cwd = process.cwd()
1268 | t.teardown(() => {
1269 | fs.unlinkSync(invalidPkg)
1270 | process.chdir(cwd)
1271 | })
1272 | process.chdir(`${path}/workspaces/one`)
1273 |
1274 | // write some garbage to the file so read-package-json-fast will throw
1275 | fs.writeFileSync(invalidPkg, 'not-json')
1276 | const config = new Config({
1277 | npmPath: cwd,
1278 | env: {},
1279 | argv: [process.execPath, __filename],
1280 | cwd: `${path}/workspaces/one`,
1281 | shorthands,
1282 | definitions,
1283 | })
1284 |
1285 | await config.load()
1286 | t.equal(config.localPrefix, path, 'localPrefix is the root')
1287 | t.same(config.get('workspace'), [join(path, 'workspaces', 'one')], 'set the workspace')
1288 | t.equal(logs.length, 1, 'got one log message')
1289 | t.match(logs[0], ['info', /^found workspace root at/], 'logged info about workspace root')
1290 | })
1291 | })
1292 |
--------------------------------------------------------------------------------