Skip to content

Commit

Permalink
Implement --config flag
Browse files Browse the repository at this point in the history
Fixes #1857.
  • Loading branch information
novemberborn committed Jul 7, 2019
1 parent 58b2350 commit 2dae2bf
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 31 deletions.
2 changes: 2 additions & 0 deletions docs/05-command-line.md
Expand Up @@ -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
Expand Down
33 changes: 32 additions & 1 deletion docs/06-configuration.md
Expand Up @@ -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).

Expand Down Expand Up @@ -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:
Expand All @@ -132,3 +161,5 @@ test('My test', t => {
```

AVA has a minimum depth of `3`.

[CLI]: ./05-command-line.md
2 changes: 1 addition & 1 deletion eslint-plugin-helper.js
Expand Up @@ -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});
}
Expand Down
13 changes: 11 additions & 2 deletions lib/cli.js
Expand Up @@ -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();
Expand All @@ -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;
}
Expand All @@ -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
Expand Down
38 changes: 26 additions & 12 deletions lib/load-config.js
Expand Up @@ -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, {
Expand All @@ -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`);
}
}

Expand Down
12 changes: 7 additions & 5 deletions profile.js
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions test/fixture/load-config/package-yes-explicit-yes/explicit.js
@@ -0,0 +1,3 @@
export default {
files: 'package-yes-explicit-yes-test-value'
};
@@ -0,0 +1,3 @@
export default {
files: 'package-yes-explicit-yes-nested-test-value'
};
@@ -0,0 +1,8 @@
{
"ava": {
"files": [
"abc",
"!xyz"
]
}
}
33 changes: 23 additions & 10 deletions test/load-config.js
Expand Up @@ -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();
});

Expand All @@ -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();
Expand Down Expand Up @@ -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();
});

Expand All @@ -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();
});

0 comments on commit 2dae2bf

Please sign in to comment.