| 'use strict'; |
| |
| /** |
| * Main entry point for handling filesystem-based configuration, |
| * whether that's `mocha.opts` or a config file or `package.json` or whatever. |
| * @module |
| */ |
| |
| const fs = require('fs'); |
| const ansi = require('ansi-colors'); |
| const yargsParser = require('yargs-parser'); |
| const {types, aliases} = require('./run-option-metadata'); |
| const {ONE_AND_DONE_ARGS} = require('./one-and-dones'); |
| const mocharc = require('../mocharc.json'); |
| const {list} = require('./run-helpers'); |
| const {loadConfig, findConfig} = require('./config'); |
| const findUp = require('find-up'); |
| const {deprecate} = require('../utils'); |
| const debug = require('debug')('mocha:cli:options'); |
| const {isNodeFlag} = require('./node-flags'); |
| |
| /** |
| * The `yargs-parser` namespace |
| * @external yargsParser |
| * @see {@link https://npm.im/yargs-parser} |
| */ |
| |
| /** |
| * An object returned by a configured `yargs-parser` representing arguments |
| * @memberof external:yargsParser |
| * @interface Arguments |
| */ |
| |
| /** |
| * Base yargs parser configuration |
| * @private |
| */ |
| const YARGS_PARSER_CONFIG = { |
| 'combine-arrays': true, |
| 'short-option-groups': false, |
| 'dot-notation': false |
| }; |
| |
| /** |
| * This is the config pulled from the `yargs` property of Mocha's |
| * `package.json`, but it also disables camel case expansion as to |
| * avoid outputting non-canonical keynames, as we need to do some |
| * lookups. |
| * @private |
| * @ignore |
| */ |
| const configuration = Object.assign({}, YARGS_PARSER_CONFIG, { |
| 'camel-case-expansion': false |
| }); |
| |
| /** |
| * This is a really fancy way to: |
| * - ensure unique values for `array`-type options |
| * - use its array's last element for `boolean`/`number`/`string`- options given multiple times |
| * This is passed as the `coerce` option to `yargs-parser` |
| * @private |
| * @ignore |
| */ |
| const coerceOpts = Object.assign( |
| types.array.reduce( |
| (acc, arg) => |
| Object.assign(acc, {[arg]: v => Array.from(new Set(list(v)))}), |
| {} |
| ), |
| types.boolean |
| .concat(types.string, types.number) |
| .reduce( |
| (acc, arg) => |
| Object.assign(acc, {[arg]: v => (Array.isArray(v) ? v.pop() : v)}), |
| {} |
| ) |
| ); |
| |
| /** |
| * We do not have a case when multiple arguments are ever allowed after a flag |
| * (e.g., `--foo bar baz quux`), so we fix the number of arguments to 1 across |
| * the board of non-boolean options. |
| * This is passed as the `narg` option to `yargs-parser` |
| * @private |
| * @ignore |
| */ |
| const nargOpts = types.array |
| .concat(types.string, types.number) |
| .reduce((acc, arg) => Object.assign(acc, {[arg]: 1}), {}); |
| |
| /** |
| * Wrapper around `yargs-parser` which applies our settings |
| * @param {string|string[]} args - Arguments to parse |
| * @param {Object} defaultValues - Default values of mocharc.json |
| * @param {...Object} configObjects - `configObjects` for yargs-parser |
| * @private |
| * @ignore |
| */ |
| const parse = (args = [], defaultValues = {}, ...configObjects) => { |
| // save node-specific args for special handling. |
| // 1. when these args have a "=" they should be considered to have values |
| // 2. if they don't, they just boolean flags |
| // 3. to avoid explicitly defining the set of them, we tell yargs-parser they |
| // are ALL boolean flags. |
| // 4. we can then reapply the values after yargs-parser is done. |
| const nodeArgs = (Array.isArray(args) ? args : args.split(' ')).reduce( |
| (acc, arg) => { |
| const pair = arg.split('='); |
| let flag = pair[0]; |
| if (isNodeFlag(flag, false)) { |
| flag = flag.replace(/^--?/, ''); |
| return arg.includes('=') |
| ? acc.concat([[flag, pair[1]]]) |
| : acc.concat([[flag, true]]); |
| } |
| return acc; |
| }, |
| [] |
| ); |
| |
| const result = yargsParser.detailed(args, { |
| configuration, |
| configObjects, |
| default: defaultValues, |
| coerce: coerceOpts, |
| narg: nargOpts, |
| alias: aliases, |
| string: types.string, |
| array: types.array, |
| number: types.number, |
| boolean: types.boolean.concat(nodeArgs.map(pair => pair[0])) |
| }); |
| if (result.error) { |
| console.error(ansi.red(`Error: ${result.error.message}`)); |
| process.exit(1); |
| } |
| |
| // reapply "=" arg values from above |
| nodeArgs.forEach(([key, value]) => { |
| result.argv[key] = value; |
| }); |
| |
| return result.argv; |
| }; |
| |
| /** |
| * - Replaces comments with empty strings |
| * - Replaces escaped spaces (e.g., 'xxx\ yyy') with HTML space |
| * - Splits on whitespace, creating array of substrings |
| * - Filters empty string elements from array |
| * - Replaces any HTML space with space |
| * @summary Parses options read from run-control file. |
| * @private |
| * @param {string} content - Content read from run-control file. |
| * @returns {string[]} cmdline options (and associated arguments) |
| * @ignore |
| */ |
| const parseMochaOpts = content => |
| content |
| .replace(/^#.*$/gm, '') |
| .replace(/\\\s/g, '%20') |
| .split(/\s/) |
| .filter(Boolean) |
| .map(value => value.replace(/%20/g, ' ')); |
| |
| /** |
| * Prepends options from run-control file to the command line arguments. |
| * |
| * @deprecated Deprecated in v6.0.0; This function is no longer used internally and will be removed in a future version. |
| * @public |
| * @alias module:lib/cli/options |
| * @see {@link https://mochajs.org/#mochaopts|mocha.opts} |
| */ |
| module.exports = function getOptions() { |
| deprecate( |
| 'getOptions() is DEPRECATED and will be removed from a future version of Mocha. Use loadOptions() instead' |
| ); |
| if (process.argv.length === 3 && ONE_AND_DONE_ARGS.has(process.argv[2])) { |
| return; |
| } |
| |
| const optsPath = |
| process.argv.indexOf('--opts') === -1 |
| ? mocharc.opts |
| : process.argv[process.argv.indexOf('--opts') + 1]; |
| |
| try { |
| const options = parseMochaOpts(fs.readFileSync(optsPath, 'utf8')); |
| |
| process.argv = process.argv |
| .slice(0, 2) |
| .concat(options.concat(process.argv.slice(2))); |
| } catch (ignore) { |
| // NOTE: should console.error() and throw the error |
| } |
| |
| process.env.LOADED_MOCHA_OPTS = true; |
| }; |
| |
| /** |
| * Given filepath in `args.opts`, attempt to load and parse a `mocha.opts` file. |
| * @param {Object} [args] - Arguments object |
| * @param {string|boolean} [args.opts] - Filepath to mocha.opts; defaults to whatever's in `mocharc.opts`, or `false` to skip |
| * @returns {external:yargsParser.Arguments|void} If read, object containing parsed arguments |
| * @memberof module:lib/cli/options |
| * @public |
| */ |
| const loadMochaOpts = (args = {}) => { |
| let result; |
| let filepath = args.opts; |
| // /dev/null is backwards compat |
| if (filepath === false || filepath === '/dev/null') { |
| return result; |
| } |
| filepath = filepath || mocharc.opts; |
| result = {}; |
| let mochaOpts; |
| try { |
| mochaOpts = fs.readFileSync(filepath, 'utf8'); |
| debug(`read ${filepath}`); |
| } catch (err) { |
| if (args.opts) { |
| throw new Error(`Unable to read ${filepath}: ${err}`); |
| } |
| // ignore otherwise. we tried |
| debug(`No mocha.opts found at ${filepath}`); |
| } |
| |
| // real args should override `mocha.opts` which should override defaults. |
| // if there's an exception to catch here, I'm not sure what it is. |
| // by attaching the `no-opts` arg, we avoid re-parsing of `mocha.opts`. |
| if (mochaOpts) { |
| result = parse(parseMochaOpts(mochaOpts)); |
| debug(`${filepath} parsed succesfully`); |
| } |
| return result; |
| }; |
| |
| module.exports.loadMochaOpts = loadMochaOpts; |
| |
| /** |
| * Given path to config file in `args.config`, attempt to load & parse config file. |
| * @param {Object} [args] - Arguments object |
| * @param {string|boolean} [args.config] - Path to config file or `false` to skip |
| * @public |
| * @memberof module:lib/cli/options |
| * @returns {external:yargsParser.Arguments|void} Parsed config, or nothing if `args.config` is `false` |
| */ |
| const loadRc = (args = {}) => { |
| if (args.config !== false) { |
| const config = args.config || findConfig(); |
| return config ? loadConfig(config) : {}; |
| } |
| }; |
| |
| module.exports.loadRc = loadRc; |
| |
| /** |
| * Given path to `package.json` in `args.package`, attempt to load config from `mocha` prop. |
| * @param {Object} [args] - Arguments object |
| * @param {string|boolean} [args.config] - Path to `package.json` or `false` to skip |
| * @public |
| * @memberof module:lib/cli/options |
| * @returns {external:yargsParser.Arguments|void} Parsed config, or nothing if `args.package` is `false` |
| */ |
| const loadPkgRc = (args = {}) => { |
| let result; |
| if (args.package === false) { |
| return result; |
| } |
| result = {}; |
| const filepath = args.package || findUp.sync(mocharc.package); |
| if (filepath) { |
| try { |
| const pkg = JSON.parse(fs.readFileSync(filepath, 'utf8')); |
| if (pkg.mocha) { |
| debug(`'mocha' prop of package.json parsed:`, pkg.mocha); |
| result = pkg.mocha; |
| } else { |
| debug(`no config found in ${filepath}`); |
| } |
| } catch (err) { |
| if (args.package) { |
| throw new Error(`Unable to read/parse ${filepath}: ${err}`); |
| } |
| debug(`failed to read default package.json at ${filepath}; ignoring`); |
| } |
| } |
| return result; |
| }; |
| |
| module.exports.loadPkgRc = loadPkgRc; |
| |
| /** |
| * Priority list: |
| * |
| * 1. Command-line args |
| * 2. RC file (`.mocharc.js`, `.mocharc.ya?ml`, `mocharc.json`) |
| * 3. `mocha` prop of `package.json` |
| * 4. `mocha.opts` |
| * 5. default configuration (`lib/mocharc.json`) |
| * |
| * 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. |
| * @summary Parses options read from `mocha.opts`, `.mocharc.*` and `package.json`. |
| * @param {string|string[]} [argv] - Arguments to parse |
| * @public |
| * @memberof module:lib/cli/options |
| * @returns {external:yargsParser.Arguments} Parsed args from everything |
| */ |
| const loadOptions = (argv = []) => { |
| let args = parse(argv); |
| // short-circuit: look for a flag that would abort loading of mocha.opts |
| if ( |
| Array.from(ONE_AND_DONE_ARGS).reduce( |
| (acc, arg) => acc || arg in args, |
| false |
| ) |
| ) { |
| return args; |
| } |
| |
| const rcConfig = loadRc(args); |
| const pkgConfig = loadPkgRc(args); |
| const optsConfig = loadMochaOpts(args); |
| |
| if (rcConfig) { |
| args.config = false; |
| args._ = args._.concat(rcConfig._ || []); |
| } |
| if (pkgConfig) { |
| args.package = false; |
| args._ = args._.concat(pkgConfig._ || []); |
| } |
| if (optsConfig) { |
| args.opts = false; |
| args._ = args._.concat(optsConfig._ || []); |
| } |
| |
| args = parse( |
| args._, |
| mocharc, |
| args, |
| rcConfig || {}, |
| pkgConfig || {}, |
| optsConfig || {} |
| ); |
| |
| // recombine positional arguments and "spec" |
| if (args.spec) { |
| args._ = args._.concat(args.spec); |
| delete args.spec; |
| } |
| |
| // make unique |
| args._ = Array.from(new Set(args._)); |
| |
| return args; |
| }; |
| |
| module.exports.loadOptions = loadOptions; |
| module.exports.YARGS_PARSER_CONFIG = YARGS_PARSER_CONFIG; |