| /* @flow */ |
| "use strict"; |
| |
| const _ = require("lodash"); |
| const chalk = require("chalk"); |
| const EOL = require("os").EOL; |
| const leven = require("leven"); |
| |
| /*:: type allowedOptionsType = { [key: string]: { alias?: string } }*/ |
| |
| const buildAllowedOptions = (allowedOptions /*: allowedOptionsType */) => { |
| let options = Object.keys(allowedOptions); |
| |
| options = options.reduce((opts, opt) => { |
| const alias = allowedOptions[opt].alias; |
| |
| if (alias) { |
| opts.push(alias); |
| } |
| |
| return opts; |
| }, options); |
| options.sort(); |
| |
| return options; |
| }; |
| |
| const suggest = (all /*: string[]*/, invalid /*: string*/) => { |
| const maxThreshold = 10; |
| |
| for (let threshold = 1; threshold <= maxThreshold; threshold++) { |
| const suggestion = all.find(option => leven(option, invalid) <= threshold); |
| |
| if (suggestion) { |
| return suggestion; |
| } |
| } |
| |
| return null; |
| }; |
| |
| const cliOption = (opt /*: string*/) => |
| opt.length === 1 ? `"-${opt}"` : `"--${_.kebabCase(opt)}"`; |
| |
| const buildMessageLine = (invalid /*: string*/, suggestion /*: ?string*/) => { |
| let line = `Invalid option ${chalk.red(cliOption(invalid))}.`; |
| |
| if (suggestion) { |
| line += ` Did you mean ${chalk.cyan(cliOption(suggestion))}?`; |
| } |
| |
| return line + EOL; |
| }; |
| |
| module.exports = function checkInvalidCLIOptions( |
| allowedOptions /*: { [key: string]: any }*/, |
| inputOptions /*: { [key: string]: any }*/ |
| ) /*: string*/ { |
| const allOptions = buildAllowedOptions(allowedOptions); |
| |
| return Object.keys(inputOptions) |
| .map(opt => _.kebabCase(opt)) |
| .filter(opt => !allOptions.includes(opt)) |
| .reduce((msg, invalid) => { |
| // NOTE: No suggestion for shortcut options because it's too difficult |
| const suggestion = |
| invalid.length >= 2 ? suggest(allOptions, invalid) : null; |
| |
| return msg + buildMessageLine(invalid, suggestion); |
| }, ""); |
| }; |