Skip to content

Commit 5c9dbb9

Browse files
Experimentally configure module formats for test files
Fixes #2345. Co-authored-by: Mark Wubben <mark@novemberborn.net>
1 parent 2b41fb0 commit 5c9dbb9

30 files changed

+345
-11
lines changed

ava.config.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1+
const skipTests = [];
2+
if (process.versions.node < '12.14.0') {
3+
skipTests.push('!test/configurable-module-format/module.js');
4+
}
5+
16
export default {
2-
files: ['test/**', '!test/**/{fixtures,helpers}/**'],
7+
files: ['test/**', '!test/**/{fixtures,helpers}/**', ...skipTests],
38
ignoredByWatcher: ['{coverage,docs,media,test-d,test-tap}/**']
49
};

docs/06-configuration.md

+22-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Arguments passed to the CLI will always take precedence over the CLI options con
5252
- `tap`: if `true`, enables the [TAP reporter](./05-command-line.md#tap-reporter)
5353
- `verbose`: if `true`, enables verbose output
5454
- `snapshotDir`: specifies a fixed location for storing snapshot files. Use this if your snapshots are ending up in the wrong location
55-
- `extensions`: extensions of test files. Setting this overrides the default `["cjs", "mjs", "js"]` value, so make sure to include those extensions in the list
55+
- `extensions`: extensions of test files. Setting this overrides the default `["cjs", "mjs", "js"]` value, so make sure to include those extensions in the list. [Experimentally you can configure how files are loaded](#configuring-module-formats)
5656
- `require`: extra modules to require before tests are run. Modules are required in the [worker processes](./01-writing-tests.md#process-isolation)
5757
- `timeout`: Timeouts in AVA behave differently than in other test frameworks. AVA resets a timer after each test, forcing tests to quit if no new test results were received within the specified timeout. This can be used to handle stalled tests. See our [timeout documentation](./07-test-timeouts.md) for more options.
5858
- `nodeArguments`: Configure Node.js arguments used to launch worker processes.
@@ -213,6 +213,27 @@ export default {
213213
};
214214
```
215215

216+
### Configuring module formats
217+
218+
Node.js can only load non-standard extension as ES Modules when using [experimental loaders](https://nodejs.org/docs/latest/api/esm.html#esm_experimental_loaders). To use this you'll also have to configure AVA to `import()` your test file.
219+
220+
This is still an experimental feature. You can opt in to it by enabling the `configurableModuleFormat` experiment. Afterwards, you'll be able to specify per-extension module formats using an object form.
221+
222+
As with the array form, you need to explicitly list `js`, `cjs`, and `mjs` extensions. These **must** be set using the `true` value; other extensions are configurable using either `'commonjs'` or `'module'`:
223+
224+
`ava.config.js`:
225+
```js
226+
export default {
227+
nonSemVerExperiments: {
228+
configurableModuleFormat: true
229+
},
230+
extensions: {
231+
js: true,
232+
ts: 'module'
233+
}
234+
};
235+
```
236+
216237
## Node arguments
217238

218239
The `nodeArguments` configuration may be used to specify additional arguments for launching worker processes. These are combined with `--node-arguments` passed on the CLI and any arguments passed to the `node` binary when starting AVA.

lib/cli.js

+8-6
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ exports.run = async () => { // eslint-disable-line complexity
284284
const TapReporter = require('./reporters/tap');
285285
const Watcher = require('./watcher');
286286
const normalizeExtensions = require('./extensions');
287+
const normalizeModuleTypes = require('./module-types');
287288
const {normalizeGlobs, normalizePattern} = require('./globs');
288289
const normalizeNodeArguments = require('./node-arguments');
289290
const validateEnvironmentVariables = require('./environment-variables');
@@ -301,12 +302,6 @@ exports.run = async () => { // eslint-disable-line complexity
301302

302303
const {type: defaultModuleType = 'commonjs'} = pkg || {};
303304

304-
const moduleTypes = {
305-
cjs: 'commonjs',
306-
mjs: 'module',
307-
js: defaultModuleType
308-
};
309-
310305
const providers = [];
311306
if (Reflect.has(conf, 'babel')) {
312307
try {
@@ -348,6 +343,13 @@ exports.run = async () => { // eslint-disable-line complexity
348343
exit(error.message);
349344
}
350345

346+
let moduleTypes;
347+
try {
348+
moduleTypes = normalizeModuleTypes(conf.extensions, defaultModuleType, experiments);
349+
} catch (error) {
350+
exit(error.message);
351+
}
352+
351353
let globs;
352354
try {
353355
globs = normalizeGlobs({files: conf.files, ignoredByWatcher: conf.ignoredByWatcher, extensions, providers});

lib/extensions.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ module.exports = (configuredExtensions, providers = []) => {
22
// Combine all extensions possible for testing. Remove duplicate extensions.
33
const duplicates = new Set();
44
const seen = new Set();
5+
6+
const normalize = extensions => Array.isArray(extensions) ? extensions : Object.keys(extensions);
7+
58
const combine = extensions => {
6-
for (const ext of extensions) {
9+
for (const ext of normalize(extensions)) {
710
if (seen.has(ext)) {
811
duplicates.add(ext);
912
} else {

lib/load-config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const pkgConf = require('pkg-conf');
77

88
const NO_SUCH_FILE = Symbol('no ava.config.js file');
99
const MISSING_DEFAULT_EXPORT = Symbol('missing default export');
10-
const EXPERIMENTS = new Set(['disableSnapshotsInHooks', 'reverseTeardowns']);
10+
const EXPERIMENTS = new Set(['configurableModuleFormat', 'disableSnapshotsInHooks', 'reverseTeardowns']);
1111

1212
// *Very* rudimentary support for loading ava.config.js files containing an `export default` statement.
1313
const evaluateJsConfig = configFile => {

lib/module-types.js

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
const requireTrueValue = value => {
2+
if (value !== true) {
3+
throw new TypeError('When specifying module types, use `true` for ’cjs’, ’mjs’ and ’js’ extensions');
4+
}
5+
};
6+
7+
const normalize = (extension, type, defaultModuleType) => {
8+
switch (extension) {
9+
case 'cjs':
10+
requireTrueValue(type);
11+
return 'commonjs';
12+
case 'mjs':
13+
requireTrueValue(type);
14+
return 'module';
15+
case 'js':
16+
requireTrueValue(type);
17+
return defaultModuleType;
18+
default:
19+
if (type !== 'commonjs' && type !== 'module') {
20+
throw new TypeError(`Module type for ’${extension}’ must be ’commonjs’ or ’module’`);
21+
}
22+
23+
return type;
24+
}
25+
};
26+
27+
const deriveFromObject = (extensionsObject, defaultModuleType) => {
28+
const moduleTypes = {};
29+
for (const [extension, type] of Object.entries(extensionsObject)) {
30+
moduleTypes[extension] = normalize(extension, type, defaultModuleType);
31+
}
32+
33+
return moduleTypes;
34+
};
35+
36+
const deriveFromArray = (extensions, defaultModuleType) => {
37+
const moduleTypes = {};
38+
for (const extension of extensions) {
39+
switch (extension) {
40+
case 'cjs':
41+
moduleTypes.cjs = 'commonjs';
42+
break;
43+
case 'mjs':
44+
moduleTypes.mjs = 'module';
45+
break;
46+
case 'js':
47+
moduleTypes.js = defaultModuleType;
48+
break;
49+
default:
50+
moduleTypes[extension] = 'commonjs';
51+
}
52+
}
53+
54+
return moduleTypes;
55+
};
56+
57+
module.exports = (configuredExtensions, defaultModuleType, experiments) => {
58+
if (configuredExtensions === undefined) {
59+
return {
60+
cjs: 'commonjs',
61+
mjs: 'module',
62+
js: defaultModuleType
63+
};
64+
}
65+
66+
if (Array.isArray(configuredExtensions)) {
67+
return deriveFromArray(configuredExtensions, defaultModuleType);
68+
}
69+
70+
if (!experiments.configurableModuleFormat) {
71+
throw new Error('You must enable the `configurableModuleFormat` experiment in order to specify module types');
72+
}
73+
74+
return deriveFromObject(configuredExtensions, defaultModuleType);
75+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const test = require('@ava/test');
2+
const exec = require('../helpers/exec');
3+
4+
test('load js and cjs as commonjs (default configuration)', async t => {
5+
const result = await exec.fixture(['*.js', '*.cjs']);
6+
const files = new Set(result.stats.passed.map(({file}) => file));
7+
t.is(files.size, 2);
8+
t.true(files.has('test.cjs'));
9+
t.true(files.has('test.js'));
10+
});
11+
12+
test('load js and cjs as commonjs (using an extensions array)', async t => {
13+
const result = await exec.fixture(['*.js', '*.cjs', '--config', 'array-extensions.config.js']);
14+
const files = new Set(result.stats.passed.map(({file}) => file));
15+
t.is(files.size, 2);
16+
t.true(files.has('test.cjs'));
17+
t.true(files.has('test.js'));
18+
});
19+
20+
test('load js and cjs as commonjs (using an extensions object)', async t => {
21+
const result = await exec.fixture(['*.js', '*.cjs', '--config', 'object-extensions.config.js']);
22+
const files = new Set(result.stats.passed.map(({file}) => file));
23+
t.is(files.size, 2);
24+
t.true(files.has('test.cjs'));
25+
t.true(files.has('test.js'));
26+
});
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const test = require('@ava/test');
2+
const exec = require('../helpers/exec');
3+
4+
test('load ts as commonjs (using an extensions array)', async t => {
5+
const result = await exec.fixture(['*.ts', '--config', 'array-custom.config.js']);
6+
const files = new Set(result.stats.passed.map(({file}) => file));
7+
t.is(files.size, 1);
8+
t.true(files.has('test.ts'));
9+
});
10+
11+
test('load ts as commonjs (using an extensions object)', async t => {
12+
const result = await exec.fixture(['*.ts', '--config', 'object-custom.config.js']);
13+
const files = new Set(result.stats.passed.map(({file}) => file));
14+
t.is(files.size, 1);
15+
t.true(files.has('test.ts'));
16+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const test = require('@ava/test');
2+
const exec = require('../helpers/exec');
3+
4+
const stripLeadingFigures = string => string.replace(/^\W+/, '');
5+
6+
test('opt-in is required', async t => {
7+
const result = await t.throwsAsync(exec.fixture(['--config', 'not-enabled.config.js']));
8+
t.is(result.exitCode, 1);
9+
t.snapshot(stripLeadingFigures(result.stderr.trim()));
10+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
extensions: ['js', 'ts']
3+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
extensions: ['js', 'cjs', 'mjs']
3+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export default {
2+
extensions: {
3+
js: true,
4+
ts: 'cjs'
5+
},
6+
nonSemVerExperiments: {
7+
configurableModuleFormat: true
8+
}
9+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default {
2+
extensions: {
3+
cjs: 'module'
4+
},
5+
nonSemVerExperiments: {
6+
configurableModuleFormat: true
7+
}
8+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default {
2+
extensions: {
3+
js: 'module'
4+
},
5+
nonSemVerExperiments: {
6+
configurableModuleFormat: true
7+
}
8+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default {
2+
extensions: {
3+
mjs: 'commonjs'
4+
},
5+
nonSemVerExperiments: {
6+
configurableModuleFormat: true
7+
}
8+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default {
2+
extensions: {
3+
js: true,
4+
cjs: true,
5+
mjs: true
6+
}
7+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export default {
2+
extensions: {
3+
js: true,
4+
ts: 'commonjs'
5+
},
6+
nonSemVerExperiments: {
7+
configurableModuleFormat: true
8+
}
9+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export default {
2+
extensions: {
3+
js: true,
4+
cjs: true,
5+
mjs: true
6+
},
7+
nonSemVerExperiments: {
8+
configurableModuleFormat: true
9+
}
10+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const test = require('ava');
2+
3+
test('always passing test', t => {
4+
t.pass();
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const test = require('ava');
2+
3+
test('always passing test', t => {
4+
t.pass();
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import test from 'ava';
2+
3+
test('always passing test', t => {
4+
t.pass();
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// eslint-disable-next-line ava/no-ignored-test-files
2+
const test = require('ava');
3+
4+
test('always passing test', t => {
5+
const numberWithTypes = 0;
6+
7+
t.is(numberWithTypes, 0);
8+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const test = require('@ava/test');
2+
const exec = require('../helpers/exec');
3+
4+
const stripLeadingFigures = string => string.replace(/^\W+/, '');
5+
6+
test('cannot configure how js extensions should be loaded', async t => {
7+
const result = await t.throwsAsync(exec.fixture(['--config', 'change-js-loading.config.js']));
8+
t.snapshot(stripLeadingFigures(result.stderr.trim()));
9+
});
10+
11+
test('cannot configure how cjs extensions should be loaded', async t => {
12+
const result = await t.throwsAsync(exec.fixture(['--config', 'change-cjs-loading.config.js']));
13+
t.snapshot(stripLeadingFigures(result.stderr.trim()));
14+
});
15+
16+
test('cannot configure how mjs extensions should be loaded', async t => {
17+
const result = await t.throwsAsync(exec.fixture(['--config', 'change-mjs-loading.config.js']));
18+
t.snapshot(stripLeadingFigures(result.stderr.trim()));
19+
});
20+
21+
test('custom extensions must be either commonjs or module', async t => {
22+
const result = await t.throwsAsync(exec.fixture(['--config', 'bad-custom-type.config.js']));
23+
t.snapshot(stripLeadingFigures(result.stderr.trim()));
24+
});
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const test = require('@ava/test');
2+
const exec = require('../helpers/exec');
3+
4+
test('load mjs as module (default configuration)', async t => {
5+
const result = await exec.fixture(['*.mjs']);
6+
const files = new Set(result.stats.passed.map(({file}) => file));
7+
t.is(files.size, 1);
8+
t.true(files.has('test.mjs'));
9+
});
10+
11+
test('load mjs as module (using an extensions array)', async t => {
12+
const result = await exec.fixture(['*.mjs', '--config', 'array-extensions.config.js']);
13+
const files = new Set(result.stats.passed.map(({file}) => file));
14+
t.is(files.size, 1);
15+
t.true(files.has('test.mjs'));
16+
});
17+
18+
test('load mjs as module (using an extensions object)', async t => {
19+
const result = await exec.fixture(['*.mjs', '--config', 'object-extensions.config.js']);
20+
const files = new Set(result.stats.passed.map(({file}) => file));
21+
t.is(files.size, 1);
22+
t.true(files.has('test.mjs'));
23+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Snapshot report for `test/configurable-module-format/experimental.js`
2+
3+
The actual snapshot is saved in `experimental.js.snap`.
4+
5+
Generated by [AVA](https://avajs.dev).
6+
7+
## opt-in is required
8+
9+
> Snapshot 1
10+
11+
'You must enable the `configurableModuleFormat` experiment in order to specify module types'
Binary file not shown.

0 commit comments

Comments
 (0)