definition.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  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. const required = [
  9. 'type',
  10. 'description',
  11. 'default',
  12. 'key',
  13. ]
  14. const allowed = [
  15. 'default',
  16. 'defaultDescription',
  17. 'deprecated',
  18. 'description',
  19. 'flatten',
  20. 'hint',
  21. 'key',
  22. 'short',
  23. 'type',
  24. 'typeDescription',
  25. 'usage',
  26. 'envExport',
  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. class 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. if (!this.typeDescription)
  46. this.typeDescription = describeType(this.type)
  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. if (!this.usage)
  55. this.usage = describeUsage(this)
  56. }
  57. validate () {
  58. for (const req of required) {
  59. if (!Object.prototype.hasOwnProperty.call(this, req))
  60. throw new Error(`config lacks ${req}: ${this.key}`)
  61. }
  62. if (!this.key)
  63. throw new Error(`config lacks key: ${this.key}`)
  64. for (const field of Object.keys(this)) {
  65. if (!allowed.includes(field))
  66. throw new Error(`config defines unknown field ${field}: ${this.key}`)
  67. }
  68. }
  69. // a textual description of this config, suitable for help output
  70. describe () {
  71. const description = unindent(this.description)
  72. const noEnvExport = this.envExport ? '' : `
  73. This value is not exported to the environment for child processes.
  74. `
  75. const deprecated = !this.deprecated ? ''
  76. : `* DEPRECATED: ${unindent(this.deprecated)}\n`
  77. return wrapAll(`#### \`${this.key}\`
  78. * Default: ${unindent(this.defaultDescription)}
  79. * Type: ${unindent(this.typeDescription)}
  80. ${deprecated}
  81. ${description}
  82. ${noEnvExport}`)
  83. }
  84. }
  85. const describeUsage = (def) => {
  86. let key = ''
  87. // Single type
  88. if (!Array.isArray(def.type)) {
  89. if (def.short)
  90. key = `-${def.short}|`
  91. if (def.type === Boolean && def.default !== false)
  92. key = `${key}--no-${def.key}`
  93. else
  94. key = `${key}--${def.key}`
  95. if (def.type !== Boolean)
  96. key = `${key} ${def.hint}`
  97. return key
  98. }
  99. key = `--${def.key}`
  100. if (def.short)
  101. key = `-${def.short}|--${def.key}`
  102. // Multiple types
  103. let types = def.type
  104. const multiple = types.includes(Array)
  105. const bool = types.includes(Boolean)
  106. // null type means optional and doesn't currently affect usage output since
  107. // all non-optional params have defaults so we render everything as optional
  108. types = types.filter(t => t !== null && t !== Array && t !== Boolean)
  109. if (!types.length)
  110. return key
  111. let description
  112. if (!types.some(t => typeof t !== 'string'))
  113. // Specific values, use specifics given
  114. description = `<${types.filter(d => d).join('|')}>`
  115. else {
  116. // Generic values, use hint
  117. description = def.hint
  118. }
  119. if (bool) {
  120. // Currently none of our multi-type configs with boolean values default to
  121. // false so all their hints should show `--no-`, if we ever add ones that
  122. // default to false we can branch the logic here
  123. key = `--no-${def.key}|${key}`
  124. }
  125. const usage = `${key} ${description}`
  126. if (multiple)
  127. return `${usage} [${usage} ...]`
  128. else
  129. return usage
  130. }
  131. const describeType = type => {
  132. if (Array.isArray(type)) {
  133. const descriptions = type
  134. .filter(t => t !== Array)
  135. .map(t => describeType(t))
  136. // [a] => "a"
  137. // [a, b] => "a or b"
  138. // [a, b, c] => "a, b, or c"
  139. // [a, Array] => "a (can be set multiple times)"
  140. // [a, Array, b] => "a or b (can be set multiple times)"
  141. const last = descriptions.length > 1 ? [descriptions.pop()] : []
  142. const oxford = descriptions.length > 1 ? ', or ' : ' or '
  143. const words = [descriptions.join(', ')].concat(last).join(oxford)
  144. const multiple = type.includes(Array) ? ' (can be set multiple times)'
  145. : ''
  146. return `${words}${multiple}`
  147. }
  148. // Note: these are not quite the same as the description printed
  149. // when validation fails. In that case, we want to give the user
  150. // a bit more information to help them figure out what's wrong.
  151. switch (type) {
  152. case String:
  153. return 'String'
  154. case Number:
  155. return 'Number'
  156. case Umask:
  157. return 'Octal numeric string in range 0000..0777 (0..511)'
  158. case Boolean:
  159. return 'Boolean'
  160. case Date:
  161. return 'Date'
  162. case path:
  163. return 'Path'
  164. case semver:
  165. return 'SemVer string'
  166. case url:
  167. return 'URL'
  168. default:
  169. return describeValue(type)
  170. }
  171. }
  172. // if it's a string, quote it. otherwise, just cast to string.
  173. const describeValue = val =>
  174. typeof val === 'string' ? JSON.stringify(val) : String(val)
  175. const unindent = s => {
  176. // get the first \n followed by a bunch of spaces, and pluck off
  177. // that many spaces from the start of every line.
  178. const match = s.match(/\n +/)
  179. return !match ? s.trim() : s.split(match[0]).join('\n').trim()
  180. }
  181. const wrap = (s) => {
  182. const cols = Math.min(Math.max(20, process.stdout.columns) || 80, 80) - 5
  183. return unindent(s).split(/[ \n]+/).reduce((left, right) => {
  184. const last = left.split('\n').pop()
  185. const join = last.length && last.length + right.length > cols ? '\n' : ' '
  186. return left + join + right
  187. })
  188. }
  189. const wrapAll = s => {
  190. let inCodeBlock = false
  191. return s.split('\n\n').map(block => {
  192. if (inCodeBlock || block.startsWith('```')) {
  193. inCodeBlock = !block.endsWith('```')
  194. return block
  195. }
  196. if (block.charAt(0) === '*') {
  197. return '* ' + block.substr(1).trim().split('\n* ').map(li => {
  198. return wrap(li).replace(/\n/g, '\n ')
  199. }).join('\n* ')
  200. } else
  201. return wrap(block)
  202. }).join('\n\n')
  203. }
  204. module.exports = Definition