|
| 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