123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232 |
- // 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 = '<number>'
- 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.substr(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
|