checkInvalidCLIOptions.js 2.3 KB

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