options.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. 'use strict';
  2. /**
  3. * Main entry point for handling filesystem-based configuration,
  4. * whether that's `mocha.opts` or a config file or `package.json` or whatever.
  5. * @module
  6. */
  7. const fs = require('fs');
  8. const yargsParser = require('yargs-parser');
  9. const {types, aliases} = require('./run-option-metadata');
  10. const {ONE_AND_DONE_ARGS} = require('./one-and-dones');
  11. const mocharc = require('../mocharc.json');
  12. const yargsParserConfig = require('../../package.json').yargs;
  13. const {list} = require('./run-helpers');
  14. const {loadConfig, findConfig} = require('./config');
  15. const findup = require('findup-sync');
  16. const {deprecate} = require('../utils');
  17. const debug = require('debug')('mocha:cli:options');
  18. const {createMissingArgumentError} = require('../errors');
  19. const {isNodeFlag} = require('./node-flags');
  20. /**
  21. * The `yargs-parser` namespace
  22. * @external yargsParser
  23. * @see {@link https://npm.im/yargs-parser}
  24. */
  25. /**
  26. * An object returned by a configured `yargs-parser` representing arguments
  27. * @memberof external:yargsParser
  28. * @interface Arguments
  29. */
  30. /**
  31. * This is the config pulled from the `yargs` property of Mocha's
  32. * `package.json`, but it also disables camel case expansion as to
  33. * avoid outputting non-canonical keynames, as we need to do some
  34. * lookups.
  35. * @private
  36. * @ignore
  37. */
  38. const configuration = Object.assign({}, yargsParserConfig, {
  39. 'camel-case-expansion': false
  40. });
  41. /**
  42. * This is a really fancy way to ensure unique values for `array`-type
  43. * options.
  44. * This is passed as the `coerce` option to `yargs-parser`
  45. * @private
  46. * @ignore
  47. */
  48. const coerceOpts = types.array.reduce(
  49. (acc, arg) => Object.assign(acc, {[arg]: v => Array.from(new Set(list(v)))}),
  50. {}
  51. );
  52. /**
  53. * We do not have a case when multiple arguments are ever allowed after a flag
  54. * (e.g., `--foo bar baz quux`), so we fix the number of arguments to 1 across
  55. * the board of non-boolean options.
  56. * This is passed as the `narg` option to `yargs-parser`
  57. * @private
  58. * @ignore
  59. */
  60. const nargOpts = types.array
  61. .concat(types.string, types.number)
  62. .reduce((acc, arg) => Object.assign(acc, {[arg]: 1}), {});
  63. /**
  64. * Wrapper around `yargs-parser` which applies our settings
  65. * @param {string|string[]} args - Arguments to parse
  66. * @param {...Object} configObjects - `configObjects` for yargs-parser
  67. * @private
  68. * @ignore
  69. */
  70. const parse = (args = [], ...configObjects) => {
  71. // save node-specific args for special handling.
  72. // 1. when these args have a "=" they should be considered to have values
  73. // 2. if they don't, they just boolean flags
  74. // 3. to avoid explicitly defining the set of them, we tell yargs-parser they
  75. // are ALL boolean flags.
  76. // 4. we can then reapply the values after yargs-parser is done.
  77. const nodeArgs = (Array.isArray(args) ? args : args.split(' ')).reduce(
  78. (acc, arg) => {
  79. const pair = arg.split('=');
  80. const flag = pair[0].replace(/^--?/, '');
  81. if (isNodeFlag(flag)) {
  82. return arg.includes('=')
  83. ? acc.concat([[flag, pair[1]]])
  84. : acc.concat([[flag, true]]);
  85. }
  86. return acc;
  87. },
  88. []
  89. );
  90. const result = yargsParser.detailed(args, {
  91. configuration,
  92. configObjects,
  93. coerce: coerceOpts,
  94. narg: nargOpts,
  95. alias: aliases,
  96. string: types.string,
  97. array: types.array,
  98. number: types.number,
  99. boolean: types.boolean.concat(nodeArgs.map(pair => pair[0]))
  100. });
  101. if (result.error) {
  102. throw createMissingArgumentError(result.error.message);
  103. }
  104. // reapply "=" arg values from above
  105. nodeArgs.forEach(([key, value]) => {
  106. result.argv[key] = value;
  107. });
  108. return result.argv;
  109. };
  110. /**
  111. * - Replaces comments with empty strings
  112. * - Replaces escaped spaces (e.g., 'xxx\ yyy') with HTML space
  113. * - Splits on whitespace, creating array of substrings
  114. * - Filters empty string elements from array
  115. * - Replaces any HTML space with space
  116. * @summary Parses options read from run-control file.
  117. * @private
  118. * @param {string} content - Content read from run-control file.
  119. * @returns {string[]} cmdline options (and associated arguments)
  120. * @ignore
  121. */
  122. const parseMochaOpts = content =>
  123. content
  124. .replace(/^#.*$/gm, '')
  125. .replace(/\\\s/g, '%20')
  126. .split(/\s/)
  127. .filter(Boolean)
  128. .map(value => value.replace(/%20/g, ' '));
  129. /**
  130. * Prepends options from run-control file to the command line arguments.
  131. *
  132. * @deprecated Deprecated in v6.0.0; This function is no longer used internally and will be removed in a future version.
  133. * @public
  134. * @alias module:lib/cli/options
  135. * @see {@link https://mochajs.org/#mochaopts|mocha.opts}
  136. */
  137. module.exports = function getOptions() {
  138. deprecate(
  139. 'getOptions() is DEPRECATED and will be removed from a future version of Mocha. Use loadOptions() instead'
  140. );
  141. if (process.argv.length === 3 && ONE_AND_DONE_ARGS.has(process.argv[2])) {
  142. return;
  143. }
  144. const optsPath =
  145. process.argv.indexOf('--opts') === -1
  146. ? mocharc.opts
  147. : process.argv[process.argv.indexOf('--opts') + 1];
  148. try {
  149. const options = parseMochaOpts(fs.readFileSync(optsPath, 'utf8'));
  150. process.argv = process.argv
  151. .slice(0, 2)
  152. .concat(options.concat(process.argv.slice(2)));
  153. } catch (ignore) {
  154. // NOTE: should console.error() and throw the error
  155. }
  156. process.env.LOADED_MOCHA_OPTS = true;
  157. };
  158. /**
  159. * Given filepath in `args.opts`, attempt to load and parse a `mocha.opts` file.
  160. * @param {Object} [args] - Arguments object
  161. * @param {string|boolean} [args.opts] - Filepath to mocha.opts; defaults to whatever's in `mocharc.opts`, or `false` to skip
  162. * @returns {external:yargsParser.Arguments|void} If read, object containing parsed arguments
  163. * @memberof module:lib/cli/options
  164. * @public
  165. */
  166. const loadMochaOpts = (args = {}) => {
  167. let result;
  168. let filepath = args.opts;
  169. // /dev/null is backwards compat
  170. if (filepath === false || filepath === '/dev/null') {
  171. return result;
  172. }
  173. filepath = filepath || mocharc.opts;
  174. result = {};
  175. let mochaOpts;
  176. try {
  177. mochaOpts = fs.readFileSync(filepath, 'utf8');
  178. debug(`read ${filepath}`);
  179. } catch (err) {
  180. if (args.opts) {
  181. throw new Error(`Unable to read ${filepath}: ${err}`);
  182. }
  183. // ignore otherwise. we tried
  184. debug(`No mocha.opts found at ${filepath}`);
  185. }
  186. // real args should override `mocha.opts` which should override defaults.
  187. // if there's an exception to catch here, I'm not sure what it is.
  188. // by attaching the `no-opts` arg, we avoid re-parsing of `mocha.opts`.
  189. if (mochaOpts) {
  190. result = parse(parseMochaOpts(mochaOpts));
  191. debug(`${filepath} parsed succesfully`);
  192. }
  193. return result;
  194. };
  195. module.exports.loadMochaOpts = loadMochaOpts;
  196. /**
  197. * Given path to config file in `args.config`, attempt to load & parse config file.
  198. * @param {Object} [args] - Arguments object
  199. * @param {string|boolean} [args.config] - Path to config file or `false` to skip
  200. * @public
  201. * @memberof module:lib/cli/options
  202. * @returns {external:yargsParser.Arguments|void} Parsed config, or nothing if `args.config` is `false`
  203. */
  204. const loadRc = (args = {}) => {
  205. if (args.config !== false) {
  206. const config = args.config || findConfig();
  207. return config ? loadConfig(config) : {};
  208. }
  209. };
  210. module.exports.loadRc = loadRc;
  211. /**
  212. * Given path to `package.json` in `args.package`, attempt to load config from `mocha` prop.
  213. * @param {Object} [args] - Arguments object
  214. * @param {string|boolean} [args.config] - Path to `package.json` or `false` to skip
  215. * @public
  216. * @memberof module:lib/cli/options
  217. * @returns {external:yargsParser.Arguments|void} Parsed config, or nothing if `args.package` is `false`
  218. */
  219. const loadPkgRc = (args = {}) => {
  220. let result;
  221. if (args.package === false) {
  222. return result;
  223. }
  224. result = {};
  225. const filepath = args.package || findup(mocharc.package);
  226. if (filepath) {
  227. try {
  228. const pkg = JSON.parse(fs.readFileSync(filepath, 'utf8'));
  229. if (pkg.mocha) {
  230. debug(`'mocha' prop of package.json parsed:`, pkg.mocha);
  231. result = pkg.mocha;
  232. } else {
  233. debug(`no config found in ${filepath}`);
  234. }
  235. } catch (err) {
  236. if (args.package) {
  237. throw new Error(`Unable to read/parse ${filepath}: ${err}`);
  238. }
  239. debug(`failed to read default package.json at ${filepath}; ignoring`);
  240. }
  241. }
  242. return result;
  243. };
  244. module.exports.loadPkgRc = loadPkgRc;
  245. /**
  246. * Priority list:
  247. *
  248. * 1. Command-line args
  249. * 2. RC file (`.mocharc.js`, `.mocharc.ya?ml`, `mocharc.json`)
  250. * 3. `mocha` prop of `package.json`
  251. * 4. `mocha.opts`
  252. * 5. default configuration (`lib/mocharc.json`)
  253. *
  254. * If a {@link module:lib/cli/one-and-dones.ONE_AND_DONE_ARGS "one-and-done" option} is present in the `argv` array, no external config files will be read.
  255. * @summary Parses options read from `mocha.opts`, `.mocharc.*` and `package.json`.
  256. * @param {string|string[]} [argv] - Arguments to parse
  257. * @public
  258. * @memberof module:lib/cli/options
  259. * @returns {external:yargsParser.Arguments} Parsed args from everything
  260. */
  261. const loadOptions = (argv = []) => {
  262. let args = parse(argv);
  263. // short-circuit: look for a flag that would abort loading of mocha.opts
  264. if (
  265. Array.from(ONE_AND_DONE_ARGS).reduce(
  266. (acc, arg) => acc || arg in args,
  267. false
  268. )
  269. ) {
  270. return args;
  271. }
  272. const rcConfig = loadRc(args);
  273. const pkgConfig = loadPkgRc(args);
  274. const optsConfig = loadMochaOpts(args);
  275. if (rcConfig) {
  276. args.config = false;
  277. }
  278. if (pkgConfig) {
  279. args.package = false;
  280. }
  281. if (optsConfig) {
  282. args.opts = false;
  283. }
  284. args = parse(
  285. args._,
  286. args,
  287. rcConfig || {},
  288. pkgConfig || {},
  289. optsConfig || {},
  290. mocharc
  291. );
  292. // recombine positional arguments and "spec"
  293. if (args.spec) {
  294. args._ = args._.concat(args.spec);
  295. delete args.spec;
  296. }
  297. return args;
  298. };
  299. module.exports.loadOptions = loadOptions;