@npmcli/config 6.2.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,253 @@
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 = ['type', 'description', 'default', 'key']
10
+
11
+ const allowed = [
12
+ 'default',
13
+ 'defaultDescription',
14
+ 'deprecated',
15
+ 'description',
16
+ 'exclusive',
17
+ 'flatten',
18
+ 'hint',
19
+ 'key',
20
+ 'short',
21
+ 'type',
22
+ 'typeDescription',
23
+ 'usage',
24
+ 'envExport',
25
+ ]
26
+
27
+ const {
28
+ semver: { type: semver },
29
+ Umask: { type: Umask },
30
+ url: { type: url },
31
+ path: { type: path },
32
+ } = require('../type-defs.js')
33
+
34
+ class Definition {
35
+ constructor (key, def) {
36
+ this.key = key
37
+ // if it's set falsey, don't export it, otherwise we do by default
38
+ this.envExport = true
39
+ Object.assign(this, def)
40
+ this.validate()
41
+ if (!this.defaultDescription) {
42
+ this.defaultDescription = describeValue(this.default)
43
+ }
44
+ if (!this.typeDescription) {
45
+ this.typeDescription = describeType(this.type)
46
+ }
47
+ // hint is only used for non-boolean values
48
+ if (!this.hint) {
49
+ if (this.type === Number) {
50
+ this.hint = '<number>'
51
+ } else {
52
+ this.hint = `<${this.key}>`
53
+ }
54
+ }
55
+ if (!this.usage) {
56
+ this.usage = describeUsage(this)
57
+ }
58
+ }
59
+
60
+ validate () {
61
+ for (const req of required) {
62
+ if (!Object.prototype.hasOwnProperty.call(this, req)) {
63
+ throw new Error(`config lacks ${req}: ${this.key}`)
64
+ }
65
+ }
66
+ if (!this.key) {
67
+ throw new Error(`config lacks key: ${this.key}`)
68
+ }
69
+ for (const field of Object.keys(this)) {
70
+ if (!allowed.includes(field)) {
71
+ throw new Error(`config defines unknown field ${field}: ${this.key}`)
72
+ }
73
+ }
74
+ }
75
+
76
+ // a textual description of this config, suitable for help output
77
+ describe () {
78
+ const description = unindent(this.description)
79
+ const noEnvExport = this.envExport
80
+ ? ''
81
+ : `
82
+ This value is not exported to the environment for child processes.
83
+ `
84
+ const deprecated = !this.deprecated ? '' : `* DEPRECATED: ${unindent(this.deprecated)}\n`
85
+ /* eslint-disable-next-line max-len */
86
+ const exclusive = !this.exclusive ? '' : `\nThis config can not be used with: \`${this.exclusive.join('`, `')}\``
87
+ return wrapAll(`#### \`${this.key}\`
88
+
89
+ * Default: ${unindent(this.defaultDescription)}
90
+ * Type: ${unindent(this.typeDescription)}
91
+ ${deprecated}
92
+ ${description}
93
+ ${exclusive}
94
+ ${noEnvExport}`)
95
+ }
96
+ }
97
+
98
+ const describeUsage = def => {
99
+ let key = ''
100
+
101
+ // Single type
102
+ if (!Array.isArray(def.type)) {
103
+ if (def.short) {
104
+ key = `-${def.short}|`
105
+ }
106
+
107
+ if (def.type === Boolean && def.default !== false) {
108
+ key = `${key}--no-${def.key}`
109
+ } else {
110
+ key = `${key}--${def.key}`
111
+ }
112
+
113
+ if (def.type !== Boolean) {
114
+ key = `${key} ${def.hint}`
115
+ }
116
+
117
+ return key
118
+ }
119
+
120
+ key = `--${def.key}`
121
+ if (def.short) {
122
+ key = `-${def.short}|--${def.key}`
123
+ }
124
+
125
+ // Multiple types
126
+ let types = def.type
127
+ const multiple = types.includes(Array)
128
+ const bool = types.includes(Boolean)
129
+
130
+ // null type means optional and doesn't currently affect usage output since
131
+ // all non-optional params have defaults so we render everything as optional
132
+ types = types.filter(t => t !== null && t !== Array && t !== Boolean)
133
+
134
+ if (!types.length) {
135
+ return key
136
+ }
137
+
138
+ let description
139
+ if (!types.some(t => typeof t !== 'string')) {
140
+ // Specific values, use specifics given
141
+ description = `<${types.filter(d => d).join('|')}>`
142
+ } else {
143
+ // Generic values, use hint
144
+ description = def.hint
145
+ }
146
+
147
+ if (bool) {
148
+ // Currently none of our multi-type configs with boolean values default to
149
+ // false so all their hints should show `--no-`, if we ever add ones that
150
+ // default to false we can branch the logic here
151
+ key = `--no-${def.key}|${key}`
152
+ }
153
+
154
+ const usage = `${key} ${description}`
155
+ if (multiple) {
156
+ return `${usage} [${usage} ...]`
157
+ } else {
158
+ return usage
159
+ }
160
+ }
161
+
162
+ const describeType = type => {
163
+ if (Array.isArray(type)) {
164
+ const descriptions = type.filter(t => t !== Array).map(t => describeType(t))
165
+
166
+ // [a] => "a"
167
+ // [a, b] => "a or b"
168
+ // [a, b, c] => "a, b, or c"
169
+ // [a, Array] => "a (can be set multiple times)"
170
+ // [a, Array, b] => "a or b (can be set multiple times)"
171
+ const last = descriptions.length > 1 ? [descriptions.pop()] : []
172
+ const oxford = descriptions.length > 1 ? ', or ' : ' or '
173
+ const words = [descriptions.join(', ')].concat(last).join(oxford)
174
+ const multiple = type.includes(Array) ? ' (can be set multiple times)' : ''
175
+ return `${words}${multiple}`
176
+ }
177
+
178
+ // Note: these are not quite the same as the description printed
179
+ // when validation fails. In that case, we want to give the user
180
+ // a bit more information to help them figure out what's wrong.
181
+ switch (type) {
182
+ case String:
183
+ return 'String'
184
+ case Number:
185
+ return 'Number'
186
+ case Umask:
187
+ return 'Octal numeric string in range 0000..0777 (0..511)'
188
+ case Boolean:
189
+ return 'Boolean'
190
+ case Date:
191
+ return 'Date'
192
+ case path:
193
+ return 'Path'
194
+ case semver:
195
+ return 'SemVer string'
196
+ case url:
197
+ return 'URL'
198
+ default:
199
+ return describeValue(type)
200
+ }
201
+ }
202
+
203
+ // if it's a string, quote it. otherwise, just cast to string.
204
+ const describeValue = val => (typeof val === 'string' ? JSON.stringify(val) : String(val))
205
+
206
+ const unindent = s => {
207
+ // get the first \n followed by a bunch of spaces, and pluck off
208
+ // that many spaces from the start of every line.
209
+ const match = s.match(/\n +/)
210
+ return !match ? s.trim() : s.split(match[0]).join('\n').trim()
211
+ }
212
+
213
+ const wrap = s => {
214
+ const cols = Math.min(Math.max(20, process.stdout.columns) || 80, 80) - 5
215
+ return unindent(s)
216
+ .split(/[ \n]+/)
217
+ .reduce((left, right) => {
218
+ const last = left.split('\n').pop()
219
+ const join = last.length && last.length + right.length > cols ? '\n' : ' '
220
+ return left + join + right
221
+ })
222
+ }
223
+
224
+ const wrapAll = s => {
225
+ let inCodeBlock = false
226
+ return s
227
+ .split('\n\n')
228
+ .map(block => {
229
+ if (inCodeBlock || block.startsWith('```')) {
230
+ inCodeBlock = !block.endsWith('```')
231
+ return block
232
+ }
233
+
234
+ if (block.charAt(0) === '*') {
235
+ return (
236
+ '* ' +
237
+ block
238
+ .slice(1)
239
+ .trim()
240
+ .split('\n* ')
241
+ .map(li => {
242
+ return wrap(li).replace(/\n/g, '\n ')
243
+ })
244
+ .join('\n* ')
245
+ )
246
+ } else {
247
+ return wrap(block)
248
+ }
249
+ })
250
+ .join('\n\n')
251
+ }
252
+
253
+ module.exports = Definition