index.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. 'use strict';
  2. const path = require('path');
  3. const buildParserOptions = require('minimist-options');
  4. const parseArguments = require('yargs-parser');
  5. const camelCaseKeys = require('camelcase-keys');
  6. const decamelize = require('decamelize');
  7. const decamelizeKeys = require('decamelize-keys');
  8. const trimNewlines = require('trim-newlines');
  9. const redent = require('redent');
  10. const readPkgUp = require('read-pkg-up');
  11. const hardRejection = require('hard-rejection');
  12. const normalizePackageData = require('normalize-package-data');
  13. // Prevent caching of this module so module.parent is always accurate
  14. delete require.cache[__filename];
  15. const parentDir = path.dirname(module.parent && module.parent.filename ? module.parent.filename : '.');
  16. const isFlagMissing = (flagName, definedFlags, receivedFlags, input) => {
  17. const flag = definedFlags[flagName];
  18. let isFlagRequired = true;
  19. if (typeof flag.isRequired === 'function') {
  20. isFlagRequired = flag.isRequired(receivedFlags, input);
  21. if (typeof isFlagRequired !== 'boolean') {
  22. throw new TypeError(`Return value for isRequired callback should be of type boolean, but ${typeof isFlagRequired} was returned.`);
  23. }
  24. }
  25. if (typeof receivedFlags[flagName] === 'undefined') {
  26. return isFlagRequired;
  27. }
  28. return flag.isMultiple && receivedFlags[flagName].length === 0;
  29. };
  30. const getMissingRequiredFlags = (flags, receivedFlags, input) => {
  31. const missingRequiredFlags = [];
  32. if (typeof flags === 'undefined') {
  33. return [];
  34. }
  35. for (const flagName of Object.keys(flags)) {
  36. if (flags[flagName].isRequired && isFlagMissing(flagName, flags, receivedFlags, input)) {
  37. missingRequiredFlags.push({key: flagName, ...flags[flagName]});
  38. }
  39. }
  40. return missingRequiredFlags;
  41. };
  42. const reportMissingRequiredFlags = missingRequiredFlags => {
  43. console.error(`Missing required flag${missingRequiredFlags.length > 1 ? 's' : ''}`);
  44. for (const flag of missingRequiredFlags) {
  45. console.error(`\t--${decamelize(flag.key, '-')}${flag.alias ? `, -${flag.alias}` : ''}`);
  46. }
  47. };
  48. const validateOptions = ({flags}) => {
  49. const invalidFlags = Object.keys(flags).filter(flagKey => flagKey.includes('-') && flagKey !== '--');
  50. if (invalidFlags.length > 0) {
  51. throw new Error(`Flag keys may not contain '-': ${invalidFlags.join(', ')}`);
  52. }
  53. };
  54. const reportUnknownFlags = unknownFlags => {
  55. console.error([
  56. `Unknown flag${unknownFlags.length > 1 ? 's' : ''}`,
  57. ...unknownFlags
  58. ].join('\n'));
  59. };
  60. const buildParserFlags = ({flags, booleanDefault}) => {
  61. const parserFlags = {};
  62. for (const [flagKey, flagValue] of Object.entries(flags)) {
  63. const flag = {...flagValue};
  64. if (
  65. typeof booleanDefault !== 'undefined' &&
  66. flag.type === 'boolean' &&
  67. !Object.prototype.hasOwnProperty.call(flag, 'default')
  68. ) {
  69. flag.default = flag.isMultiple ? [booleanDefault] : booleanDefault;
  70. }
  71. if (flag.isMultiple) {
  72. flag.type = flag.type ? `${flag.type}-array` : 'array';
  73. flag.default = flag.default || [];
  74. delete flag.isMultiple;
  75. }
  76. parserFlags[flagKey] = flag;
  77. }
  78. return parserFlags;
  79. };
  80. const validateFlags = (flags, options) => {
  81. for (const [flagKey, flagValue] of Object.entries(options.flags)) {
  82. if (flagKey !== '--' && !flagValue.isMultiple && Array.isArray(flags[flagKey])) {
  83. throw new Error(`The flag --${flagKey} can only be set once.`);
  84. }
  85. }
  86. };
  87. const meow = (helpText, options) => {
  88. if (typeof helpText !== 'string') {
  89. options = helpText;
  90. helpText = '';
  91. }
  92. const foundPkg = readPkgUp.sync({
  93. cwd: parentDir,
  94. normalize: false
  95. });
  96. options = {
  97. pkg: foundPkg ? foundPkg.packageJson : {},
  98. argv: process.argv.slice(2),
  99. flags: {},
  100. inferType: false,
  101. input: 'string',
  102. help: helpText,
  103. autoHelp: true,
  104. autoVersion: true,
  105. booleanDefault: false,
  106. hardRejection: true,
  107. allowUnknownFlags: true,
  108. ...options
  109. };
  110. if (options.hardRejection) {
  111. hardRejection();
  112. }
  113. validateOptions(options);
  114. let parserOptions = {
  115. arguments: options.input,
  116. ...buildParserFlags(options)
  117. };
  118. parserOptions = decamelizeKeys(parserOptions, '-', {exclude: ['stopEarly', '--']});
  119. if (options.inferType) {
  120. delete parserOptions.arguments;
  121. }
  122. parserOptions = buildParserOptions(parserOptions);
  123. parserOptions.configuration = {
  124. ...parserOptions.configuration,
  125. 'greedy-arrays': false
  126. };
  127. if (parserOptions['--']) {
  128. parserOptions.configuration['populate--'] = true;
  129. }
  130. if (!options.allowUnknownFlags) {
  131. // Collect unknown options in `argv._` to be checked later.
  132. parserOptions.configuration['unknown-options-as-args'] = true;
  133. }
  134. const {pkg} = options;
  135. const argv = parseArguments(options.argv, parserOptions);
  136. let help = redent(trimNewlines((options.help || '').replace(/\t+\n*$/, '')), 2);
  137. normalizePackageData(pkg);
  138. process.title = pkg.bin ? Object.keys(pkg.bin)[0] : pkg.name;
  139. let {description} = options;
  140. if (!description && description !== false) {
  141. ({description} = pkg);
  142. }
  143. help = (description ? `\n ${description}\n` : '') + (help ? `\n${help}\n` : '\n');
  144. const showHelp = code => {
  145. console.log(help);
  146. process.exit(typeof code === 'number' ? code : 2);
  147. };
  148. const showVersion = () => {
  149. console.log(typeof options.version === 'string' ? options.version : pkg.version);
  150. process.exit(0);
  151. };
  152. if (argv._.length === 0 && options.argv.length === 1) {
  153. if (argv.version === true && options.autoVersion) {
  154. showVersion();
  155. }
  156. if (argv.help === true && options.autoHelp) {
  157. showHelp(0);
  158. }
  159. }
  160. const input = argv._;
  161. delete argv._;
  162. if (!options.allowUnknownFlags) {
  163. const unknownFlags = input.filter(item => typeof item === 'string' && item.startsWith('-'));
  164. if (unknownFlags.length > 0) {
  165. reportUnknownFlags(unknownFlags);
  166. process.exit(2);
  167. }
  168. }
  169. const flags = camelCaseKeys(argv, {exclude: ['--', /^\w$/]});
  170. const unnormalizedFlags = {...flags};
  171. validateFlags(flags, options);
  172. for (const flagValue of Object.values(options.flags)) {
  173. delete flags[flagValue.alias];
  174. }
  175. const missingRequiredFlags = getMissingRequiredFlags(options.flags, flags, input);
  176. if (missingRequiredFlags.length > 0) {
  177. reportMissingRequiredFlags(missingRequiredFlags);
  178. process.exit(2);
  179. }
  180. return {
  181. input,
  182. flags,
  183. unnormalizedFlags,
  184. pkg,
  185. help,
  186. showHelp,
  187. showVersion
  188. };
  189. };
  190. module.exports = meow;