Skip to content

Commit 6598bfe

Browse files
committed
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.
1 parent 8cce428 commit 6598bfe

14 files changed

+5075
-0
lines changed

lib/utils/config/definition.js

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// class that describes a config key we know about
2+
// this keeps us from defining a config key and not
3+
// providing a default, description, etc.
4+
//
5+
// TODO: some kind of categorization system, so we can
6+
// say "these are for registry access", "these are for
7+
// version resolution" etc.
8+
9+
const required = [
10+
'type',
11+
'description',
12+
'default',
13+
'key',
14+
]
15+
16+
const allowed = [
17+
'default',
18+
'type',
19+
'description',
20+
'flatten',
21+
'short',
22+
'typeDescription',
23+
'defaultDescription',
24+
'deprecated',
25+
'key',
26+
]
27+
28+
const {
29+
typeDefs: {
30+
semver: { type: semver },
31+
Umask: { type: Umask },
32+
url: { type: url },
33+
path: { type: path },
34+
},
35+
} = require('@npmcli/config')
36+
37+
class Definition {
38+
constructor (key, def) {
39+
this.key = key
40+
Object.assign(this, def)
41+
this.validate()
42+
if (!this.defaultDescription)
43+
this.defaultDescription = describeValue(this.default)
44+
if (!this.typeDescription)
45+
this.typeDescription = describeType(this.type)
46+
}
47+
48+
validate () {
49+
for (const req of required) {
50+
if (!Object.prototype.hasOwnProperty.call(this, req))
51+
throw new Error(`config lacks ${req}: ${this.key}`)
52+
}
53+
if (!this.key)
54+
throw new Error(`config lacks key: ${this.key}`)
55+
for (const field of Object.keys(this)) {
56+
if (!allowed.includes(field))
57+
throw new Error(`config defines unknown field ${field}: ${this.key}`)
58+
}
59+
}
60+
61+
// a textual description of this config, suitable for help output
62+
describe () {
63+
const description = unindent(this.description)
64+
const deprecated = !this.deprecated ? ''
65+
: `* DEPRECATED: ${unindent(this.deprecated)}\n`
66+
return wrapAll(`#### \`${this.key}\`
67+
68+
* Default: ${unindent(this.defaultDescription)}
69+
* Type: ${unindent(this.typeDescription)}
70+
${deprecated}
71+
${description}
72+
`)
73+
}
74+
}
75+
76+
const describeType = type => {
77+
if (Array.isArray(type)) {
78+
const descriptions = type
79+
.filter(t => t !== Array)
80+
.map(t => describeType(t))
81+
82+
// [a] => "a"
83+
// [a, b] => "a or b"
84+
// [a, b, c] => "a, b, or c"
85+
// [a, Array] => "a (can be set multiple times)"
86+
// [a, Array, b] => "a or b (can be set multiple times)"
87+
const last = descriptions.length > 1 ? [descriptions.pop()] : []
88+
const oxford = descriptions.length > 1 ? ', or ' : ' or '
89+
const words = [descriptions.join(', ')].concat(last).join(oxford)
90+
const multiple = type.includes(Array) ? ' (can be set multiple times)'
91+
: ''
92+
return `${words}${multiple}`
93+
}
94+
95+
// Note: these are not quite the same as the description printed
96+
// when validation fails. In that case, we want to give the user
97+
// a bit more information to help them figure out what's wrong.
98+
switch (type) {
99+
case String:
100+
return 'String'
101+
case Number:
102+
return 'Number'
103+
case Umask:
104+
return 'Octal numeric string in range 0000..0777 (0..511)'
105+
case Boolean:
106+
return 'Boolean'
107+
case Date:
108+
return 'Date'
109+
case path:
110+
return 'Path'
111+
case semver:
112+
return 'SemVer string'
113+
case url:
114+
return 'URL'
115+
default:
116+
return describeValue(type)
117+
}
118+
}
119+
120+
// if it's a string, quote it. otherwise, just cast to string.
121+
const describeValue = val =>
122+
typeof val === 'string' ? JSON.stringify(val) : String(val)
123+
124+
const unindent = s => {
125+
// get the first \n followed by a bunch of spaces, and pluck off
126+
// that many spaces from the start of every line.
127+
const match = s.match(/\n +/)
128+
return !match ? s.trim() : s.split(match[0]).join('\n').trim()
129+
}
130+
131+
const wrap = (s) => {
132+
const cols = Math.min(Math.max(20, process.stdout.columns) || 80, 80) - 5
133+
return unindent(s).split(/[ \n]+/).reduce((left, right) => {
134+
const last = left.split('\n').pop()
135+
const join = last.length && last.length + right.length > cols ? '\n' : ' '
136+
return left + join + right
137+
})
138+
}
139+
140+
const wrapAll = s => {
141+
let inCodeBlock = false
142+
return s.split('\n\n').map(block => {
143+
if (inCodeBlock || block.startsWith('```')) {
144+
inCodeBlock = !block.endsWith('```')
145+
return block
146+
}
147+
148+
if (block.charAt(0) === '*') {
149+
return '* ' + block.substr(1).trim().split('\n* ').map(li => {
150+
return wrap(li).replace(/\n/g, '\n ')
151+
}).join('\n* ')
152+
} else
153+
return wrap(block)
154+
}).join('\n\n')
155+
}
156+
157+
module.exports = Definition

0 commit comments

Comments
 (0)