diff --git a/docs/05-command-line.md b/docs/05-command-line.md index e805bf2e7..4abdf07a3 100644 --- a/docs/05-command-line.md +++ b/docs/05-command-line.md @@ -21,6 +21,8 @@ $ npx ava --help --color Force color output --no-color Disable color output --reset-cache Reset AVA's compilation cache and exit + --config JavaScript file for AVA to read its config from, instead of using package.json + or ava.config.js files Examples ava diff --git a/docs/06-configuration.md b/docs/06-configuration.md index 8d5521d2f..d707a82b1 100644 --- a/docs/06-configuration.md +++ b/docs/06-configuration.md @@ -2,7 +2,7 @@ Translations: [Français](https://github.com/avajs/ava-docs/blob/master/fr_FR/docs/06-configuration.md) -All of the [CLI options](./05-command-line.md) can be configured in the `ava` section of either your `package.json` file, or an `ava.config.js` file. This allows you to modify the default behavior of the `ava` command, so you don't have to repeatedly type the same options on the command prompt. +All of the [CLI options][CLI] can be configured in the `ava` section of either your `package.json` file, or an `ava.config.js` file. This allows you to modify the default behavior of the `ava` command, so you don't have to repeatedly type the same options on the command prompt. To ignore files, prefix the pattern with an `!` (exclamation mark). @@ -115,6 +115,35 @@ export default ({projectDir}) => { Note that the final configuration must not be a promise. +## Alternative configuration files + +The [CLI] lets you specify a specific configuration file, using the `--config` flag. This file is processed just like an `ava.config.js` file would be. When the `--config` flag is set, the provided file will override all configuration from the `package.json` and `ava.config.js` files. The configuration is not merged. + +The configuration file *must* be in the same directory as the `package.json` file. + +You can use this to customize configuration for a specific test run. For instance, you may want to run unit tests separately from integration tests: + +`ava.config.js`: + +```js +export default { + files: ['unit-tests/**/*'] +}; +``` + +`integration-tests.config.js`: + +```js +import baseConfig from './ava.config.js'; + +export default { + ...baseConfig, + files: ['integration-tests/**/*'] +}; +``` + +You can now run your unit tests through `npx ava` and the integration tests through `npx ava --config integration-tests.config.js`. + ## Object printing depth By default, AVA prints nested objects to a depth of `3`. However, when debugging tests with deeply nested objects, it can be useful to print with more detail. This can be done by setting [`util.inspect.defaultOptions.depth`](https://nodejs.org/api/util.html#util_util_inspect_defaultoptions) to the desired depth, before the test is executed: @@ -132,3 +161,5 @@ test('My test', t => { ``` AVA has a minimum depth of `3`. + +[CLI]: ./05-command-line.md diff --git a/eslint-plugin-helper.js b/eslint-plugin-helper.js index 513cdd383..6b04e0fb8 100644 --- a/eslint-plugin-helper.js +++ b/eslint-plugin-helper.js @@ -18,7 +18,7 @@ function load(projectDir, overrides) { if (configCache.has(projectDir)) { ({conf, babelConfig} = configCache.get(projectDir)); } else { - conf = loadConfig(projectDir); + conf = loadConfig({resolveFrom: projectDir}); babelConfig = babelPipeline.validate(conf.babel); configCache.set(projectDir, {conf, babelConfig}); } diff --git a/lib/cli.js b/lib/cli.js index f94aa34da..b5d3c3497 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -7,7 +7,7 @@ const arrify = require('arrify'); const meow = require('meow'); const Promise = require('bluebird'); const isCi = require('is-ci'); -const loadConf = require('./load-config'); +const loadConfig = require('./load-config'); // Bluebird specific Promise.longStackTraces(); @@ -18,10 +18,17 @@ function exit(message) { } exports.run = async () => { // eslint-disable-line complexity + const {flags: {config: configFile}} = meow({ // Process the --config flag first + autoHelp: false, // --help should get picked up by the next meow invocation. + flags: { + config: {type: 'string'} + } + }); + let conf = {}; let confError = null; try { - conf = loadConf(); + conf = loadConfig({configFile}); } catch (error) { confError = error; } @@ -43,6 +50,8 @@ exports.run = async () => { // eslint-disable-line complexity --color Force color output --no-color Disable color output --reset-cache Reset AVA's compilation cache and exit + --config JavaScript file for AVA to read its config from, instead of using package.json + or ava.config.js files Examples ava diff --git a/lib/load-config.js b/lib/load-config.js index 526137421..9ebb45c65 100644 --- a/lib/load-config.js +++ b/lib/load-config.js @@ -7,11 +7,23 @@ const pkgConf = require('pkg-conf'); const NO_SUCH_FILE = Symbol('no ava.config.js file'); const MISSING_DEFAULT_EXPORT = Symbol('missing default export'); -function loadConfig(resolveFrom = process.cwd(), defaults = {}) { - const packageConf = pkgConf.sync('ava', {cwd: resolveFrom}); +function loadConfig({configFile, resolveFrom = process.cwd(), defaults = {}} = {}) { // eslint-disable-line complexity + let packageConf = pkgConf.sync('ava', {cwd: resolveFrom}); const filepath = pkgConf.filepath(packageConf); const projectDir = filepath === null ? resolveFrom : path.dirname(filepath); + const fileForErrorMessage = configFile || 'ava.config.js'; + const allowConflictWithPackageJson = Boolean(configFile); + + if (configFile) { + configFile = path.resolve(configFile); // Relative to CWD + if (path.basename(configFile) !== path.relative(projectDir, configFile)) { + throw new Error('Config files must be located next to the package.json file'); + } + } else { + configFile = path.join(projectDir, 'ava.config.js'); + } + let fileConf; try { ({default: fileConf = MISSING_DEFAULT_EXPORT} = esm(module, { @@ -26,45 +38,47 @@ function loadConfig(resolveFrom = process.cwd(), defaults = {}) { }, force: true, mode: 'all' - })(path.join(projectDir, 'ava.config.js'))); + })(configFile)); } catch (error) { if (error && error.code === 'MODULE_NOT_FOUND') { fileConf = NO_SUCH_FILE; } else { - throw Object.assign(new Error('Error loading ava.config.js'), {parent: error}); + throw Object.assign(new Error(`Error loading ${fileForErrorMessage}`), {parent: error}); } } if (fileConf === MISSING_DEFAULT_EXPORT) { - throw new Error('ava.config.js must have a default export, using ES module syntax'); + throw new Error(`${fileForErrorMessage} must have a default export, using ES module syntax`); } if (fileConf !== NO_SUCH_FILE) { - if (Object.keys(packageConf).length > 0) { - throw new Error('Conflicting configuration in ava.config.js and package.json'); + if (allowConflictWithPackageJson) { + packageConf = {}; + } else if (Object.keys(packageConf).length > 0) { + throw new Error(`Conflicting configuration in ${fileForErrorMessage} and package.json`); } if (fileConf && typeof fileConf.then === 'function') { // eslint-disable-line promise/prefer-await-to-then - throw new TypeError('ava.config.js must not export a promise'); + throw new TypeError(`${fileForErrorMessage} must not export a promise`); } if (!isPlainObject(fileConf) && typeof fileConf !== 'function') { - throw new TypeError('ava.config.js must export a plain object or factory function'); + throw new TypeError(`${fileForErrorMessage} must export a plain object or factory function`); } if (typeof fileConf === 'function') { fileConf = fileConf({projectDir}); if (fileConf && typeof fileConf.then === 'function') { // eslint-disable-line promise/prefer-await-to-then - throw new TypeError('Factory method exported by ava.config.js must not return a promise'); + throw new TypeError(`Factory method exported by ${fileForErrorMessage} must not return a promise`); } if (!isPlainObject(fileConf)) { - throw new TypeError('Factory method exported by ava.config.js must return a plain object'); + throw new TypeError(`Factory method exported by ${fileForErrorMessage} must return a plain object`); } } if ('ava' in fileConf) { - throw new Error('Encountered \'ava\' property in ava.config.js; avoid wrapping the configuration'); + throw new Error(`Encountered 'ava' property in ${fileForErrorMessage}; avoid wrapping the configuration`); } } diff --git a/profile.js b/profile.js index 5b13c1152..e9f5ee1ec 100644 --- a/profile.js +++ b/profile.js @@ -32,11 +32,13 @@ function resolveModules(modules) { Promise.longStackTraces(); -const conf = loadConfig(undefined, { - babel: { - testOptions: {} - }, - compileEnhancements: true +const conf = loadConfig({ + defaults: { + babel: { + testOptions: {} + }, + compileEnhancements: true + } }); const {projectDir} = conf; diff --git a/test/fixture/load-config/package-yes-explicit-yes/explicit.js b/test/fixture/load-config/package-yes-explicit-yes/explicit.js new file mode 100644 index 000000000..3369bfc60 --- /dev/null +++ b/test/fixture/load-config/package-yes-explicit-yes/explicit.js @@ -0,0 +1,3 @@ +export default { + files: 'package-yes-explicit-yes-test-value' +}; diff --git a/test/fixture/load-config/package-yes-explicit-yes/nested/explicit.js b/test/fixture/load-config/package-yes-explicit-yes/nested/explicit.js new file mode 100644 index 000000000..796807be0 --- /dev/null +++ b/test/fixture/load-config/package-yes-explicit-yes/nested/explicit.js @@ -0,0 +1,3 @@ +export default { + files: 'package-yes-explicit-yes-nested-test-value' +}; diff --git a/test/fixture/load-config/package-yes-explicit-yes/package.json b/test/fixture/load-config/package-yes-explicit-yes/package.json new file mode 100644 index 000000000..2ae3bb0fd --- /dev/null +++ b/test/fixture/load-config/package-yes-explicit-yes/package.json @@ -0,0 +1,8 @@ +{ + "ava": { + "files": [ + "abc", + "!xyz" + ] + } +} diff --git a/test/load-config.js b/test/load-config.js index ed68fc475..8769fb5bd 100644 --- a/test/load-config.js +++ b/test/load-config.js @@ -23,14 +23,27 @@ test('finds config in package.json', t => { test('loads config from a particular directory', t => { changeDir('throws'); - const conf = loadConfig(path.resolve(__dirname, 'fixture', 'load-config', 'package-only')); + const conf = loadConfig({resolveFrom: path.resolve(__dirname, 'fixture', 'load-config', 'package-only')}); t.is(conf.failFast, true); t.end(); }); -test('throws a warning of both configs are present', t => { +test('throws a warning if both configs are present', t => { changeDir('package-yes-file-yes'); - t.throws(loadConfig); + t.throws(loadConfig, /Conflicting configuration in ava.config.js and package.json/); + t.end(); +}); + +test('explicit configFile option overrides package.json config', t => { + changeDir('package-yes-explicit-yes'); + const {files} = loadConfig({configFile: 'explicit.js'}); + t.is(files, 'package-yes-explicit-yes-test-value'); + t.end(); +}); + +test('throws if configFile option is not in the same directory as the package.json file', t => { + changeDir('package-yes-explicit-yes'); + t.throws(() => loadConfig({configFile: 'nested/explicit.js'}), /Config files must be located next to the package.json file/); t.end(); }); @@ -39,7 +52,7 @@ test('merges in defaults passed with initial call', t => { const defaults = { files: ['123', '!456'] }; - const {files, failFast} = loadConfig(undefined, defaults); + const {files, failFast} = loadConfig({defaults}); t.is(failFast, true, 'preserves original props'); t.is(files, defaults.files, 'merges in extra props'); t.end(); @@ -68,25 +81,25 @@ test('supports require() inside config file', t => { test('throws an error if a config factory returns a promise', t => { changeDir('factory-no-promise-return'); - t.throws(loadConfig); + t.throws(loadConfig, /Factory method exported by ava.config.js must not return a promise/); t.end(); }); test('throws an error if a config exports a promise', t => { changeDir('no-promise-config'); - t.throws(loadConfig); + t.throws(loadConfig, /ava.config.js must not export a promise/); t.end(); }); test('throws an error if a config factory does not return a plain object', t => { changeDir('factory-no-plain-return'); - t.throws(loadConfig); + t.throws(loadConfig, /Factory method exported by ava.config.js must return a plain object/); t.end(); }); test('throws an error if a config does not export a plain object', t => { changeDir('no-plain-config'); - t.throws(loadConfig); + t.throws(loadConfig, /ava.config.js must export a plain object or factory function/); t.end(); }); @@ -109,12 +122,12 @@ test('rethrows wrapped module errors', t => { test('throws an error if a config file has no default export', t => { changeDir('no-default-export'); - t.throws(loadConfig); + t.throws(loadConfig, /ava.config.js must have a default export, using ES module syntax/); t.end(); }); test('throws an error if a config file contains `ava` property', t => { changeDir('contains-ava-property'); - t.throws(loadConfig); + t.throws(loadConfig, /Encountered 'ava' property in ava.config.js; avoid wrapping the configuration/); t.end(); });