Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
New consolidated config definitions
This replaces the multiple separate sets of objects and documentation,
some of which had defaults and/or types, some of which didn't, and cleans
up a lot of configs that are no longer used.

Deprecated configs are now marked, and the approach used to create these
config definitions ensures that it is impossible to create a new config
option that lacks the appropriate data for it.
  • Loading branch information
isaacs committed Mar 18, 2021
1 parent 8cce428 commit 6598bfe
Show file tree
Hide file tree
Showing 14 changed files with 5,075 additions and 0 deletions.
157 changes: 157 additions & 0 deletions lib/utils/config/definition.js
@@ -0,0 +1,157 @@
// 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',
'type',
'description',
'flatten',
'short',
'typeDescription',
'defaultDescription',
'deprecated',
'key',
]

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
Object.assign(this, def)
this.validate()
if (!this.defaultDescription)
this.defaultDescription = describeValue(this.default)
if (!this.typeDescription)
this.typeDescription = describeType(this.type)
}

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 deprecated = !this.deprecated ? ''
: `* DEPRECATED: ${unindent(this.deprecated)}\n`
return wrapAll(`#### \`${this.key}\`
* Default: ${unindent(this.defaultDescription)}
* Type: ${unindent(this.typeDescription)}
${deprecated}
${description}
`)
}
}

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

0 comments on commit 6598bfe

Please sign in to comment.