├── 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 | --------------------------------------------------------------------------------