Skip to content

Commit 3533aba

Browse files
greym0uthnovemberborn
authored andcommitted
Make test & helper file extensions configurable
Allow configuration of test & helper file extensions. Files will be recognized when searching for tests. Users can choose whether the files should participate in the Babel precompilation.
1 parent f0f0c3b commit 3533aba

File tree

37 files changed

+485
-48
lines changed

37 files changed

+485
-48
lines changed

api.js

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const path = require('path');
33
const fs = require('fs');
44
const os = require('os');
55
const commonPathPrefix = require('common-path-prefix');
6+
const escapeStringRegexp = require('escape-string-regexp');
67
const uniqueTempDir = require('unique-temp-dir');
78
const isCi = require('is-ci');
89
const resolveCwd = require('resolve-cwd');
@@ -38,6 +39,8 @@ class Api extends Emittery {
3839
this.options = Object.assign({match: []}, options);
3940
this.options.require = resolveModules(this.options.require);
4041

42+
this._allExtensions = this.options.extensions.all;
43+
this._regexpFullExtensions = new RegExp(`\\.(${this.options.extensions.full.map(ext => escapeStringRegexp(ext)).join('|')})$`);
4144
this._precompiler = null;
4245
}
4346

@@ -78,7 +81,7 @@ class Api extends Emittery {
7881
}
7982

8083
// Find all test files.
81-
return new AvaFiles({cwd: apiOptions.resolveTestsFrom, files}).findTestFiles()
84+
return new AvaFiles({cwd: apiOptions.resolveTestsFrom, files, extensions: this._allExtensions}).findTestFiles()
8285
.then(files => {
8386
runStatus = new RunStatus(files.length);
8487

@@ -122,31 +125,35 @@ class Api extends Emittery {
122125
return emittedRun
123126
.then(() => this._setupPrecompiler())
124127
.then(precompilation => {
125-
if (!precompilation.precompileFile) {
128+
if (!precompilation.enabled) {
126129
return null;
127130
}
128131

129132
// Compile all test and helper files. Assumes the tests only load
130133
// helpers from within the `resolveTestsFrom` directory. Without
131134
// arguments this is the `projectDir`, else it's `process.cwd()`
132135
// which may be nested too deeply.
133-
return new AvaFiles({cwd: this.options.resolveTestsFrom}).findTestHelpers().then(helpers => {
134-
return {
135-
cacheDir: precompilation.cacheDir,
136-
map: [...files, ...helpers].reduce((acc, file) => {
137-
try {
138-
const realpath = fs.realpathSync(file);
139-
const cachePath = precompilation.precompileFile(realpath);
140-
if (cachePath) {
141-
acc[realpath] = cachePath;
136+
return new AvaFiles({cwd: this.options.resolveTestsFrom, extensions: this._allExtensions})
137+
.findTestHelpers().then(helpers => {
138+
return {
139+
cacheDir: precompilation.cacheDir,
140+
map: [...files, ...helpers].reduce((acc, file) => {
141+
try {
142+
const realpath = fs.realpathSync(file);
143+
const filename = path.basename(realpath);
144+
const cachePath = this._regexpFullExtensions.test(filename) ?
145+
precompilation.precompileFull(realpath) :
146+
precompilation.precompileEnhancementsOnly(realpath);
147+
if (cachePath) {
148+
acc[realpath] = cachePath;
149+
}
150+
} catch (err) {
151+
throw Object.assign(err, {file});
142152
}
143-
} catch (err) {
144-
throw Object.assign(err, {file});
145-
}
146-
return acc;
147-
}, {})
148-
};
149-
});
153+
return acc;
154+
}, {})
155+
};
156+
});
150157
})
151158
.then(precompilation => {
152159
// Resolve the correct concurrency value.
@@ -218,10 +225,24 @@ class Api extends Emittery {
218225
// Ensure cacheDir exists
219226
makeDir.sync(cacheDir);
220227

221-
const {projectDir, babelConfig, compileEnhancements} = this.options;
228+
const {projectDir, babelConfig} = this.options;
229+
const compileEnhancements = this.options.compileEnhancements !== false;
230+
const precompileFull = babelConfig ?
231+
babelPipeline.build(projectDir, cacheDir, babelConfig, compileEnhancements) :
232+
filename => {
233+
throw new Error(`Cannot apply full precompilation, possible bad usage: ${filename}`);
234+
};
235+
const precompileEnhancementsOnly = compileEnhancements && this.options.extensions.enhancementsOnly.length > 0 ?
236+
babelPipeline.build(projectDir, cacheDir, null, compileEnhancements) :
237+
filename => {
238+
throw new Error(`Cannot apply enhancement-only precompilation, possible bad usage: ${filename}`);
239+
};
240+
222241
this._precompiler = {
223242
cacheDir,
224-
precompileFile: babelPipeline.build(projectDir, cacheDir, babelConfig, compileEnhancements !== false)
243+
enabled: babelConfig || compileEnhancements,
244+
precompileEnhancementsOnly,
245+
precompileFull
225246
};
226247
return this._precompiler;
227248
}

docs/recipes/babel.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,25 @@ Instead run the following to reset AVA's cache when you change the configuration
3737
$ npx ava --reset-cache
3838
```
3939

40+
## Add additional extensions
41+
42+
You can configure AVA to recognize additional file extensions and compile those test & helper files using Babel:
43+
44+
```json
45+
{
46+
"ava": {
47+
"babel": {
48+
"extensions": [
49+
"js",
50+
"jsx"
51+
]
52+
}
53+
}
54+
}
55+
```
56+
57+
See also AVA's [`extensions` option](../../readme.md#options).
58+
4059
## Make AVA skip your project's Babel options
4160

4261
You may not want AVA to use your project's Babel options, for example if your project is relying on Babel 6. You can set the `babelrc` option to `false`:

docs/recipes/es-modules.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,43 @@ test('2 + 2 = 4', t => {
4545
});
4646
```
4747

48-
Note that test files still need to use the `.js` extension.
48+
You need to configure AVA to recognize `.mjs` extensions. If you want AVA to apply its Babel presets use:
49+
50+
```json
51+
{
52+
"ava": {
53+
"babel": {
54+
"extensions": [
55+
"js",
56+
"mjs"
57+
]
58+
}
59+
}
60+
}
61+
```
62+
63+
Alternatively you can use:
64+
65+
```json
66+
{
67+
"ava": {
68+
"babel": false,
69+
"extensions": [
70+
"js",
71+
"mjs"
72+
]
73+
}
74+
}
75+
```
76+
77+
Or leave Babel enabled (which means it's applied to `.js` files), but don't apply it to `.mjs` files:
78+
79+
```json
80+
{
81+
"ava": {
82+
"extensions": [
83+
"mjs"
84+
]
85+
}
86+
}
87+
```

lib/ava-files.js

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const autoBind = require('auto-bind');
99
const defaultIgnore = require('ignore-by-default').directories();
1010
const multimatch = require('multimatch');
1111

12-
function handlePaths(files, excludePatterns, globOptions) {
12+
function handlePaths(files, extensions, excludePatterns, globOptions) {
1313
// Convert Promise to Bluebird
1414
files = Promise.resolve(globby(files.concat(excludePatterns), globOptions));
1515

@@ -42,22 +42,23 @@ function handlePaths(files, excludePatterns, globOptions) {
4242

4343
searchedParents.add(file);
4444

45-
let pattern = path.join(file, '**', '*.js');
45+
let pattern = path.join(file, '**', `*.${extensions.length === 1 ?
46+
extensions[0] : `{${extensions.join(',')}}`}`);
4647

4748
if (process.platform === 'win32') {
4849
// Always use `/` in patterns, harmonizing matching across platforms
4950
pattern = slash(pattern);
5051
}
5152

52-
return handlePaths([pattern], excludePatterns, globOptions);
53+
return handlePaths([pattern], extensions, excludePatterns, globOptions);
5354
}
5455

5556
// `globby` returns slashes even on Windows. Normalize here so the file
5657
// paths are consistently platform-accurate as tests are run.
5758
return path.normalize(file);
5859
})
5960
.then(flatten)
60-
.filter(file => file && path.extname(file) === '.js')
61+
.filter(file => file && extensions.includes(path.extname(file).substr(1)))
6162
.filter(file => {
6263
if (path.basename(file)[0] === '_' && globOptions.includeUnderscoredFiles !== true) {
6364
return false;
@@ -79,19 +80,19 @@ const defaultExcludePatterns = () => [
7980
'!**/helpers/**'
8081
];
8182

82-
const defaultIncludePatterns = () => [
83-
'test.js',
84-
'test-*.js',
85-
'test',
86-
'**/__tests__',
87-
'**/*.test.js'
83+
const defaultIncludePatterns = extPattern => [
84+
`test.${extPattern}`,
85+
`test-*.${extPattern}`,
86+
'test', // Directory
87+
'**/__tests__', // Directory
88+
`**/*.test.${extPattern}`
8889
];
8990

90-
const defaultHelperPatterns = () => [
91-
'**/__tests__/helpers/**/*.js',
92-
'**/__tests__/**/_*.js',
93-
'**/test/helpers/**/*.js',
94-
'**/test/**/_*.js'
91+
const defaultHelperPatterns = extPattern => [
92+
`**/__tests__/helpers/**/*.${extPattern}`,
93+
`**/__tests__/**/_*.${extPattern}`,
94+
`**/test/helpers/**/*.${extPattern}`,
95+
`**/test/**/_*.${extPattern}`
9596
];
9697

9798
const getDefaultIgnorePatterns = () => defaultIgnore.map(dir => `${dir}/**/*`);
@@ -114,11 +115,13 @@ class AvaFiles {
114115
return file;
115116
});
116117

118+
this.extensions = options.extensions || ['js'];
119+
this.extensionPattern = this.extensions.length === 1 ?
120+
this.extensions[0] : `{${this.extensions.join(',')}}`;
121+
this.excludePatterns = defaultExcludePatterns();
117122
if (files.length === 0) {
118-
files = defaultIncludePatterns();
123+
files = defaultIncludePatterns(this.extensionPattern);
119124
}
120-
121-
this.excludePatterns = defaultExcludePatterns();
122125
this.files = files;
123126
this.sources = options.sources || [];
124127
this.cwd = options.cwd || process.cwd();
@@ -133,15 +136,15 @@ class AvaFiles {
133136
}
134137

135138
findTestFiles() {
136-
return handlePaths(this.files, this.excludePatterns, Object.assign({
139+
return handlePaths(this.files, this.extensions, this.excludePatterns, Object.assign({
137140
cwd: this.cwd,
138141
expandDirectories: false,
139142
nodir: false
140143
}, this.globCaches));
141144
}
142145

143146
findTestHelpers() {
144-
return handlePaths(defaultHelperPatterns(), ['!**/node_modules/**'], Object.assign({
147+
return handlePaths(defaultHelperPatterns(this.extensionPattern), this.extensions, ['!**/node_modules/**'], Object.assign({
145148
cwd: this.cwd,
146149
includeUnderscoredFiles: true,
147150
expandDirectories: false,
@@ -151,7 +154,7 @@ class AvaFiles {
151154

152155
isSource(filePath) {
153156
let mixedPatterns = [];
154-
const defaultIgnorePatterns = getDefaultIgnorePatterns();
157+
const defaultIgnorePatterns = getDefaultIgnorePatterns(this.extensionPattern);
155158
const overrideDefaultIgnorePatterns = [];
156159

157160
let hasPositivePattern = false;

lib/babel-pipeline.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const path = require('path');
44
const writeFileAtomic = require('@ava/write-file-atomic');
55
const babel = require('@babel/core');
66
const convertSourceMap = require('convert-source-map');
7+
const isPlainObject = require('is-plain-object');
78
const md5Hex = require('md5-hex');
89
const packageHash = require('package-hash');
910
const stripBomBuf = require('strip-bom-buf');
@@ -20,6 +21,14 @@ function getSourceMap(filePath, code) {
2021
return sourceMap ? sourceMap.toObject() : undefined;
2122
}
2223

24+
function hasValidKeys(conf) {
25+
return Object.keys(conf).every(key => key === 'extensions' || key === 'testOptions');
26+
}
27+
28+
function isValidExtensions(extensions) {
29+
return Array.isArray(extensions) && extensions.every(ext => typeof ext === 'string' && ext !== '');
30+
}
31+
2332
function validate(conf) {
2433
if (conf === false) {
2534
return null;
@@ -31,11 +40,17 @@ function validate(conf) {
3140
return {testOptions: defaultOptions};
3241
}
3342

34-
if (!conf || typeof conf !== 'object' || !conf.testOptions || typeof conf.testOptions !== 'object' || Array.isArray(conf.testOptions) || Object.keys(conf).length > 1) {
43+
if (
44+
!isPlainObject(conf) ||
45+
!hasValidKeys(conf) ||
46+
(conf.testOptions !== undefined && !isPlainObject(conf.testOptions)) ||
47+
(conf.extensions !== undefined && !isValidExtensions(conf.extensions))
48+
) {
3549
throw new Error(`Unexpected Babel configuration for AVA. See ${chalk.underline('https://github.com/avajs/ava/blob/master/docs/recipes/babel.md')} for allowed values.`);
3650
}
3751

3852
return {
53+
extensions: conf.extensions,
3954
testOptions: Object.assign({}, defaultOptions, conf.testOptions)
4055
};
4156
}

lib/cli.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ exports.run = () => { // eslint-disable-line complexity
149149
const TapReporter = require('./reporters/tap');
150150
const Watcher = require('./watcher');
151151
const babelPipeline = require('./babel-pipeline');
152+
const normalizeExtensions = require('./extensions');
152153

153154
let babelConfig = null;
154155
try {
@@ -157,6 +158,13 @@ exports.run = () => { // eslint-disable-line complexity
157158
exit(err.message);
158159
}
159160

161+
let extensions;
162+
try {
163+
extensions = normalizeExtensions(conf.extensions || [], babelConfig);
164+
} catch (err) {
165+
exit(err.message);
166+
}
167+
160168
// Copy resultant cli.flags into conf for use with Api and elsewhere
161169
Object.assign(conf, cli.flags);
162170

@@ -168,6 +176,7 @@ exports.run = () => { // eslint-disable-line complexity
168176
require: arrify(conf.require),
169177
cacheEnabled: conf.cache !== false,
170178
compileEnhancements: conf.compileEnhancements !== false,
179+
extensions,
171180
match,
172181
babelConfig,
173182
resolveTestsFrom: cli.input.length === 0 ? projectDir : process.cwd(),

0 commit comments

Comments
 (0)