blob: 6f8bb5eee2ba95543b9c067e52c7638f73b6f4f8 [file] [log] [blame]
Mathias Bynens79e2cf02020-05-29 16:46:17 +02001'use strict';
2
Mathias Bynens79e2cf02020-05-29 16:46:17 +02003const EOL = require('os').EOL;
Tim van der Lippeefb716a2020-12-01 12:54:04 +00004const levenshtein = require('fastest-levenshtein');
Tim van der Lippe16b82282021-11-08 13:50:26 +00005const { red, cyan } = require('picocolors');
Mathias Bynens79e2cf02020-05-29 16:46:17 +02006
7/**
8 * @param {{ [key: string]: { alias?: string } }} allowedOptions
9 * @return {string[]}
10 */
11const 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 */
33const suggest = (all, invalid) => {
34 const maxThreshold = 10;
35
36 for (let threshold = 1; threshold <= maxThreshold; threshold++) {
Tim van der Lippeefb716a2020-12-01 12:54:04 +000037 const suggestion = all.find((option) => levenshtein.distance(option, invalid) <= threshold);
Mathias Bynens79e2cf02020-05-29 16:46:17 +020038
39 if (suggestion) {
40 return suggestion;
41 }
42 }
43
44 return null;
45};
46
47/**
Tim van der Lippe16b82282021-11-08 13:50:26 +000048 * Converts a string to kebab case.
49 * For example, `kebabCase('oneTwoThree') === 'one-two-three'`.
50 * @param {string} opt
51 * @returns {string}
52 */
53const 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 Bynens79e2cf02020-05-29 16:46:17 +020064 * @param {string} opt
65 * @return {string}
66 */
67const cliOption = (opt) => {
68 if (opt.length === 1) {
69 return `"-${opt}"`;
70 }
71
Tim van der Lippe16b82282021-11-08 13:50:26 +000072 return `"--${kebabCase(opt)}"`;
Mathias Bynens79e2cf02020-05-29 16:46:17 +020073};
74
75/**
76 * @param {string} invalid
77 * @param {string|null} suggestion
78 * @return {string}
79 */
80const buildMessageLine = (invalid, suggestion) => {
Tim van der Lippe16b82282021-11-08 13:50:26 +000081 let line = `Invalid option ${red(cliOption(invalid))}.`;
Mathias Bynens79e2cf02020-05-29 16:46:17 +020082
83 if (suggestion) {
Tim van der Lippe16b82282021-11-08 13:50:26 +000084 line += ` Did you mean ${cyan(cliOption(suggestion))}?`;
Mathias Bynens79e2cf02020-05-29 16:46:17 +020085 }
86
87 return line + EOL;
88};
89
90/**
91 * @param {{ [key: string]: any }} allowedOptions
92 * @param {{ [key: string]: any }} inputOptions
93 * @return {string}
94 */
95module.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 Lippe16b82282021-11-08 13:50:26 +0000100 .map(kebabCase)
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200101 .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};