Added test to ensure that release notes are parsed properly Updated to latest dependencies to fix security vulnerability Updated GH Actions Workflow to run test before updating tag Updated README.md to reflect proper whatsnew documentation.
541 lines
15 KiB
JavaScript
541 lines
15 KiB
JavaScript
'use strict'
|
|
// this file handles outputting usage instructions,
|
|
// failures, etc. keeps logging in one place.
|
|
const decamelize = require('./decamelize')
|
|
const stringWidth = require('string-width')
|
|
const objFilter = require('./obj-filter')
|
|
const path = require('path')
|
|
const setBlocking = require('set-blocking')
|
|
const YError = require('./yerror')
|
|
|
|
module.exports = function usage (yargs, y18n) {
|
|
const __ = y18n.__
|
|
const self = {}
|
|
|
|
// methods for ouputting/building failure message.
|
|
const fails = []
|
|
self.failFn = function failFn (f) {
|
|
fails.push(f)
|
|
}
|
|
|
|
let failMessage = null
|
|
let showHelpOnFail = true
|
|
self.showHelpOnFail = function showHelpOnFailFn (enabled, message) {
|
|
if (typeof enabled === 'string') {
|
|
message = enabled
|
|
enabled = true
|
|
} else if (typeof enabled === 'undefined') {
|
|
enabled = true
|
|
}
|
|
failMessage = message
|
|
showHelpOnFail = enabled
|
|
return self
|
|
}
|
|
|
|
let failureOutput = false
|
|
self.fail = function fail (msg, err) {
|
|
const logger = yargs._getLoggerInstance()
|
|
|
|
if (fails.length) {
|
|
for (let i = fails.length - 1; i >= 0; --i) {
|
|
fails[i](msg, err, self)
|
|
}
|
|
} else {
|
|
if (yargs.getExitProcess()) setBlocking(true)
|
|
|
|
// don't output failure message more than once
|
|
if (!failureOutput) {
|
|
failureOutput = true
|
|
if (showHelpOnFail) {
|
|
yargs.showHelp('error')
|
|
logger.error()
|
|
}
|
|
if (msg || err) logger.error(msg || err)
|
|
if (failMessage) {
|
|
if (msg || err) logger.error('')
|
|
logger.error(failMessage)
|
|
}
|
|
}
|
|
|
|
err = err || new YError(msg)
|
|
if (yargs.getExitProcess()) {
|
|
return yargs.exit(1)
|
|
} else if (yargs._hasParseCallback()) {
|
|
return yargs.exit(1, err)
|
|
} else {
|
|
throw err
|
|
}
|
|
}
|
|
}
|
|
|
|
// methods for ouputting/building help (usage) message.
|
|
let usages = []
|
|
let usageDisabled = false
|
|
self.usage = (msg, description) => {
|
|
if (msg === null) {
|
|
usageDisabled = true
|
|
usages = []
|
|
return
|
|
}
|
|
usageDisabled = false
|
|
usages.push([msg, description || ''])
|
|
return self
|
|
}
|
|
self.getUsage = () => {
|
|
return usages
|
|
}
|
|
self.getUsageDisabled = () => {
|
|
return usageDisabled
|
|
}
|
|
|
|
self.getPositionalGroupName = () => {
|
|
return __('Positionals:')
|
|
}
|
|
|
|
let examples = []
|
|
self.example = (cmd, description) => {
|
|
examples.push([cmd, description || ''])
|
|
}
|
|
|
|
let commands = []
|
|
self.command = function command (cmd, description, isDefault, aliases) {
|
|
// the last default wins, so cancel out any previously set default
|
|
if (isDefault) {
|
|
commands = commands.map((cmdArray) => {
|
|
cmdArray[2] = false
|
|
return cmdArray
|
|
})
|
|
}
|
|
commands.push([cmd, description || '', isDefault, aliases])
|
|
}
|
|
self.getCommands = () => commands
|
|
|
|
let descriptions = {}
|
|
self.describe = function describe (key, desc) {
|
|
if (typeof key === 'object') {
|
|
Object.keys(key).forEach((k) => {
|
|
self.describe(k, key[k])
|
|
})
|
|
} else {
|
|
descriptions[key] = desc
|
|
}
|
|
}
|
|
self.getDescriptions = () => descriptions
|
|
|
|
let epilog
|
|
self.epilog = (msg) => {
|
|
epilog = msg
|
|
}
|
|
|
|
let wrapSet = false
|
|
let wrap
|
|
self.wrap = (cols) => {
|
|
wrapSet = true
|
|
wrap = cols
|
|
}
|
|
|
|
function getWrap () {
|
|
if (!wrapSet) {
|
|
wrap = windowWidth()
|
|
wrapSet = true
|
|
}
|
|
|
|
return wrap
|
|
}
|
|
|
|
const deferY18nLookupPrefix = '__yargsString__:'
|
|
self.deferY18nLookup = str => deferY18nLookupPrefix + str
|
|
|
|
const defaultGroup = 'Options:'
|
|
self.help = function help () {
|
|
normalizeAliases()
|
|
|
|
// handle old demanded API
|
|
const base$0 = path.basename(yargs.$0)
|
|
const demandedOptions = yargs.getDemandedOptions()
|
|
const demandedCommands = yargs.getDemandedCommands()
|
|
const groups = yargs.getGroups()
|
|
const options = yargs.getOptions()
|
|
|
|
let keys = []
|
|
keys = keys.concat(Object.keys(descriptions))
|
|
keys = keys.concat(Object.keys(demandedOptions))
|
|
keys = keys.concat(Object.keys(demandedCommands))
|
|
keys = keys.concat(Object.keys(options.default))
|
|
keys = keys.filter(filterHiddenOptions)
|
|
keys = Object.keys(keys.reduce((acc, key) => {
|
|
if (key !== '_') acc[key] = true
|
|
return acc
|
|
}, {}))
|
|
|
|
const theWrap = getWrap()
|
|
const ui = require('cliui')({
|
|
width: theWrap,
|
|
wrap: !!theWrap
|
|
})
|
|
|
|
// the usage string.
|
|
if (!usageDisabled) {
|
|
if (usages.length) {
|
|
// user-defined usage.
|
|
usages.forEach((usage) => {
|
|
ui.div(`${usage[0].replace(/\$0/g, base$0)}`)
|
|
if (usage[1]) {
|
|
ui.div({ text: `${usage[1]}`, padding: [1, 0, 0, 0] })
|
|
}
|
|
})
|
|
ui.div()
|
|
} else if (commands.length) {
|
|
let u = null
|
|
// demonstrate how commands are used.
|
|
if (demandedCommands._) {
|
|
u = `${base$0} <${__('command')}>\n`
|
|
} else {
|
|
u = `${base$0} [${__('command')}]\n`
|
|
}
|
|
ui.div(`${u}`)
|
|
}
|
|
}
|
|
|
|
// your application's commands, i.e., non-option
|
|
// arguments populated in '_'.
|
|
if (commands.length) {
|
|
ui.div(__('Commands:'))
|
|
|
|
const context = yargs.getContext()
|
|
const parentCommands = context.commands.length ? `${context.commands.join(' ')} ` : ''
|
|
|
|
if (yargs.getParserConfiguration()['sort-commands'] === true) {
|
|
commands = commands.sort((a, b) => a[0].localeCompare(b[0]))
|
|
}
|
|
|
|
commands.forEach((command) => {
|
|
const commandString = `${base$0} ${parentCommands}${command[0].replace(/^\$0 ?/, '')}` // drop $0 from default commands.
|
|
ui.span(
|
|
{
|
|
text: commandString,
|
|
padding: [0, 2, 0, 2],
|
|
width: maxWidth(commands, theWrap, `${base$0}${parentCommands}`) + 4
|
|
},
|
|
{ text: command[1] }
|
|
)
|
|
const hints = []
|
|
if (command[2]) hints.push(`[${__('default:').slice(0, -1)}]`) // TODO hacking around i18n here
|
|
if (command[3] && command[3].length) {
|
|
hints.push(`[${__('aliases:')} ${command[3].join(', ')}]`)
|
|
}
|
|
if (hints.length) {
|
|
ui.div({ text: hints.join(' '), padding: [0, 0, 0, 2], align: 'right' })
|
|
} else {
|
|
ui.div()
|
|
}
|
|
})
|
|
|
|
ui.div()
|
|
}
|
|
|
|
// perform some cleanup on the keys array, making it
|
|
// only include top-level keys not their aliases.
|
|
const aliasKeys = (Object.keys(options.alias) || [])
|
|
.concat(Object.keys(yargs.parsed.newAliases) || [])
|
|
|
|
keys = keys.filter(key => !yargs.parsed.newAliases[key] && aliasKeys.every(alias => (options.alias[alias] || []).indexOf(key) === -1))
|
|
|
|
// populate 'Options:' group with any keys that have not
|
|
// explicitly had a group set.
|
|
if (!groups[defaultGroup]) groups[defaultGroup] = []
|
|
addUngroupedKeys(keys, options.alias, groups)
|
|
|
|
// display 'Options:' table along with any custom tables:
|
|
Object.keys(groups).forEach((groupName) => {
|
|
if (!groups[groupName].length) return
|
|
|
|
// if we've grouped the key 'f', but 'f' aliases 'foobar',
|
|
// normalizedKeys should contain only 'foobar'.
|
|
const normalizedKeys = groups[groupName].filter(filterHiddenOptions).map((key) => {
|
|
if (~aliasKeys.indexOf(key)) return key
|
|
for (let i = 0, aliasKey; (aliasKey = aliasKeys[i]) !== undefined; i++) {
|
|
if (~(options.alias[aliasKey] || []).indexOf(key)) return aliasKey
|
|
}
|
|
return key
|
|
})
|
|
|
|
if (normalizedKeys.length < 1) return
|
|
|
|
ui.div(__(groupName))
|
|
|
|
// actually generate the switches string --foo, -f, --bar.
|
|
const switches = normalizedKeys.reduce((acc, key) => {
|
|
acc[key] = [ key ].concat(options.alias[key] || [])
|
|
.map(sw => {
|
|
// for the special positional group don't
|
|
// add '--' or '-' prefix.
|
|
if (groupName === self.getPositionalGroupName()) return sw
|
|
else return (sw.length > 1 ? '--' : '-') + sw
|
|
})
|
|
.join(', ')
|
|
|
|
return acc
|
|
}, {})
|
|
|
|
normalizedKeys.forEach((key) => {
|
|
const kswitch = switches[key]
|
|
let desc = descriptions[key] || ''
|
|
let type = null
|
|
|
|
if (~desc.lastIndexOf(deferY18nLookupPrefix)) desc = __(desc.substring(deferY18nLookupPrefix.length))
|
|
|
|
if (~options.boolean.indexOf(key)) type = `[${__('boolean')}]`
|
|
if (~options.count.indexOf(key)) type = `[${__('count')}]`
|
|
if (~options.string.indexOf(key)) type = `[${__('string')}]`
|
|
if (~options.normalize.indexOf(key)) type = `[${__('string')}]`
|
|
if (~options.array.indexOf(key)) type = `[${__('array')}]`
|
|
if (~options.number.indexOf(key)) type = `[${__('number')}]`
|
|
|
|
const extra = [
|
|
type,
|
|
(key in demandedOptions) ? `[${__('required')}]` : null,
|
|
options.choices && options.choices[key] ? `[${__('choices:')} ${
|
|
self.stringifiedValues(options.choices[key])}]` : null,
|
|
defaultString(options.default[key], options.defaultDescription[key])
|
|
].filter(Boolean).join(' ')
|
|
|
|
ui.span(
|
|
{ text: kswitch, padding: [0, 2, 0, 2], width: maxWidth(switches, theWrap) + 4 },
|
|
desc
|
|
)
|
|
|
|
if (extra) ui.div({ text: extra, padding: [0, 0, 0, 2], align: 'right' })
|
|
else ui.div()
|
|
})
|
|
|
|
ui.div()
|
|
})
|
|
|
|
// describe some common use-cases for your application.
|
|
if (examples.length) {
|
|
ui.div(__('Examples:'))
|
|
|
|
examples.forEach((example) => {
|
|
example[0] = example[0].replace(/\$0/g, base$0)
|
|
})
|
|
|
|
examples.forEach((example) => {
|
|
if (example[1] === '') {
|
|
ui.div(
|
|
{
|
|
text: example[0],
|
|
padding: [0, 2, 0, 2]
|
|
}
|
|
)
|
|
} else {
|
|
ui.div(
|
|
{
|
|
text: example[0],
|
|
padding: [0, 2, 0, 2],
|
|
width: maxWidth(examples, theWrap) + 4
|
|
}, {
|
|
text: example[1]
|
|
}
|
|
)
|
|
}
|
|
})
|
|
|
|
ui.div()
|
|
}
|
|
|
|
// the usage string.
|
|
if (epilog) {
|
|
const e = epilog.replace(/\$0/g, base$0)
|
|
ui.div(`${e}\n`)
|
|
}
|
|
|
|
// Remove the trailing white spaces
|
|
return ui.toString().replace(/\s*$/, '')
|
|
}
|
|
|
|
// return the maximum width of a string
|
|
// in the left-hand column of a table.
|
|
function maxWidth (table, theWrap, modifier) {
|
|
let width = 0
|
|
|
|
// table might be of the form [leftColumn],
|
|
// or {key: leftColumn}
|
|
if (!Array.isArray(table)) {
|
|
table = Object.keys(table).map(key => [table[key]])
|
|
}
|
|
|
|
table.forEach((v) => {
|
|
width = Math.max(
|
|
stringWidth(modifier ? `${modifier} ${v[0]}` : v[0]),
|
|
width
|
|
)
|
|
})
|
|
|
|
// if we've enabled 'wrap' we should limit
|
|
// the max-width of the left-column.
|
|
if (theWrap) width = Math.min(width, parseInt(theWrap * 0.5, 10))
|
|
|
|
return width
|
|
}
|
|
|
|
// make sure any options set for aliases,
|
|
// are copied to the keys being aliased.
|
|
function normalizeAliases () {
|
|
// handle old demanded API
|
|
const demandedOptions = yargs.getDemandedOptions()
|
|
const options = yargs.getOptions()
|
|
|
|
;(Object.keys(options.alias) || []).forEach((key) => {
|
|
options.alias[key].forEach((alias) => {
|
|
// copy descriptions.
|
|
if (descriptions[alias]) self.describe(key, descriptions[alias])
|
|
// copy demanded.
|
|
if (alias in demandedOptions) yargs.demandOption(key, demandedOptions[alias])
|
|
// type messages.
|
|
if (~options.boolean.indexOf(alias)) yargs.boolean(key)
|
|
if (~options.count.indexOf(alias)) yargs.count(key)
|
|
if (~options.string.indexOf(alias)) yargs.string(key)
|
|
if (~options.normalize.indexOf(alias)) yargs.normalize(key)
|
|
if (~options.array.indexOf(alias)) yargs.array(key)
|
|
if (~options.number.indexOf(alias)) yargs.number(key)
|
|
})
|
|
})
|
|
}
|
|
|
|
// given a set of keys, place any keys that are
|
|
// ungrouped under the 'Options:' grouping.
|
|
function addUngroupedKeys (keys, aliases, groups) {
|
|
let groupedKeys = []
|
|
let toCheck = null
|
|
Object.keys(groups).forEach((group) => {
|
|
groupedKeys = groupedKeys.concat(groups[group])
|
|
})
|
|
|
|
keys.forEach((key) => {
|
|
toCheck = [key].concat(aliases[key])
|
|
if (!toCheck.some(k => groupedKeys.indexOf(k) !== -1)) {
|
|
groups[defaultGroup].push(key)
|
|
}
|
|
})
|
|
return groupedKeys
|
|
}
|
|
|
|
function filterHiddenOptions (key) {
|
|
return yargs.getOptions().hiddenOptions.indexOf(key) < 0 || yargs.parsed.argv[yargs.getOptions().showHiddenOpt]
|
|
}
|
|
|
|
self.showHelp = (level) => {
|
|
const logger = yargs._getLoggerInstance()
|
|
if (!level) level = 'error'
|
|
const emit = typeof level === 'function' ? level : logger[level]
|
|
emit(self.help())
|
|
}
|
|
|
|
self.functionDescription = (fn) => {
|
|
const description = fn.name ? decamelize(fn.name, '-') : __('generated-value')
|
|
return ['(', description, ')'].join('')
|
|
}
|
|
|
|
self.stringifiedValues = function stringifiedValues (values, separator) {
|
|
let string = ''
|
|
const sep = separator || ', '
|
|
const array = [].concat(values)
|
|
|
|
if (!values || !array.length) return string
|
|
|
|
array.forEach((value) => {
|
|
if (string.length) string += sep
|
|
string += JSON.stringify(value)
|
|
})
|
|
|
|
return string
|
|
}
|
|
|
|
// format the default-value-string displayed in
|
|
// the right-hand column.
|
|
function defaultString (value, defaultDescription) {
|
|
let string = `[${__('default:')} `
|
|
|
|
if (value === undefined && !defaultDescription) return null
|
|
|
|
if (defaultDescription) {
|
|
string += defaultDescription
|
|
} else {
|
|
switch (typeof value) {
|
|
case 'string':
|
|
string += `"${value}"`
|
|
break
|
|
case 'object':
|
|
string += JSON.stringify(value)
|
|
break
|
|
default:
|
|
string += value
|
|
}
|
|
}
|
|
|
|
return `${string}]`
|
|
}
|
|
|
|
// guess the width of the console window, max-width 80.
|
|
function windowWidth () {
|
|
const maxWidth = 80
|
|
if (typeof process === 'object' && process.stdout && process.stdout.columns) {
|
|
return Math.min(maxWidth, process.stdout.columns)
|
|
} else {
|
|
return maxWidth
|
|
}
|
|
}
|
|
|
|
// logic for displaying application version.
|
|
let version = null
|
|
self.version = (ver) => {
|
|
version = ver
|
|
}
|
|
|
|
self.showVersion = () => {
|
|
const logger = yargs._getLoggerInstance()
|
|
logger.log(version)
|
|
}
|
|
|
|
self.reset = function reset (localLookup) {
|
|
// do not reset wrap here
|
|
// do not reset fails here
|
|
failMessage = null
|
|
failureOutput = false
|
|
usages = []
|
|
usageDisabled = false
|
|
epilog = undefined
|
|
examples = []
|
|
commands = []
|
|
descriptions = objFilter(descriptions, (k, v) => !localLookup[k])
|
|
return self
|
|
}
|
|
|
|
let frozen
|
|
self.freeze = function freeze () {
|
|
frozen = {}
|
|
frozen.failMessage = failMessage
|
|
frozen.failureOutput = failureOutput
|
|
frozen.usages = usages
|
|
frozen.usageDisabled = usageDisabled
|
|
frozen.epilog = epilog
|
|
frozen.examples = examples
|
|
frozen.commands = commands
|
|
frozen.descriptions = descriptions
|
|
}
|
|
self.unfreeze = function unfreeze () {
|
|
failMessage = frozen.failMessage
|
|
failureOutput = frozen.failureOutput
|
|
usages = frozen.usages
|
|
usageDisabled = frozen.usageDisabled
|
|
epilog = frozen.epilog
|
|
examples = frozen.examples
|
|
commands = frozen.commands
|
|
descriptions = frozen.descriptions
|
|
frozen = undefined
|
|
}
|
|
|
|
return self
|
|
}
|