Skip to content

Commit a2f2614

Browse files
authored
Next-generation configuration loading
* 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.
1 parent 711bcf2 commit a2f2614

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+796
-89
lines changed

ava.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const skipTests = [];
22
if (process.versions.node < '12.17.0') {
33
skipTests.push(
4+
'!test/config/next-gen.js',
45
'!test/configurable-module-format/module.js',
56
'!test/shared-workers/!(requires-newish-node)/**'
67
);

docs/06-configuration.md

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,13 @@ To use these files:
7373
2. Your `package.json` must not contain an `ava` property (or, if it does, it must be an empty object)
7474
3. You must not both have an `ava.config.js` *and* an `ava.config.cjs` file
7575

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

7878
### `ava.config.js`
7979

80-
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.
80+
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.
81+
82+
This is changing in AVA 4, [see below](#next-generation-configuration).
8183

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

@@ -111,7 +113,7 @@ export default ({projectDir}) => {
111113
};
112114
```
113115

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

116118
### `ava.config.cjs`
117119

@@ -149,12 +151,14 @@ module.exports = ({projectDir}) => {
149151
};
150152
```
151153

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

154156
## Alternative configuration files
155157

156158
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.
157159

160+
AVA 4 also supports `.mjs` extensions, [see below](#next-generation-configuration).
161+
158162
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.
159163

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

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

189+
## Next generation configuration
190+
191+
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:
192+
193+
```js
194+
export default {
195+
nonSemVerExperiments: {
196+
nextGenConfig: true
197+
},
198+
files: ['unit-tests/**/*]
199+
};
200+
```
201+
202+
This also allows you to pass an `.mjs` file using the `--config` argument.
203+
204+
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.
205+
206+
You mustn't have an `ava.config.mjs` file next to an `ava.config.js` or `ava.config.cjs` file.
207+
185208
## Object printing depth
186209

187210
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:

eslint-plugin-helper.js

Lines changed: 134 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,25 @@
11
'use strict';
2-
const normalizeExtensions = require('./lib/extensions');
2+
let isMainThread = true;
3+
let supportsWorkers = false;
4+
try {
5+
({isMainThread} = require('worker_threads'));
6+
supportsWorkers = true;
7+
} catch {}
8+
39
const {classify, hasExtension, isHelperish, matches, normalizeFileForMatching, normalizeGlobs, normalizePatterns} = require('./lib/globs');
4-
const loadConfig = require('./lib/load-config');
5-
const providerManager = require('./lib/provider-manager');
610

7-
const configCache = new Map();
8-
const helperCache = new Map();
11+
let resolveGlobs;
12+
let resolveGlobsSync;
913

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

16-
let conf;
17-
let providers;
18-
if (configCache.has(projectDir)) {
19-
({conf, providers} = configCache.get(projectDir));
20-
} else {
21-
conf = loadConfig({resolveFrom: projectDir});
19+
const configCache = new Map();
2220

23-
providers = [];
21+
const collectProviders = ({conf, projectDir}) => {
22+
const providers = [];
2423
if (Reflect.has(conf, 'babel')) {
2524
const {level, main} = providerManager.babel(projectDir);
2625
providers.push({
@@ -39,12 +38,125 @@ function load(projectDir, overrides) {
3938
});
4039
}
4140

42-
configCache.set(projectDir, {conf, providers});
41+
return providers;
42+
};
43+
44+
const buildGlobs = ({conf, providers, projectDir, overrideExtensions, overrideFiles}) => {
45+
const extensions = overrideExtensions ?
46+
normalizeExtensions(overrideExtensions) :
47+
normalizeExtensions(conf.extensions, providers);
48+
49+
return {
50+
cwd: projectDir,
51+
...normalizeGlobs({
52+
extensions,
53+
files: overrideFiles ? overrideFiles : conf.files,
54+
providers
55+
})
56+
};
57+
};
58+
59+
resolveGlobsSync = (projectDir, overrideExtensions, overrideFiles) => {
60+
if (!configCache.has(projectDir)) {
61+
const conf = loadConfigSync({resolveFrom: projectDir});
62+
const providers = collectProviders({conf, projectDir});
63+
configCache.set(projectDir, {conf, providers});
64+
}
65+
66+
const {conf, providers} = configCache.get(projectDir);
67+
return buildGlobs({conf, providers, projectDir, overrideExtensions, overrideFiles});
68+
};
69+
70+
resolveGlobs = async (projectDir, overrideExtensions, overrideFiles) => {
71+
if (!configCache.has(projectDir)) {
72+
configCache.set(projectDir, loadConfig({resolveFrom: projectDir}).then(conf => { // eslint-disable-line promise/prefer-await-to-then
73+
const providers = collectProviders({conf, projectDir});
74+
return {conf, providers};
75+
}));
76+
}
77+
78+
const {conf, providers} = await configCache.get(projectDir);
79+
return buildGlobs({conf, providers, projectDir, overrideExtensions, overrideFiles});
80+
};
81+
}
82+
83+
if (supportsWorkers) {
84+
const v8 = require('v8');
85+
86+
const MAX_DATA_LENGTH_EXCLUSIVE = 100 * 1024; // Allocate 100 KiB to exchange globs.
87+
88+
if (isMainThread) {
89+
const {Worker} = require('worker_threads');
90+
let data;
91+
let sync;
92+
let worker;
93+
94+
resolveGlobsSync = (projectDir, overrideExtensions, overrideFiles) => {
95+
if (worker === undefined) {
96+
const dataBuffer = new SharedArrayBuffer(MAX_DATA_LENGTH_EXCLUSIVE);
97+
data = new Uint8Array(dataBuffer);
98+
99+
const syncBuffer = new SharedArrayBuffer(4);
100+
sync = new Int32Array(syncBuffer);
101+
102+
worker = new Worker(__filename, {
103+
workerData: {
104+
dataBuffer,
105+
syncBuffer,
106+
firstMessage: {projectDir, overrideExtensions, overrideFiles}
107+
}
108+
});
109+
worker.unref();
110+
} else {
111+
worker.postMessage({projectDir, overrideExtensions, overrideFiles});
112+
}
113+
114+
Atomics.wait(sync, 0, 0);
115+
116+
const byteLength = Atomics.exchange(sync, 0, 0);
117+
if (byteLength === MAX_DATA_LENGTH_EXCLUSIVE) {
118+
throw new Error('Globs are over 100 KiB and cannot be resolved');
119+
}
120+
121+
const globsOrError = v8.deserialize(data.slice(0, byteLength));
122+
if (globsOrError instanceof Error) {
123+
throw globsOrError;
124+
}
125+
126+
return globsOrError;
127+
};
128+
} else {
129+
const {parentPort, workerData} = require('worker_threads');
130+
const data = new Uint8Array(workerData.dataBuffer);
131+
const sync = new Int32Array(workerData.syncBuffer);
132+
133+
const handleMessage = async ({projectDir, overrideExtensions, overrideFiles}) => {
134+
let encoded;
135+
try {
136+
const globs = await resolveGlobs(projectDir, overrideExtensions, overrideFiles);
137+
encoded = v8.serialize(globs);
138+
} catch (error) {
139+
encoded = v8.serialize(error);
140+
}
141+
142+
const byteLength = encoded.length < MAX_DATA_LENGTH_EXCLUSIVE ? encoded.copy(data) : MAX_DATA_LENGTH_EXCLUSIVE;
143+
Atomics.store(sync, 0, byteLength);
144+
Atomics.notify(sync, 0);
145+
};
146+
147+
parentPort.on('message', handleMessage);
148+
handleMessage(workerData.firstMessage);
149+
delete workerData.firstMessage;
43150
}
151+
}
152+
153+
const helperCache = new Map();
44154

45-
const extensions = overrides && overrides.extensions ?
46-
normalizeExtensions(overrides.extensions) :
47-
normalizeExtensions(conf.extensions, providers);
155+
function load(projectDir, overrides) {
156+
const cacheKey = `${JSON.stringify(overrides)}\n${projectDir}`;
157+
if (helperCache.has(cacheKey)) {
158+
return helperCache.get(cacheKey);
159+
}
48160

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

58-
const globs = {
59-
cwd: projectDir,
60-
...normalizeGlobs({
61-
extensions,
62-
files: overrides && overrides.files ? overrides.files : conf.files,
63-
providers
64-
})
65-
};
170+
const globs = resolveGlobsSync(projectDir, overrides && overrides.extensions, overrides && overrides.files);
66171

67172
const classifyForESLint = file => {
68173
const {isTest} = classify(file, globs);

lib/cli.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const arrify = require('arrify');
77
const yargs = require('yargs');
88
const readPkg = require('read-pkg');
99
const isCi = require('./is-ci');
10-
const loadConfig = require('./load-config');
10+
const {loadConfig} = require('./load-config');
1111

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

0 commit comments

Comments
 (0)