npm
Version:
a package manager for JavaScript
967 lines (838 loc) • 30.2 kB
JavaScript
// TODO: set the scope config from package.json or explicit cli config
const { walkUp } = require('walk-up-path')
const ini = require('ini')
const nopt = require('nopt')
const { log, time } = require('proc-log')
const { resolve, dirname, join } = require('node:path')
const { homedir } = require('node:os')
const {
readFile,
writeFile,
chmod,
unlink,
stat,
mkdir,
} = require('node:fs/promises')
// TODO these need to be either be ignored when parsing env, formalized as config, or not exported to the env in the first place. For now this list is just to suppress warnings till we can pay off this tech debt.
const internalEnv = [
'global-prefix',
'local-prefix',
'npm-version',
'node-gyp',
]
const fileExists = (...p) => stat(resolve(...p))
.then((st) => st.isFile())
.catch(() => false)
const dirExists = (...p) => stat(resolve(...p))
.then((st) => st.isDirectory())
.catch(() => false)
const hasOwnProperty = (obj, key) =>
Object.prototype.hasOwnProperty.call(obj, key)
const typeDefs = require('./type-defs.js')
const nerfDart = require('./nerf-dart.js')
const envReplace = require('./env-replace.js')
const parseField = require('./parse-field.js')
const setEnvs = require('./set-envs.js')
// types that can be saved back to
const confFileTypes = new Set([
'global',
'user',
'project',
])
const confTypes = new Set([
'default',
'builtin',
...confFileTypes,
'env',
'cli',
])
class Config {
#loaded = false
#flatten
// populated the first time we flatten the object
#flatOptions = null
static get typeDefs () {
return typeDefs
}
constructor ({
definitions,
shorthands,
flatten,
nerfDarts = [],
npmPath,
// options just to override in tests, mostly
env = process.env,
argv = process.argv,
platform = process.platform,
execPath = process.execPath,
cwd = process.cwd(),
excludeNpmCwd = false,
}) {
this.nerfDarts = nerfDarts
this.definitions = definitions
// turn the definitions into nopt's weirdo syntax
const types = {}
const defaults = {}
this.deprecated = {}
for (const [key, def] of Object.entries(definitions)) {
defaults[key] = def.default
types[key] = def.type
if (def.deprecated) {
this.deprecated[key] = def.deprecated.trim().replace(/\n +/, '\n')
}
}
this.#flatten = flatten
this.types = types
this.shorthands = shorthands
this.defaults = defaults
this.npmPath = npmPath
this.npmBin = join(this.npmPath, 'bin/npm-cli.js')
this.argv = argv
this.env = env
this.execPath = execPath
this.platform = platform
this.cwd = cwd
this.excludeNpmCwd = excludeNpmCwd
// set when we load configs
this.globalPrefix = null
this.localPrefix = null
this.localPackage = null
// defaults to env.HOME, but will always be *something*
this.home = null
// set up the prototype chain of config objects
const wheres = [...confTypes]
this.data = new Map()
let parent = null
for (const where of wheres) {
this.data.set(where, parent = new ConfigData(parent))
}
this.data.set = () => {
throw new Error('cannot change internal config data structure')
}
this.data.delete = () => {
throw new Error('cannot change internal config data structure')
}
this.sources = new Map([])
this.list = []
for (const { data } of this.data.values()) {
this.list.unshift(data)
}
Object.freeze(this.list)
this.#loaded = false
}
get loaded () {
return this.#loaded
}
get prefix () {
return this.#get('global') ? this.globalPrefix : this.localPrefix
}
// return the location where key is found.
find (key) {
if (!this.loaded) {
throw new Error('call config.load() before reading values')
}
// have to look in reverse order
const entries = [...this.data.entries()]
for (let i = entries.length - 1; i > -1; i--) {
const [where, { data }] = entries[i]
if (hasOwnProperty(data, key)) {
return where
}
}
return null
}
get (key, where) {
if (!this.loaded) {
throw new Error('call config.load() before reading values')
}
return this.#get(key, where)
}
// we need to get values sometimes, so use this internal one to do so
// while in the process of loading.
#get (key, where = null) {
if (where !== null && !confTypes.has(where)) {
throw new Error('invalid config location param: ' + where)
}
const { data } = this.data.get(where || 'cli')
return where === null || hasOwnProperty(data, key) ? data[key] : undefined
}
set (key, val, where = 'cli') {
if (!this.loaded) {
throw new Error('call config.load() before setting values')
}
if (!confTypes.has(where)) {
throw new Error('invalid config location param: ' + where)
}
this.#checkDeprecated(key)
const { data, raw } = this.data.get(where)
data[key] = val
if (['global', 'user', 'project'].includes(where)) {
raw[key] = val
}
// this is now dirty, the next call to this.valid will have to check it
this.data.get(where)[_valid] = null
// the flat options are invalidated, regenerate next time they're needed
this.#flatOptions = null
}
get flat () {
if (this.#flatOptions) {
return this.#flatOptions
}
// create the object for flat options passed to deps
const timeEnd = time.start('config:load:flatten')
this.#flatOptions = {}
// walk from least priority to highest
for (const { data } of this.data.values()) {
this.#flatten(data, this.#flatOptions)
}
this.#flatOptions.nodeBin = this.execPath
this.#flatOptions.npmBin = this.npmBin
timeEnd()
return this.#flatOptions
}
delete (key, where = 'cli') {
if (!this.loaded) {
throw new Error('call config.load() before deleting values')
}
if (!confTypes.has(where)) {
throw new Error('invalid config location param: ' + where)
}
const { data, raw } = this.data.get(where)
delete data[key]
if (['global', 'user', 'project'].includes(where)) {
delete raw[key]
}
}
async load () {
if (this.loaded) {
throw new Error('attempting to load npm config multiple times')
}
// first load the defaults, which sets the global prefix
this.loadDefaults()
// next load the builtin config, as this sets new effective defaults
await this.loadBuiltinConfig()
// cli and env are not async, and can set the prefix, relevant to project
this.loadCLI()
this.loadEnv()
// next project config, which can affect userconfig location
await this.loadProjectConfig()
// then user config, which can affect globalconfig location
await this.loadUserConfig()
// last but not least, global config file
await this.loadGlobalConfig()
// set this before calling setEnvs, so that we don't have to share
// private attributes, as that module also does a bunch of get operations
this.#loaded = true
// set proper globalPrefix now that everything is loaded
this.globalPrefix = this.get('prefix')
this.setEnvs()
}
loadDefaults () {
this.loadGlobalPrefix()
this.loadHome()
const defaultsObject = {
...this.defaults,
prefix: this.globalPrefix,
}
try {
// This does not have an actual definition
defaultsObject['npm-version'] = require(join(this.npmPath, 'package.json')).version
} catch {
// in some weird state where the passed in npmPath does not have a package.json
// this will never happen in npm, but is guarded here in case this is consumed
// in other ways + tests
}
this.#loadObject(defaultsObject, 'default', 'default values')
const { data } = this.data.get('default')
// if the prefix is set on cli, env, or userconfig, then we need to
// default the globalconfig file to that location, instead of the default
// global prefix. It's weird that `npm get globalconfig --prefix=/foo`
// returns `/foo/etc/npmrc`, but better to not change it at this point.
// define a custom getter, but turn into a normal prop
// if we set it. otherwise it can't be set on child objects
Object.defineProperty(data, 'globalconfig', {
get: () => resolve(this.#get('prefix'), 'etc/npmrc'),
set (value) {
Object.defineProperty(data, 'globalconfig', {
value,
configurable: true,
writable: true,
enumerable: true,
})
},
configurable: true,
enumerable: true,
})
}
loadHome () {
this.home = this.env.HOME || homedir()
}
loadGlobalPrefix () {
if (this.globalPrefix) {
throw new Error('cannot load default global prefix more than once')
}
if (this.env.PREFIX) {
this.globalPrefix = this.env.PREFIX
} else if (this.platform === 'win32') {
// c:\node\node.exe --> prefix=c:\node\
this.globalPrefix = dirname(this.execPath)
} else {
// /usr/local/bin/node --> prefix=/usr/local
this.globalPrefix = dirname(dirname(this.execPath))
// destdir only is respected on Unix
if (this.env.DESTDIR) {
this.globalPrefix = join(this.env.DESTDIR, this.globalPrefix)
}
}
}
loadEnv () {
const conf = Object.create(null)
for (const [envKey, envVal] of Object.entries(this.env)) {
if (!/^npm_config_/i.test(envKey) || envVal === '') {
continue
}
let key = envKey.slice('npm_config_'.length)
if (!key.startsWith('//')) { // don't normalize nerf-darted keys
key = key.replace(/(?!^)_/g, '-') // don't replace _ at the start of the key
.toLowerCase()
}
conf[key] = envVal
}
this.#loadObject(conf, 'env', 'environment')
}
loadCLI () {
for (const s of Object.keys(this.shorthands)) {
if (s.length > 1 && this.argv.includes(`-${s}`)) {
log.warn(`-${s} is not a valid single-hyphen cli flag and will be removed in the future`)
}
}
nopt.invalidHandler = (k, val, type) =>
this.invalidHandler(k, val, type, 'command line options', 'cli')
const conf = nopt(this.types, this.shorthands, this.argv)
nopt.invalidHandler = null
this.parsedArgv = conf.argv
delete conf.argv
this.#loadObject(conf, 'cli', 'command line options')
}
get valid () {
for (const [where, { valid }] of this.data.entries()) {
if (valid === false || valid === null && !this.validate(where)) {
return false
}
}
return true
}
validate (where) {
if (!where) {
let valid = true
const authProblems = []
for (const entryWhere of this.data.keys()) {
// no need to validate our defaults, we know they're fine
// cli was already validated when parsed the first time
if (entryWhere === 'default' || entryWhere === 'builtin' || entryWhere === 'cli') {
continue
}
const ret = this.validate(entryWhere)
valid = valid && ret
if (['global', 'user', 'project'].includes(entryWhere)) {
// after validating everything else, we look for old auth configs we no longer support
// if these keys are found, we build up a list of them and the appropriate action and
// attach it as context on the thrown error
// first, keys that should be removed
for (const key of ['_authtoken', '-authtoken']) {
if (this.get(key, entryWhere)) {
authProblems.push({ action: 'delete', key, where: entryWhere })
}
}
// NOTE we pull registry without restricting to the current 'where' because we want to
// suggest scoping things to the registry they would be applied to, which is the default
// regardless of where it was defined
const nerfedReg = nerfDart(this.get('registry'))
// keys that should be nerfed but currently are not
for (const key of ['_auth', '_authToken', 'username', '_password']) {
if (this.get(key, entryWhere)) {
// username and _password must both exist in the same file to be recognized correctly
if (key === 'username' && !this.get('_password', entryWhere)) {
authProblems.push({ action: 'delete', key, where: entryWhere })
} else if (key === '_password' && !this.get('username', entryWhere)) {
authProblems.push({ action: 'delete', key, where: entryWhere })
} else {
authProblems.push({
action: 'rename',
from: key,
to: `${nerfedReg}:${key}`,
where: entryWhere,
})
}
}
}
}
}
if (authProblems.length) {
const { ErrInvalidAuth } = require('./errors.js')
throw new ErrInvalidAuth(authProblems)
}
return valid
} else {
const obj = this.data.get(where)
obj[_valid] = true
nopt.invalidHandler = (k, val, type) =>
this.invalidHandler(k, val, type, obj.source, where)
nopt.clean(obj.data, this.types, typeDefs)
nopt.invalidHandler = null
return obj[_valid]
}
}
// fixes problems identified by validate(), accepts the 'problems' property from a thrown
// ErrInvalidAuth to avoid having to check everything again
repair (problems) {
if (!problems) {
try {
this.validate()
} catch (err) {
// coverage skipped here because we don't need to test re-throwing an error
// istanbul ignore next
if (err.code !== 'ERR_INVALID_AUTH') {
throw err
}
problems = err.problems
} finally {
if (!problems) {
problems = []
}
}
}
for (const problem of problems) {
// coverage disabled for else branch because it doesn't do anything and shouldn't
// istanbul ignore else
if (problem.action === 'delete') {
this.delete(problem.key, problem.where)
} else if (problem.action === 'rename') {
const raw = this.data.get(problem.where).raw?.[problem.from]
const calculated = this.get(problem.from, problem.where)
this.set(problem.to, raw || calculated, problem.where)
this.delete(problem.from, problem.where)
}
}
}
// Returns true if the value is coming directly from the source defined
// in default definitions, if the current value for the key config is
// coming from any other different source, returns false
isDefault (key) {
const [defaultType, ...types] = [...confTypes]
const defaultData = this.data.get(defaultType).data
return hasOwnProperty(defaultData, key)
&& types.every(type => {
const typeData = this.data.get(type).data
return !hasOwnProperty(typeData, key)
})
}
invalidHandler (k, val, type, source, where) {
const typeDescription = require('./type-description.js')
log.warn(
'invalid config',
k + '=' + JSON.stringify(val),
`set in ${source}`
)
this.data.get(where)[_valid] = false
if (Array.isArray(type)) {
if (type.includes(typeDefs.url.type)) {
type = typeDefs.url.type
} else {
/* istanbul ignore if - no actual configs matching this, but
* path types SHOULD be handled this way, like URLs, for the
* same reason */
if (type.includes(typeDefs.path.type)) {
type = typeDefs.path.type
}
}
}
const typeDesc = typeDescription(type)
const mustBe = typeDesc
.filter(m => m !== undefined && m !== Array)
const msg = 'Must be' + this.#getOneOfKeywords(mustBe, typeDesc)
const desc = mustBe.length === 1 ? mustBe[0]
: [...new Set(mustBe.map(n => typeof n === 'string' ? n : JSON.stringify(n)))].join(', ')
log.warn('invalid config', msg, desc)
}
#getOneOfKeywords (mustBe, typeDesc) {
let keyword
if (mustBe.length === 1 && typeDesc.includes(Array)) {
keyword = ' one or more'
} else if (mustBe.length > 1 && typeDesc.includes(Array)) {
keyword = ' one or more of:'
} else if (mustBe.length > 1) {
keyword = ' one of:'
} else {
keyword = ''
}
return keyword
}
#loadObject (obj, where, source, er = null) {
// obj is the raw data read from the file
const conf = this.data.get(where)
if (conf.source) {
const m = `double-loading "${where}" configs from ${source}, ` +
`previously loaded from ${conf.source}`
throw new Error(m)
}
if (this.sources.has(source)) {
const m = `double-loading config "${source}" as "${where}", ` +
`previously loaded as "${this.sources.get(source)}"`
throw new Error(m)
}
conf.source = source
this.sources.set(source, where)
if (er) {
conf.loadError = er
if (er.code !== 'ENOENT') {
log.verbose('config', `error loading ${where} config`, er)
}
} else {
conf.raw = obj
for (const [key, value] of Object.entries(obj)) {
const k = envReplace(key, this.env)
const v = this.parseField(value, k)
if (where !== 'default') {
this.#checkDeprecated(k)
if (this.definitions[key]?.exclusive) {
for (const exclusive of this.definitions[key].exclusive) {
if (!this.isDefault(exclusive)) {
throw new TypeError(`--${key} can not be provided when using --${exclusive}`)
}
}
}
}
// Some defaults like npm-version are not user-definable and thus don't have definitions
if (where !== 'default') {
this.checkUnknown(where, key)
}
conf.data[k] = v
}
}
}
checkUnknown (where, key) {
if (!this.definitions[key]) {
if (internalEnv.includes(key)) {
return
}
if (!key.includes(':')) {
log.warn(`Unknown ${where} config "${where === 'cli' ? '--' : ''}${key}". This will stop working in the next major version of npm.`)
return
}
const baseKey = key.split(':').pop()
if (!this.definitions[baseKey] && !this.nerfDarts.includes(baseKey)) {
log.warn(`Unknown ${where} config "${baseKey}" (${key}). This will stop working in the next major version of npm.`)
}
}
}
#checkDeprecated (key) {
if (this.deprecated[key]) {
log.warn('config', key, this.deprecated[key])
}
}
// Parse a field, coercing it to the best type available.
parseField (f, key, listElement = false) {
return parseField(f, key, this, listElement)
}
async #loadFile (file, type) {
// only catch the error from readFile, not from the loadObject call
log.silly('config', `load:file:${file}`)
await readFile(file, 'utf8').then(
data => {
const parsedConfig = ini.parse(data)
if (type === 'project' && parsedConfig.prefix) {
// Log error if prefix is mentioned in project .npmrc
/* eslint-disable-next-line max-len */
log.error('config', `prefix cannot be changed from project config: ${file}.`)
}
return this.#loadObject(parsedConfig, type, file)
},
er => this.#loadObject(null, type, file, er)
)
}
loadBuiltinConfig () {
return this.#loadFile(resolve(this.npmPath, 'npmrc'), 'builtin')
}
async loadProjectConfig () {
// the localPrefix can be set by the CLI config, but otherwise is
// found by walking up the folder tree. either way, we load it before
// we return to make sure localPrefix is set
await this.loadLocalPrefix()
// if we have not detected a local package json yet, try now that we
// have a local prefix
if (this.localPackage == null) {
this.localPackage = await fileExists(this.localPrefix, 'package.json')
}
if (this.#get('global') === true || this.#get('location') === 'global') {
this.data.get('project').source = '(global mode enabled, ignored)'
this.sources.set(this.data.get('project').source, 'project')
return
}
const projectFile = resolve(this.localPrefix, '.npmrc')
// if we're in the ~ directory, and there happens to be a node_modules
// folder (which is not TOO uncommon, it turns out), then we can end
// up loading the "project" config where the "userconfig" will be,
// which causes some calamaties. So, we only load project config if
// it doesn't match what the userconfig will be.
if (projectFile !== this.#get('userconfig')) {
return this.#loadFile(projectFile, 'project')
} else {
this.data.get('project').source = '(same as "user" config, ignored)'
this.sources.set(this.data.get('project').source, 'project')
}
}
async loadLocalPrefix () {
const cliPrefix = this.#get('prefix', 'cli')
if (cliPrefix) {
this.localPrefix = cliPrefix
return
}
const cliWorkspaces = this.#get('workspaces', 'cli')
const isGlobal = this.#get('global') || this.#get('location') === 'global'
for (const p of walkUp(this.cwd)) {
// HACK: this is an option set in tests to stop the local prefix from being set
// on tests that are created inside the npm repo
if (this.excludeNpmCwd && p === this.npmPath) {
break
}
const hasPackageJson = await fileExists(p, 'package.json')
if (!this.localPrefix && (hasPackageJson || await dirExists(p, 'node_modules'))) {
this.localPrefix = p
this.localPackage = hasPackageJson
// if workspaces are disabled, or we're in global mode, return now
if (cliWorkspaces === false || isGlobal) {
return
}
// otherwise, continue the loop
continue
}
if (this.localPrefix && hasPackageJson) {
const pkgJson = require('@npmcli/package-json')
// if we already set localPrefix but this dir has a package.json
// then we need to see if `p` is a workspace root by reading its package.json
// however, if reading it fails then we should just move on
const { content: pkg } = await pkgJson.normalize(p).catch(() => ({ content: {} }))
if (!pkg?.workspaces) {
continue
}
const mapWorkspaces = require('@npmcli/map-workspaces')
const workspaces = await mapWorkspaces({ cwd: p, pkg })
for (const w of workspaces.values()) {
if (w === this.localPrefix) {
// see if there's a .npmrc file in the workspace, if so log a warning
if (await fileExists(this.localPrefix, '.npmrc')) {
log.warn('config', `ignoring workspace config at ${this.localPrefix}/.npmrc`)
}
// set the workspace in the default layer, which allows it to be overridden easily
const { data } = this.data.get('default')
data.workspace = [this.localPrefix]
this.localPrefix = p
this.localPackage = hasPackageJson
log.info('config', `found workspace root at ${this.localPrefix}`)
// we found a root, so we return now
return
}
}
}
}
if (!this.localPrefix) {
this.localPrefix = this.cwd
}
}
loadUserConfig () {
return this.#loadFile(this.#get('userconfig'), 'user')
}
loadGlobalConfig () {
return this.#loadFile(this.#get('globalconfig'), 'global')
}
async save (where) {
if (!this.loaded) {
throw new Error('call config.load() before saving')
}
if (!confFileTypes.has(where)) {
throw new Error('invalid config location param: ' + where)
}
const conf = this.data.get(where)
conf[_loadError] = null
if (where === 'user') {
// if email is nerfed, then we want to de-nerf it
const nerfed = nerfDart(this.get('registry'))
const email = this.get(`${nerfed}:email`, 'user')
if (email) {
this.delete(`${nerfed}:email`, 'user')
this.set('email', email, 'user')
}
}
// We need the actual raw data before we called parseField so that we are
// saving the same content back to the file
const iniData = ini.stringify(conf.raw).trim() + '\n'
if (!iniData.trim()) {
// ignore the unlink error (eg, if file doesn't exist)
await unlink(conf.source).catch(() => {})
return
}
const dir = dirname(conf.source)
await mkdir(dir, { recursive: true })
await writeFile(conf.source, iniData, 'utf8')
const mode = where === 'user' ? 0o600 : 0o666
await chmod(conf.source, mode)
}
clearCredentialsByURI (uri, level = 'user') {
const nerfed = nerfDart(uri)
const def = nerfDart(this.get('registry'))
if (def === nerfed) {
this.delete(`-authtoken`, level)
this.delete(`_authToken`, level)
this.delete(`_authtoken`, level)
this.delete(`_auth`, level)
this.delete(`_password`, level)
this.delete(`username`, level)
// de-nerf email if it's nerfed to the default registry
const email = this.get(`${nerfed}:email`, level)
if (email) {
this.set('email', email, level)
}
}
this.delete(`${nerfed}:_authToken`, level)
this.delete(`${nerfed}:_auth`, level)
this.delete(`${nerfed}:_password`, level)
this.delete(`${nerfed}:username`, level)
this.delete(`${nerfed}:email`, level)
this.delete(`${nerfed}:certfile`, level)
this.delete(`${nerfed}:keyfile`, level)
}
setCredentialsByURI (uri, { token, username, password, certfile, keyfile }) {
const nerfed = nerfDart(uri)
// field that hasn't been used as documented for a LONG time,
// and as of npm 7.10.0, isn't used at all. We just always
// send auth if we have it, only to the URIs under the nerf dart.
this.delete(`${nerfed}:always-auth`, 'user')
this.delete(`${nerfed}:email`, 'user')
if (certfile && keyfile) {
this.set(`${nerfed}:certfile`, certfile, 'user')
this.set(`${nerfed}:keyfile`, keyfile, 'user')
// cert/key may be used in conjunction with other credentials, thus no `else`
}
if (token) {
this.set(`${nerfed}:_authToken`, token, 'user')
this.delete(`${nerfed}:_password`, 'user')
this.delete(`${nerfed}:username`, 'user')
} else if (username || password) {
if (!username) {
throw new Error('must include username')
}
if (!password) {
throw new Error('must include password')
}
this.delete(`${nerfed}:_authToken`, 'user')
this.set(`${nerfed}:username`, username, 'user')
// note: not encrypted, no idea why we bothered to do this, but oh well
// protects against shoulder-hacks if password is memorable, I guess?
const encoded = Buffer.from(password, 'utf8').toString('base64')
this.set(`${nerfed}:_password`, encoded, 'user')
} else if (!certfile || !keyfile) {
throw new Error('No credentials to set.')
}
}
// this has to be a bit more complicated to support legacy data of all forms
getCredentialsByURI (uri) {
const nerfed = nerfDart(uri)
const def = nerfDart(this.get('registry'))
const creds = {}
// email is handled differently, it used to always be nerfed and now it never should be
// if it's set nerfed to the default registry, then we copy it to the unnerfed key
// TODO: evaluate removing 'email' from the credentials object returned here
const email = this.get(`${nerfed}:email`) || this.get('email')
if (email) {
if (nerfed === def) {
this.set('email', email, 'user')
}
creds.email = email
}
const certfileReg = this.get(`${nerfed}:certfile`)
const keyfileReg = this.get(`${nerfed}:keyfile`)
if (certfileReg && keyfileReg) {
creds.certfile = certfileReg
creds.keyfile = keyfileReg
// cert/key may be used in conjunction with other credentials, thus no `return`
}
const tokenReg = this.get(`${nerfed}:_authToken`)
if (tokenReg) {
creds.token = tokenReg
return creds
}
const userReg = this.get(`${nerfed}:username`)
const passReg = this.get(`${nerfed}:_password`)
if (userReg && passReg) {
creds.username = userReg
creds.password = Buffer.from(passReg, 'base64').toString('utf8')
const auth = `${creds.username}:${creds.password}`
creds.auth = Buffer.from(auth, 'utf8').toString('base64')
return creds
}
const authReg = this.get(`${nerfed}:_auth`)
if (authReg) {
const authDecode = Buffer.from(authReg, 'base64').toString('utf8')
const authSplit = authDecode.split(':')
creds.username = authSplit.shift()
creds.password = authSplit.join(':')
creds.auth = authReg
return creds
}
// at this point, nothing else is usable so just return what we do have
return creds
}
// set up the environment object we have with npm_config_* environs
// for all configs that are different from their default values, and
// set EDITOR and HOME.
setEnvs () {
setEnvs(this)
}
}
const _loadError = Symbol('loadError')
const _valid = Symbol('valid')
class ConfigData {
#data
#source = null
#raw = null
constructor (parent) {
this.#data = Object.create(parent && parent.data)
this.#raw = {}
this[_valid] = true
}
get data () {
return this.#data
}
get valid () {
return this[_valid]
}
set source (s) {
if (this.#source) {
throw new Error('cannot set ConfigData source more than once')
}
this.#source = s
}
get source () {
return this.#source
}
set loadError (e) {
if (this[_loadError] || (Object.keys(this.#raw).length)) {
throw new Error('cannot set ConfigData loadError after load')
}
this[_loadError] = e
}
get loadError () {
return this[_loadError]
}
set raw (r) {
if (Object.keys(this.#raw).length || this[_loadError]) {
throw new Error('cannot set ConfigData raw after load')
}
this.#raw = r
}
get raw () {
return this.#raw
}
}
module.exports = Config