checkInvalidCLIOptions.js 2.2 KB

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