// class that describes a config key we know about // this keeps us from defining a config key and not // providing a default, description, etc. // // TODO: some kind of categorization system, so we can // say "these are for registry access", "these are for // version resolution" etc. const required = ['type', 'description', 'default', 'key'] const allowed = [ 'default', 'defaultDescription', 'deprecated', 'description', 'flatten', 'hint', 'key', 'short', 'type', 'typeDescription', 'usage', 'envExport', ] const { typeDefs: { semver: { type: semver }, Umask: { type: Umask }, url: { type: url }, path: { type: path }, }, } = require('@npmcli/config') class Definition { constructor (key, def) { this.key = key // if it's set falsey, don't export it, otherwise we do by default this.envExport = true Object.assign(this, def) this.validate() if (!this.defaultDescription) { this.defaultDescription = describeValue(this.default) } if (!this.typeDescription) { this.typeDescription = describeType(this.type) } // hint is only used for non-boolean values if (!this.hint) { if (this.type === Number) { this.hint = '' } else { this.hint = `<${this.key}>` } } if (!this.usage) { this.usage = describeUsage(this) } } validate () { for (const req of required) { if (!Object.prototype.hasOwnProperty.call(this, req)) { throw new Error(`config lacks ${req}: ${this.key}`) } } if (!this.key) { throw new Error(`config lacks key: ${this.key}`) } for (const field of Object.keys(this)) { if (!allowed.includes(field)) { throw new Error(`config defines unknown field ${field}: ${this.key}`) } } } // a textual description of this config, suitable for help output describe () { const description = unindent(this.description) const noEnvExport = this.envExport ? '' : ` This value is not exported to the environment for child processes. ` const deprecated = !this.deprecated ? '' : `* DEPRECATED: ${unindent(this.deprecated)}\n` return wrapAll(`#### \`${this.key}\` * Default: ${unindent(this.defaultDescription)} * Type: ${unindent(this.typeDescription)} ${deprecated} ${description} ${noEnvExport}`) } } const describeUsage = def => { let key = '' // Single type if (!Array.isArray(def.type)) { if (def.short) { key = `-${def.short}|` } if (def.type === Boolean && def.default !== false) { key = `${key}--no-${def.key}` } else { key = `${key}--${def.key}` } if (def.type !== Boolean) { key = `${key} ${def.hint}` } return key } key = `--${def.key}` if (def.short) { key = `-${def.short}|--${def.key}` } // Multiple types let types = def.type const multiple = types.includes(Array) const bool = types.includes(Boolean) // null type means optional and doesn't currently affect usage output since // all non-optional params have defaults so we render everything as optional types = types.filter(t => t !== null && t !== Array && t !== Boolean) if (!types.length) { return key } let description if (!types.some(t => typeof t !== 'string')) { // Specific values, use specifics given description = `<${types.filter(d => d).join('|')}>` } else { // Generic values, use hint description = def.hint } if (bool) { // Currently none of our multi-type configs with boolean values default to // false so all their hints should show `--no-`, if we ever add ones that // default to false we can branch the logic here key = `--no-${def.key}|${key}` } const usage = `${key} ${description}` if (multiple) { return `${usage} [${usage} ...]` } else { return usage } } const describeType = type => { if (Array.isArray(type)) { const descriptions = type.filter(t => t !== Array).map(t => describeType(t)) // [a] => "a" // [a, b] => "a or b" // [a, b, c] => "a, b, or c" // [a, Array] => "a (can be set multiple times)" // [a, Array, b] => "a or b (can be set multiple times)" const last = descriptions.length > 1 ? [descriptions.pop()] : [] const oxford = descriptions.length > 1 ? ', or ' : ' or ' const words = [descriptions.join(', ')].concat(last).join(oxford) const multiple = type.includes(Array) ? ' (can be set multiple times)' : '' return `${words}${multiple}` } // Note: these are not quite the same as the description printed // when validation fails. In that case, we want to give the user // a bit more information to help them figure out what's wrong. switch (type) { case String: return 'String' case Number: return 'Number' case Umask: return 'Octal numeric string in range 0000..0777 (0..511)' case Boolean: return 'Boolean' case Date: return 'Date' case path: return 'Path' case semver: return 'SemVer string' case url: return 'URL' default: return describeValue(type) } } // if it's a string, quote it. otherwise, just cast to string. const describeValue = val => (typeof val === 'string' ? JSON.stringify(val) : String(val)) const unindent = s => { // get the first \n followed by a bunch of spaces, and pluck off // that many spaces from the start of every line. const match = s.match(/\n +/) return !match ? s.trim() : s.split(match[0]).join('\n').trim() } const wrap = s => { const cols = Math.min(Math.max(20, process.stdout.columns) || 80, 80) - 5 return unindent(s) .split(/[ \n]+/) .reduce((left, right) => { const last = left.split('\n').pop() const join = last.length && last.length + right.length > cols ? '\n' : ' ' return left + join + right }) } const wrapAll = s => { let inCodeBlock = false return s .split('\n\n') .map(block => { if (inCodeBlock || block.startsWith('```')) { inCodeBlock = !block.endsWith('```') return block } if (block.charAt(0) === '*') { return ( '* ' + block .slice(1) .trim() .split('\n* ') .map(li => { return wrap(li).replace(/\n/g, '\n ') }) .join('\n* ') ) } else { return wrap(block) } }) .join('\n\n') } module.exports = Definition