Skip to content

Next-generation configuration loading #2629

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ava.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const skipTests = [];
if (process.versions.node < '12.17.0') {
skipTests.push(
'!test/config/next-gen.js',
'!test/configurable-module-format/module.js',
'!test/shared-workers/!(requires-newish-node)/**'
);
Expand Down
31 changes: 27 additions & 4 deletions docs/06-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,13 @@ To use these files:
2. Your `package.json` must not contain an `ava` property (or, if it does, it must be an empty object)
3. You must not both have an `ava.config.js` *and* an `ava.config.cjs` file

AVA recognizes `ava.config.mjs` files but refuses to load them.
AVA 3 recognizes `ava.config.mjs` files but refuses to load them. This is changing in AVA 4, [see below](#next-generation-configuration).

### `ava.config.js`

For `ava.config.js` files you must use `export default`. You cannot use ["module scope"](https://nodejs.org/docs/latest-v12.x/api/modules.html#modules_the_module_scope). You cannot import dependencies.
In AVA 3, for `ava.config.js` files you must use `export default`. You cannot use ["module scope"](https://nodejs.org/docs/latest-v12.x/api/modules.html#modules_the_module_scope). You cannot import dependencies.

This is changing in AVA 4, [see below](#next-generation-configuration).

The default export can either be a plain object or a factory function which returns a plain object:

Expand Down Expand Up @@ -111,7 +113,7 @@ export default ({projectDir}) => {
};
```

Note that the final configuration must not be a promise.
Note that the final configuration must not be a promise. This is changing in AVA 4, [see below](#next-generation-configuration).

### `ava.config.cjs`

Expand Down Expand Up @@ -149,12 +151,14 @@ module.exports = ({projectDir}) => {
};
```

Note that the final configuration must not be a promise.
Note that the final configuration must not be a promise. This is changing in AVA 4, [see below](#next-generation-configuration).

## Alternative configuration files

The [CLI] lets you specify a specific configuration file, using the `--config` flag. This file must have either a `.js` or `.cjs` extension and is processed like an `ava.config.js` or `ava.config.cjs` file would be.

AVA 4 also supports `.mjs` extensions, [see below](#next-generation-configuration).

When the `--config` flag is set, the provided file will override all configuration from the `package.json` and `ava.config.js` or `ava.config.cjs` files. The configuration is not merged.

The configuration file *must* be in the same directory as the `package.json` file.
Expand Down Expand Up @@ -182,6 +186,25 @@ module.exports = {

You can now run your unit tests through `npx ava` and the integration tests through `npx ava --config integration-tests.config.cjs`.

## Next generation configuration

AVA 4 will add full support for ESM configuration files as well as allowing you to have asynchronous factory functions. If you're using Node.js 12 or later you can opt-in to these features in AVA 3 by enabling the `nextGenConfig` experiment. Say in an `ava.config.mjs` file:

```js
export default {
nonSemVerExperiments: {
nextGenConfig: true
},
files: ['unit-tests/**/*]
};
```

This also allows you to pass an `.mjs` file using the `--config` argument.

With this experiment enabled, AVA will no longer have special treatment for `ava.config.js` files. Instead AVA follows Node.js' behavior, so if you've set [`"type": "module"`](https://nodejs.org/docs/latest/api/packages.html#packages_type) you must use ESM, and otherwise you must use CommonJS.

You mustn't have an `ava.config.mjs` file next to an `ava.config.js` or `ava.config.cjs` file.

## 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 Down
163 changes: 134 additions & 29 deletions eslint-plugin-helper.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
'use strict';
const normalizeExtensions = require('./lib/extensions');
let isMainThread = true;
let supportsWorkers = false;
try {
({isMainThread} = require('worker_threads'));
supportsWorkers = true;
} catch {}

const {classify, hasExtension, isHelperish, matches, normalizeFileForMatching, normalizeGlobs, normalizePatterns} = require('./lib/globs');
const loadConfig = require('./lib/load-config');
const providerManager = require('./lib/provider-manager');

const configCache = new Map();
const helperCache = new Map();
let resolveGlobs;
let resolveGlobsSync;

function load(projectDir, overrides) {
const cacheKey = `${JSON.stringify(overrides)}\n${projectDir}`;
if (helperCache.has(cacheKey)) {
return helperCache.get(cacheKey);
}
if (!supportsWorkers || !isMainThread) {
const normalizeExtensions = require('./lib/extensions');
const {loadConfig, loadConfigSync} = require('./lib/load-config');
const providerManager = require('./lib/provider-manager');

let conf;
let providers;
if (configCache.has(projectDir)) {
({conf, providers} = configCache.get(projectDir));
} else {
conf = loadConfig({resolveFrom: projectDir});
const configCache = new Map();

providers = [];
const collectProviders = ({conf, projectDir}) => {
const providers = [];
if (Reflect.has(conf, 'babel')) {
const {level, main} = providerManager.babel(projectDir);
providers.push({
Expand All @@ -39,12 +38,125 @@ function load(projectDir, overrides) {
});
}

configCache.set(projectDir, {conf, providers});
return providers;
};

const buildGlobs = ({conf, providers, projectDir, overrideExtensions, overrideFiles}) => {
const extensions = overrideExtensions ?
normalizeExtensions(overrideExtensions) :
normalizeExtensions(conf.extensions, providers);

return {
cwd: projectDir,
...normalizeGlobs({
extensions,
files: overrideFiles ? overrideFiles : conf.files,
providers
})
};
};

resolveGlobsSync = (projectDir, overrideExtensions, overrideFiles) => {
if (!configCache.has(projectDir)) {
const conf = loadConfigSync({resolveFrom: projectDir});
const providers = collectProviders({conf, projectDir});
configCache.set(projectDir, {conf, providers});
}

const {conf, providers} = configCache.get(projectDir);
return buildGlobs({conf, providers, projectDir, overrideExtensions, overrideFiles});
};

resolveGlobs = async (projectDir, overrideExtensions, overrideFiles) => {
if (!configCache.has(projectDir)) {
configCache.set(projectDir, loadConfig({resolveFrom: projectDir}).then(conf => { // eslint-disable-line promise/prefer-await-to-then
const providers = collectProviders({conf, projectDir});
return {conf, providers};
}));
}

const {conf, providers} = await configCache.get(projectDir);
return buildGlobs({conf, providers, projectDir, overrideExtensions, overrideFiles});
};
}

if (supportsWorkers) {
const v8 = require('v8');

const MAX_DATA_LENGTH_EXCLUSIVE = 100 * 1024; // Allocate 100 KiB to exchange globs.

if (isMainThread) {
const {Worker} = require('worker_threads');
let data;
let sync;
let worker;

resolveGlobsSync = (projectDir, overrideExtensions, overrideFiles) => {
if (worker === undefined) {
const dataBuffer = new SharedArrayBuffer(MAX_DATA_LENGTH_EXCLUSIVE);
data = new Uint8Array(dataBuffer);

const syncBuffer = new SharedArrayBuffer(4);
sync = new Int32Array(syncBuffer);

worker = new Worker(__filename, {
workerData: {
dataBuffer,
syncBuffer,
firstMessage: {projectDir, overrideExtensions, overrideFiles}
}
});
worker.unref();
} else {
worker.postMessage({projectDir, overrideExtensions, overrideFiles});
}

Atomics.wait(sync, 0, 0);

const byteLength = Atomics.exchange(sync, 0, 0);
if (byteLength === MAX_DATA_LENGTH_EXCLUSIVE) {
throw new Error('Globs are over 100 KiB and cannot be resolved');
}

const globsOrError = v8.deserialize(data.slice(0, byteLength));
if (globsOrError instanceof Error) {
throw globsOrError;
}

return globsOrError;
};
} else {
const {parentPort, workerData} = require('worker_threads');
const data = new Uint8Array(workerData.dataBuffer);
const sync = new Int32Array(workerData.syncBuffer);

const handleMessage = async ({projectDir, overrideExtensions, overrideFiles}) => {
let encoded;
try {
const globs = await resolveGlobs(projectDir, overrideExtensions, overrideFiles);
encoded = v8.serialize(globs);
} catch (error) {
encoded = v8.serialize(error);
}

const byteLength = encoded.length < MAX_DATA_LENGTH_EXCLUSIVE ? encoded.copy(data) : MAX_DATA_LENGTH_EXCLUSIVE;
Atomics.store(sync, 0, byteLength);
Atomics.notify(sync, 0);
};

parentPort.on('message', handleMessage);
handleMessage(workerData.firstMessage);
delete workerData.firstMessage;
}
}

const helperCache = new Map();

const extensions = overrides && overrides.extensions ?
normalizeExtensions(overrides.extensions) :
normalizeExtensions(conf.extensions, providers);
function load(projectDir, overrides) {
const cacheKey = `${JSON.stringify(overrides)}\n${projectDir}`;
if (helperCache.has(cacheKey)) {
return helperCache.get(cacheKey);
}

let helperPatterns = [];
if (overrides && overrides.helpers !== undefined) {
Expand All @@ -55,14 +167,7 @@ function load(projectDir, overrides) {
helperPatterns = normalizePatterns(overrides.helpers);
}

const globs = {
cwd: projectDir,
...normalizeGlobs({
extensions,
files: overrides && overrides.files ? overrides.files : conf.files,
providers
})
};
const globs = resolveGlobsSync(projectDir, overrides && overrides.extensions, overrides && overrides.files);

const classifyForESLint = file => {
const {isTest} = classify(file, globs);
Expand Down
4 changes: 2 additions & 2 deletions lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const arrify = require('arrify');
const yargs = require('yargs');
const readPkg = require('read-pkg');
const isCi = require('./is-ci');
const loadConfig = require('./load-config');
const {loadConfig} = require('./load-config');

function exit(message) {
console.error(`\n ${require('./chalk').get().red(figures.cross)} ${message}`);
Expand Down Expand Up @@ -83,7 +83,7 @@ exports.run = async () => { // eslint-disable-line complexity
let confError = null;
try {
const {argv: {config: configFile}} = yargs.help(false);
conf = loadConfig({configFile});
conf = await loadConfig({configFile});
} catch (error) {
confError = error;
}
Expand Down
Loading