Skip to content

Commit

Permalink
Make test & helper file extensions configurable
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
greym0uth authored and novemberborn committed May 30, 2018
1 parent f0f0c3b commit 3533aba
Show file tree
Hide file tree
Showing 37 changed files with 485 additions and 48 deletions.
61 changes: 41 additions & 20 deletions api.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const path = require('path');
const fs = require('fs');
const os = require('os');
const commonPathPrefix = require('common-path-prefix');
const escapeStringRegexp = require('escape-string-regexp');
const uniqueTempDir = require('unique-temp-dir');
const isCi = require('is-ci');
const resolveCwd = require('resolve-cwd');
Expand Down Expand Up @@ -38,6 +39,8 @@ class Api extends Emittery {
this.options = Object.assign({match: []}, options);
this.options.require = resolveModules(this.options.require);

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

Expand Down Expand Up @@ -78,7 +81,7 @@ class Api extends Emittery {
}

// Find all test files.
return new AvaFiles({cwd: apiOptions.resolveTestsFrom, files}).findTestFiles()
return new AvaFiles({cwd: apiOptions.resolveTestsFrom, files, extensions: this._allExtensions}).findTestFiles()
.then(files => {
runStatus = new RunStatus(files.length);

Expand Down Expand Up @@ -122,31 +125,35 @@ class Api extends Emittery {
return emittedRun
.then(() => this._setupPrecompiler())
.then(precompilation => {
if (!precompilation.precompileFile) {
if (!precompilation.enabled) {
return null;
}

// Compile all test and helper files. Assumes the tests only load
// helpers from within the `resolveTestsFrom` directory. Without
// arguments this is the `projectDir`, else it's `process.cwd()`
// which may be nested too deeply.
return new AvaFiles({cwd: this.options.resolveTestsFrom}).findTestHelpers().then(helpers => {
return {
cacheDir: precompilation.cacheDir,
map: [...files, ...helpers].reduce((acc, file) => {
try {
const realpath = fs.realpathSync(file);
const cachePath = precompilation.precompileFile(realpath);
if (cachePath) {
acc[realpath] = cachePath;
return new AvaFiles({cwd: this.options.resolveTestsFrom, extensions: this._allExtensions})
.findTestHelpers().then(helpers => {
return {
cacheDir: precompilation.cacheDir,
map: [...files, ...helpers].reduce((acc, file) => {
try {
const realpath = fs.realpathSync(file);
const filename = path.basename(realpath);
const cachePath = this._regexpFullExtensions.test(filename) ?
precompilation.precompileFull(realpath) :
precompilation.precompileEnhancementsOnly(realpath);
if (cachePath) {
acc[realpath] = cachePath;
}
} catch (err) {
throw Object.assign(err, {file});
}
} catch (err) {
throw Object.assign(err, {file});
}
return acc;
}, {})
};
});
return acc;
}, {})
};
});
})
.then(precompilation => {
// Resolve the correct concurrency value.
Expand Down Expand Up @@ -218,10 +225,24 @@ class Api extends Emittery {
// Ensure cacheDir exists
makeDir.sync(cacheDir);

const {projectDir, babelConfig, compileEnhancements} = this.options;
const {projectDir, babelConfig} = this.options;
const compileEnhancements = this.options.compileEnhancements !== false;
const precompileFull = babelConfig ?
babelPipeline.build(projectDir, cacheDir, babelConfig, compileEnhancements) :
filename => {
throw new Error(`Cannot apply full precompilation, possible bad usage: ${filename}`);
};
const precompileEnhancementsOnly = compileEnhancements && this.options.extensions.enhancementsOnly.length > 0 ?
babelPipeline.build(projectDir, cacheDir, null, compileEnhancements) :
filename => {
throw new Error(`Cannot apply enhancement-only precompilation, possible bad usage: ${filename}`);
};

this._precompiler = {
cacheDir,
precompileFile: babelPipeline.build(projectDir, cacheDir, babelConfig, compileEnhancements !== false)
enabled: babelConfig || compileEnhancements,
precompileEnhancementsOnly,
precompileFull
};
return this._precompiler;
}
Expand Down
19 changes: 19 additions & 0 deletions docs/recipes/babel.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@ Instead run the following to reset AVA's cache when you change the configuration
$ npx ava --reset-cache
```

## Add additional extensions

You can configure AVA to recognize additional file extensions and compile those test & helper files using Babel:

```json
{
"ava": {
"babel": {
"extensions": [
"js",
"jsx"
]
}
}
}
```

See also AVA's [`extensions` option](../../readme.md#options).

## Make AVA skip your project's Babel options

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`:
Expand Down
41 changes: 40 additions & 1 deletion docs/recipes/es-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,43 @@ test('2 + 2 = 4', t => {
});
```

Note that test files still need to use the `.js` extension.
You need to configure AVA to recognize `.mjs` extensions. If you want AVA to apply its Babel presets use:

```json
{
"ava": {
"babel": {
"extensions": [
"js",
"mjs"
]
}
}
}
```

Alternatively you can use:

```json
{
"ava": {
"babel": false,
"extensions": [
"js",
"mjs"
]
}
}
```

Or leave Babel enabled (which means it's applied to `.js` files), but don't apply it to `.mjs` files:

```json
{
"ava": {
"extensions": [
"mjs"
]
}
}
```
45 changes: 24 additions & 21 deletions lib/ava-files.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const autoBind = require('auto-bind');
const defaultIgnore = require('ignore-by-default').directories();
const multimatch = require('multimatch');

function handlePaths(files, excludePatterns, globOptions) {
function handlePaths(files, extensions, excludePatterns, globOptions) {
// Convert Promise to Bluebird
files = Promise.resolve(globby(files.concat(excludePatterns), globOptions));

Expand Down Expand Up @@ -42,22 +42,23 @@ function handlePaths(files, excludePatterns, globOptions) {

searchedParents.add(file);

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

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

return handlePaths([pattern], excludePatterns, globOptions);
return handlePaths([pattern], extensions, excludePatterns, globOptions);
}

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

const defaultIncludePatterns = () => [
'test.js',
'test-*.js',
'test',
'**/__tests__',
'**/*.test.js'
const defaultIncludePatterns = extPattern => [
`test.${extPattern}`,
`test-*.${extPattern}`,
'test', // Directory
'**/__tests__', // Directory
`**/*.test.${extPattern}`
];

const defaultHelperPatterns = () => [
'**/__tests__/helpers/**/*.js',
'**/__tests__/**/_*.js',
'**/test/helpers/**/*.js',
'**/test/**/_*.js'
const defaultHelperPatterns = extPattern => [
`**/__tests__/helpers/**/*.${extPattern}`,
`**/__tests__/**/_*.${extPattern}`,
`**/test/helpers/**/*.${extPattern}`,
`**/test/**/_*.${extPattern}`
];

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

this.extensions = options.extensions || ['js'];
this.extensionPattern = this.extensions.length === 1 ?
this.extensions[0] : `{${this.extensions.join(',')}}`;
this.excludePatterns = defaultExcludePatterns();
if (files.length === 0) {
files = defaultIncludePatterns();
files = defaultIncludePatterns(this.extensionPattern);
}

this.excludePatterns = defaultExcludePatterns();
this.files = files;
this.sources = options.sources || [];
this.cwd = options.cwd || process.cwd();
Expand All @@ -133,15 +136,15 @@ class AvaFiles {
}

findTestFiles() {
return handlePaths(this.files, this.excludePatterns, Object.assign({
return handlePaths(this.files, this.extensions, this.excludePatterns, Object.assign({
cwd: this.cwd,
expandDirectories: false,
nodir: false
}, this.globCaches));
}

findTestHelpers() {
return handlePaths(defaultHelperPatterns(), ['!**/node_modules/**'], Object.assign({
return handlePaths(defaultHelperPatterns(this.extensionPattern), this.extensions, ['!**/node_modules/**'], Object.assign({
cwd: this.cwd,
includeUnderscoredFiles: true,
expandDirectories: false,
Expand All @@ -151,7 +154,7 @@ class AvaFiles {

isSource(filePath) {
let mixedPatterns = [];
const defaultIgnorePatterns = getDefaultIgnorePatterns();
const defaultIgnorePatterns = getDefaultIgnorePatterns(this.extensionPattern);
const overrideDefaultIgnorePatterns = [];

let hasPositivePattern = false;
Expand Down
17 changes: 16 additions & 1 deletion lib/babel-pipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const path = require('path');
const writeFileAtomic = require('@ava/write-file-atomic');
const babel = require('@babel/core');
const convertSourceMap = require('convert-source-map');
const isPlainObject = require('is-plain-object');
const md5Hex = require('md5-hex');
const packageHash = require('package-hash');
const stripBomBuf = require('strip-bom-buf');
Expand All @@ -20,6 +21,14 @@ function getSourceMap(filePath, code) {
return sourceMap ? sourceMap.toObject() : undefined;
}

function hasValidKeys(conf) {
return Object.keys(conf).every(key => key === 'extensions' || key === 'testOptions');
}

function isValidExtensions(extensions) {
return Array.isArray(extensions) && extensions.every(ext => typeof ext === 'string' && ext !== '');
}

function validate(conf) {
if (conf === false) {
return null;
Expand All @@ -31,11 +40,17 @@ function validate(conf) {
return {testOptions: defaultOptions};
}

if (!conf || typeof conf !== 'object' || !conf.testOptions || typeof conf.testOptions !== 'object' || Array.isArray(conf.testOptions) || Object.keys(conf).length > 1) {
if (
!isPlainObject(conf) ||
!hasValidKeys(conf) ||
(conf.testOptions !== undefined && !isPlainObject(conf.testOptions)) ||
(conf.extensions !== undefined && !isValidExtensions(conf.extensions))
) {
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.`);
}

return {
extensions: conf.extensions,
testOptions: Object.assign({}, defaultOptions, conf.testOptions)
};
}
Expand Down
9 changes: 9 additions & 0 deletions lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ exports.run = () => { // eslint-disable-line complexity
const TapReporter = require('./reporters/tap');
const Watcher = require('./watcher');
const babelPipeline = require('./babel-pipeline');
const normalizeExtensions = require('./extensions');

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

let extensions;
try {
extensions = normalizeExtensions(conf.extensions || [], babelConfig);
} catch (err) {
exit(err.message);
}

// Copy resultant cli.flags into conf for use with Api and elsewhere
Object.assign(conf, cli.flags);

Expand All @@ -168,6 +176,7 @@ exports.run = () => { // eslint-disable-line complexity
require: arrify(conf.require),
cacheEnabled: conf.cache !== false,
compileEnhancements: conf.compileEnhancements !== false,
extensions,
match,
babelConfig,
resolveTestsFrom: cli.input.length === 0 ? projectDir : process.cwd(),
Expand Down
Loading

0 comments on commit 3533aba

Please sign in to comment.