Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 1 | 'use strict'; |
| 2 | |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 3 | const EOL = require('os').EOL; |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame] | 4 | const levenshtein = require('fastest-levenshtein'); |
Tim van der Lippe | 16b8228 | 2021-11-08 13:50:26 +0000 | [diff] [blame^] | 5 | const { red, cyan } = require('picocolors'); |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 6 | |
| 7 | /** |
| 8 | * @param {{ [key: string]: { alias?: string } }} allowedOptions |
| 9 | * @return {string[]} |
| 10 | */ |
| 11 | const buildAllowedOptions = (allowedOptions) => { |
| 12 | let options = Object.keys(allowedOptions); |
| 13 | |
| 14 | options = options.reduce((opts, opt) => { |
| 15 | const alias = allowedOptions[opt].alias; |
| 16 | |
| 17 | if (alias) { |
| 18 | opts.push(alias); |
| 19 | } |
| 20 | |
| 21 | return opts; |
| 22 | }, options); |
| 23 | options.sort(); |
| 24 | |
| 25 | return options; |
| 26 | }; |
| 27 | |
| 28 | /** |
| 29 | * @param {string[]} all |
| 30 | * @param {string} invalid |
| 31 | * @return {null|string} |
| 32 | */ |
| 33 | const suggest = (all, invalid) => { |
| 34 | const maxThreshold = 10; |
| 35 | |
| 36 | for (let threshold = 1; threshold <= maxThreshold; threshold++) { |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame] | 37 | const suggestion = all.find((option) => levenshtein.distance(option, invalid) <= threshold); |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 38 | |
| 39 | if (suggestion) { |
| 40 | return suggestion; |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | return null; |
| 45 | }; |
| 46 | |
| 47 | /** |
Tim van der Lippe | 16b8228 | 2021-11-08 13:50:26 +0000 | [diff] [blame^] | 48 | * Converts a string to kebab case. |
| 49 | * For example, `kebabCase('oneTwoThree') === 'one-two-three'`. |
| 50 | * @param {string} opt |
| 51 | * @returns {string} |
| 52 | */ |
| 53 | const kebabCase = (opt) => { |
| 54 | const matches = opt.match(/[A-Z]?[a-z]+|[A-Z]|[0-9]+/g); |
| 55 | |
| 56 | if (matches) { |
| 57 | return matches.map((s) => s.toLowerCase()).join('-'); |
| 58 | } |
| 59 | |
| 60 | return ''; |
| 61 | }; |
| 62 | |
| 63 | /** |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 64 | * @param {string} opt |
| 65 | * @return {string} |
| 66 | */ |
| 67 | const cliOption = (opt) => { |
| 68 | if (opt.length === 1) { |
| 69 | return `"-${opt}"`; |
| 70 | } |
| 71 | |
Tim van der Lippe | 16b8228 | 2021-11-08 13:50:26 +0000 | [diff] [blame^] | 72 | return `"--${kebabCase(opt)}"`; |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 73 | }; |
| 74 | |
| 75 | /** |
| 76 | * @param {string} invalid |
| 77 | * @param {string|null} suggestion |
| 78 | * @return {string} |
| 79 | */ |
| 80 | const buildMessageLine = (invalid, suggestion) => { |
Tim van der Lippe | 16b8228 | 2021-11-08 13:50:26 +0000 | [diff] [blame^] | 81 | let line = `Invalid option ${red(cliOption(invalid))}.`; |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 82 | |
| 83 | if (suggestion) { |
Tim van der Lippe | 16b8228 | 2021-11-08 13:50:26 +0000 | [diff] [blame^] | 84 | line += ` Did you mean ${cyan(cliOption(suggestion))}?`; |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 85 | } |
| 86 | |
| 87 | return line + EOL; |
| 88 | }; |
| 89 | |
| 90 | /** |
| 91 | * @param {{ [key: string]: any }} allowedOptions |
| 92 | * @param {{ [key: string]: any }} inputOptions |
| 93 | * @return {string} |
| 94 | */ |
| 95 | module.exports = function checkInvalidCLIOptions(allowedOptions, inputOptions) { |
| 96 | const allOptions = buildAllowedOptions(allowedOptions); |
| 97 | |
| 98 | return Object.keys(inputOptions) |
| 99 | .filter((opt) => !allOptions.includes(opt)) |
Tim van der Lippe | 16b8228 | 2021-11-08 13:50:26 +0000 | [diff] [blame^] | 100 | .map(kebabCase) |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 101 | .reduce((msg, invalid) => { |
| 102 | // NOTE: No suggestion for shortcut options because it's too difficult |
| 103 | const suggestion = invalid.length >= 2 ? suggest(allOptions, invalid) : null; |
| 104 | |
| 105 | return msg + buildMessageLine(invalid, suggestion); |
| 106 | }, ''); |
| 107 | }; |