Team Ai
Datasetpublic

codekingpro/portable-devtools

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