Skip to content

Commit fd477f1

Browse files
committed
Refactor, improve and test extension validation
Support periods in extensions.
1 parent 17cc02c commit fd477f1

File tree

13 files changed

+169
-59
lines changed

13 files changed

+169
-59
lines changed

api.js

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ 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');
910
const debounce = require('lodash.debounce');
10-
const difference = require('lodash.difference');
1111
const Bluebird = require('bluebird');
1212
const getPort = require('get-port');
1313
const arrify = require('arrify');
@@ -39,30 +39,8 @@ class Api extends Emittery {
3939
this.options = Object.assign({match: []}, options);
4040
this.options.require = resolveModules(this.options.require);
4141

42-
const {babelConfig, extensions: doNotCompileExtensions = []} = this.options;
43-
const {extensions: babelExtensions = []} = babelConfig || {};
44-
45-
if (babelConfig) {
46-
if (babelExtensions.length === 0) {
47-
if (doNotCompileExtensions.includes('js')) {
48-
throw new Error(`Cannot specify generic 'js' extension without disabling AVA's Babel usage`);
49-
}
50-
51-
babelExtensions.push('js');
52-
}
53-
} else if (doNotCompileExtensions.length === 0) {
54-
doNotCompileExtensions.push('js');
55-
}
56-
57-
// Combine all extensions possible for testing. Removing duplicate extensions.
58-
this._allExtensions = [...new Set([...doNotCompileExtensions, ...babelExtensions])];
59-
if (this._allExtensions.length !== doNotCompileExtensions.length + babelExtensions.length) {
60-
const notRepeated = difference(doNotCompileExtensions, babelExtensions);
61-
const duplicates = this._allExtensions.filter(ext => !notRepeated.includes(ext));
62-
throw new Error(`Unexpected duplicate extensions in options: ${duplicates.join(', ')}`);
63-
}
64-
65-
this._doNotCompileExtensions = new Set(doNotCompileExtensions);
42+
this._allExtensions = this.options.extensions.all;
43+
this._regexpFullExtensions = new RegExp(`\\.(${this.options.extensions.full.map(ext => escapeStringRegexp(ext)).join('|')})$`);
6644
this._precompiler = null;
6745
}
6846

@@ -162,10 +140,10 @@ class Api extends Emittery {
162140
map: [...files, ...helpers].reduce((acc, file) => {
163141
try {
164142
const realpath = fs.realpathSync(file);
165-
const ext = path.extname(realpath).slice(1);
166-
const cachePath = this._doNotCompileExtensions.has(ext) ?
167-
precompilation.precompileEnhancementsOnly(realpath) :
168-
precompilation.precompileFull(realpath);
143+
const filename = path.basename(realpath);
144+
const cachePath = this._regexpFullExtensions.test(filename) ?
145+
precompilation.precompileFull(realpath) :
146+
precompilation.precompileEnhancementsOnly(realpath);
169147
if (cachePath) {
170148
acc[realpath] = cachePath;
171149
}
@@ -254,7 +232,7 @@ class Api extends Emittery {
254232
filename => {
255233
throw new Error(`Cannot apply full precompilation, possible bad usage: ${filename}`);
256234
};
257-
const precompileEnhancementsOnly = compileEnhancements && this._doNotCompileExtensions.size > 0 ?
235+
const precompileEnhancementsOnly = compileEnhancements && this.options.extensions.enhancementsOnly.length > 0 ?
258236
babelPipeline.build(projectDir, cacheDir, null, compileEnhancements) :
259237
filename => {
260238
throw new Error(`Cannot apply enhancement-only precompilation, possible bad usage: ${filename}`);

lib/cli.js

Lines changed: 9 additions & 1 deletion
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,7 +176,7 @@ exports.run = () => { // eslint-disable-line complexity
168176
require: arrify(conf.require),
169177
cacheEnabled: conf.cache !== false,
170178
compileEnhancements: conf.compileEnhancements !== false,
171-
extensions: conf.extensions,
179+
extensions,
172180
match,
173181
babelConfig,
174182
resolveTestsFrom: cli.input.length === 0 ? projectDir : process.cwd(),

lib/extensions.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
module.exports = (enhancementsOnly, babelConfig) => {
2+
const {extensions: full = []} = babelConfig || {};
3+
4+
// Combine all extensions possible for testing. Remove duplicate extensions.
5+
const duplicates = [];
6+
const seen = new Set();
7+
for (const ext of [...enhancementsOnly, ...full]) {
8+
if (seen.has(ext)) {
9+
duplicates.push(ext);
10+
} else {
11+
seen.add(ext);
12+
}
13+
}
14+
15+
// Decide if and where to add the default `js` extension. Keep in mind it's not
16+
// added if extensions have been explicitly given.
17+
if (!seen.has('js')) {
18+
if (babelConfig && full.length === 0) {
19+
seen.add('js');
20+
full.push('js');
21+
}
22+
if (!babelConfig && enhancementsOnly.length === 0) {
23+
seen.add('js');
24+
enhancementsOnly.push('js');
25+
}
26+
} else if (babelConfig && full.length === 0) {
27+
// If Babel is not disabled, and has the default extensions (or, explicitly,
28+
// no configured extensions), thes the `js` extension must have come from
29+
// the `enhancementsOnly` value. That's not allowed since it'd be a
30+
// roundabout way of disabling Babel.
31+
throw new Error(`Cannot specify generic 'js' extension without disabling AVA's Babel usage.`);
32+
}
33+
34+
if (duplicates.length > 0) {
35+
throw new Error(`Unexpected duplicate extensions in options: '${duplicates.join('\', \'')}'.`);
36+
}
37+
38+
const all = [...seen];
39+
return {all, enhancementsOnly, full};
40+
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
"emittery": "^0.3.0",
9292
"empower-core": "^0.6.1",
9393
"equal-length": "^1.0.0",
94+
"escape-string-regexp": "^1.0.5",
9495
"figures": "^2.0.0",
9596
"get-port": "^3.2.0",
9697
"globby": "^7.1.1",

test/api.js

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ function apiCreator(options) {
3030
options = options || {};
3131
options.babelConfig = babelPipeline.validate(options.babelConfig);
3232
options.concurrency = 2;
33+
options.extensions = options.extensions || {all: ['js'], enhancementsOnly: [], full: ['js']};
3334
options.projectDir = options.projectDir || ROOT_DIR;
3435
options.resolveTestsFrom = options.resolveTestsFrom || options.projectDir;
3536
const instance = new Api(options);
@@ -941,34 +942,6 @@ test('babel.testOptions with extends still merges plugins with .babelrc', t => {
941942
});
942943
});
943944

944-
test('precompiles tests when babelConfig.extensions is defined', t => {
945-
t.plan(3);
946-
947-
const api = apiCreator({
948-
babelConfig: {
949-
extensions: [],
950-
testOptions: {
951-
babelrc: true
952-
}
953-
},
954-
cacheEnabled: false,
955-
projectDir: path.join(__dirname, 'fixture/babelrc')
956-
});
957-
958-
api.on('run', plan => {
959-
plan.status.on('stateChange', evt => {
960-
if (evt.type === 'test-passed') {
961-
t.ok(evt.title === 'foo' || evt.title === 'repeated test: foo');
962-
}
963-
});
964-
});
965-
966-
return api.run()
967-
.then(runStatus => {
968-
t.is(runStatus.stats.passedTests, 2);
969-
});
970-
});
971-
972945
test('extended config can disable ava/stage-4', t => {
973946
t.plan(1);
974947

@@ -1041,6 +1014,38 @@ test('uses "development" Babel environment if NODE_ENV is the empty string', t =
10411014
});
10421015
});
10431016

1017+
test('full extensions take precedence over enhancements-only', t => {
1018+
t.plan(2);
1019+
1020+
const api = apiCreator({
1021+
babelConfig: {
1022+
testOptions: {
1023+
plugins: [testCapitalizerPlugin]
1024+
}
1025+
},
1026+
extensions: {
1027+
all: ['foo.bar', 'bar'],
1028+
enhancementsOnly: ['bar'],
1029+
full: ['foo.bar']
1030+
},
1031+
cacheEnabled: false,
1032+
projectDir: path.join(__dirname, 'fixture/extensions')
1033+
});
1034+
1035+
api.on('run', plan => {
1036+
plan.status.on('stateChange', evt => {
1037+
if (evt.type === 'test-passed') {
1038+
t.ok(evt.title === 'FOO');
1039+
}
1040+
});
1041+
});
1042+
1043+
return api.run()
1044+
.then(runStatus => {
1045+
t.is(runStatus.stats.passedTests, 1);
1046+
});
1047+
});
1048+
10441049
test('using --match with matching tests will only report those passing tests', t => {
10451050
t.plan(3);
10461051

test/cli.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,38 @@ for (const which of [
8989
});
9090
}
9191

92+
test('errors if top-level extensions include "js" without babel=false', t => {
93+
execCli(['es2015.js'], {dirname: `fixture/invalid-extensions/top-level`}, (err, stdout, stderr) => {
94+
t.ok(err);
95+
96+
let expectedOutput = '\n';
97+
expectedOutput += figures.cross + ' Cannot specify generic \'js\' extension without disabling AVA\'s Babel usage.';
98+
expectedOutput += '\n';
99+
100+
t.is(stderr, expectedOutput);
101+
t.end();
102+
});
103+
});
104+
105+
for (const [where, which, msg = '\'js\', \'jsx\''] of [
106+
['top-level', 'top-level-duplicates'],
107+
['babel', 'babel-duplicates'],
108+
['top-level and babel', 'shared-duplicates', '\'jsx\'']
109+
]) {
110+
test(`errors if ${where} extensions include duplicates`, t => {
111+
execCli(['es2015.js'], {dirname: `fixture/invalid-extensions/${which}`}, (err, stdout, stderr) => {
112+
t.ok(err);
113+
114+
let expectedOutput = '\n';
115+
expectedOutput += figures.cross + ` Unexpected duplicate extensions in options: ${msg}.`;
116+
expectedOutput += '\n';
117+
118+
t.is(stderr, expectedOutput);
119+
t.end();
120+
});
121+
});
122+
}
123+
92124
test('enabling long stack traces will provide detailed debug information', t => {
93125
execCli('fixture/long-stack-trace', (err, stdout, stderr) => {
94126
t.ok(err);

test/fixture/extensions/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "application-name",
3+
"version": "0.0.1"
4+
}

test/fixture/extensions/test.foo.bar

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import test from '../../..';
2+
3+
const one = {one: 1};
4+
const two = {two: 2};
5+
6+
test('foo', t => {
7+
// Using object rest/spread to ensure it transpiles on Node.js 6, since this
8+
// is a Node.js 8 feature
9+
const actual = {...one, ...two};
10+
t.deepEqual(actual, {one: 1, two: 2});
11+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"ava": {
3+
"babel": {
4+
"extensions": ["js", "js", "jsx", "jsx"]
5+
}
6+
}
7+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"ava": {
3+
"babel": {
4+
"extensions": ["js", "jsx"]
5+
},
6+
"extensions": ["jsx"]
7+
}
8+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"ava": {
3+
"babel": false,
4+
"extensions": ["js", "js", "jsx", "jsx"]
5+
}
6+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"ava": {
3+
"extensions": ["js"]
4+
}
5+
}

test/helper/report.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ const run = (type, reporter) => {
7979
const projectDir = path.join(__dirname, '../fixture/report', type.toLowerCase());
8080

8181
const api = createApi({
82+
extensions: {
83+
all: ['js'],
84+
enhancementsOnly: [],
85+
full: ['js']
86+
},
8287
failFast: type === 'failFast' || type === 'failFast2',
8388
failWithoutAssertions: false,
8489
serial: type === 'failFast' || type === 'failFast2',

0 commit comments

Comments
 (0)