Skip to content

Commit

Permalink
Next-generation configuration loading
Browse files Browse the repository at this point in the history
* When worker threads are available, support asynchronous configuration loading in the ESLint plugin helper

* Experimental implementation of next-generation configuration loading. This adds support for `.mjs` files, fixing #2346. I've removed the special handling of `ava.config.js` files, relying on Node.js to follow the package type instead. We now also support asynchronous factories.
  • Loading branch information
novemberborn committed Jan 1, 2021
1 parent 711bcf2 commit a2f2614
Show file tree
Hide file tree
Showing 46 changed files with 796 additions and 89 deletions.
1 change: 1 addition & 0 deletions ava.config.js
@@ -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
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
@@ -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
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

0 comments on commit a2f2614

Please sign in to comment.