Skip to content

Commit 0464b14

Browse files
novemberbornsindresorhus
authored andcommitted
Resolve Babel options ahead of time 🎉 (#1262)
* package-hash@^2 * Allow precompiler setup to be asynchronous * Consistently refer to babel-config module as babelConfigHelper * Manage Babel config using hullabaloo Fixes #707 * Disable Babel cache when precompiling
1 parent bd5ed60 commit 0464b14

12 files changed

+494
-350
lines changed

api.js

+11-8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const Promise = require('bluebird');
1212
const getPort = require('get-port');
1313
const arrify = require('arrify');
1414
const ms = require('ms');
15+
const babelConfigHelper = require('./lib/babel-config');
1516
const CachingPrecompiler = require('./lib/caching-precompiler');
1617
const RunStatus = require('./lib/run-status');
1718
const AvaError = require('./lib/ava-error');
@@ -105,11 +106,14 @@ class Api extends EventEmitter {
105106
this.options.cacheDir = cacheDir;
106107

107108
const isPowerAssertEnabled = this.options.powerAssert !== false;
108-
this.precompiler = new CachingPrecompiler({
109-
path: cacheDir,
110-
babel: this.options.babelConfig,
111-
powerAssert: isPowerAssertEnabled
112-
});
109+
return babelConfigHelper.build(this.options.projectDir, cacheDir, this.options.babelConfig, isPowerAssertEnabled)
110+
.then(result => {
111+
this.precompiler = new CachingPrecompiler({
112+
path: cacheDir,
113+
getBabelOptions: result.getOptions,
114+
babelCacheKeys: result.cacheKeys
115+
});
116+
});
113117
}
114118
_precompileHelpers() {
115119
this._precompiledHelpers = {};
@@ -144,9 +148,8 @@ class Api extends EventEmitter {
144148
return Promise.resolve(runStatus);
145149
}
146150

147-
this._setupPrecompiler(files);
148-
149-
return this._precompileHelpers()
151+
return this._setupPrecompiler(files)
152+
.then(() => this._precompileHelpers())
150153
.then(() => {
151154
if (this.options.timeout) {
152155
this._setupTimeout(runStatus);

docs/recipes/babelrc.md

+4
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ Using the `"inherit"` shortcut will cause your tests to be transpiled the same a
6969

7070
In the above example, both tests and sources will be transpiled using the [`@ava/stage-4`](https://github.com/avajs/babel-preset-stage-4) and [`react`](http://babeljs.io/docs/plugins/preset-react/) presets.
7171

72+
AVA will only look for a `.babelrc` file in the same directory as the `package.json` file. If not found then it assumes your Babel config lives in the `package.json` file.
73+
7274
## Extend your source transpilation configuration
7375

7476
When specifying the Babel config for your tests, you can set the `babelrc` option to `true`. This will merge the specified plugins with those from your [`babelrc`](http://babeljs.io/docs/usage/babelrc/).
@@ -93,6 +95,8 @@ When specifying the Babel config for your tests, you can set the `babelrc` optio
9395

9496
In the above example, *sources* are compiled use [`@ava/stage-4`](https://github.com/avajs/babel-preset-stage-4) and [`react`](http://babeljs.io/docs/plugins/preset-react/), *tests* use those same plugins, plus the additional `custom` plugins specified.
9597

98+
AVA will only look for a `.babelrc` file in the same directory as the `package.json` file. If not found then it assumes your Babel config lives in the `package.json` file.
99+
96100
## Extend an alternate config file.
97101

98102

lib/babel-config.js

+107-67
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
'use strict';
2+
const fs = require('fs');
23
const path = require('path');
34
const chalk = require('chalk');
45
const figures = require('figures');
5-
const convertSourceMap = require('convert-source-map');
6+
const configManager = require('hullabaloo-config-manager');
7+
const md5Hex = require('md5-hex');
8+
const mkdirp = require('mkdirp');
69
const colors = require('./colors');
710

811
function validate(conf) {
@@ -24,85 +27,122 @@ function validate(conf) {
2427
return conf;
2528
}
2629

27-
function lazy(buildPreset) {
28-
let preset;
29-
30-
return babel => {
31-
if (!preset) {
32-
preset = buildPreset(babel);
30+
const SOURCE = '(AVA) Base Babel config';
31+
const AVA_DIR = path.join(__dirname, '..');
32+
33+
function verifyExistingOptions(verifierFile, baseConfig, cache) {
34+
return new Promise((resolve, reject) => {
35+
try {
36+
resolve(fs.readFileSync(verifierFile));
37+
} catch (err) {
38+
if (err && err.code === 'ENOENT') {
39+
resolve(null);
40+
} else {
41+
reject(err);
42+
}
3343
}
34-
35-
return preset;
36-
};
44+
})
45+
.then(buffer => {
46+
if (!buffer) {
47+
return null;
48+
}
49+
50+
const verifier = configManager.restoreVerifier(buffer);
51+
const fixedSourceHashes = new Map();
52+
fixedSourceHashes.set(baseConfig.source, baseConfig.hash);
53+
if (baseConfig.extends) {
54+
fixedSourceHashes.set(baseConfig.extends.source, baseConfig.extends.hash);
55+
}
56+
return verifier.verifyCurrentEnv({sources: fixedSourceHashes}, cache)
57+
.then(result => {
58+
if (!result.cacheKeys) {
59+
return null;
60+
}
61+
62+
if (result.dependenciesChanged) {
63+
fs.writeFileSync(verifierFile, result.verifier.toBuffer());
64+
}
65+
66+
return result.cacheKeys;
67+
});
68+
});
3769
}
3870

39-
const stage4 = lazy(() => require('@ava/babel-preset-stage-4')());
40-
41-
function makeTransformTestFiles(powerAssert) {
42-
return lazy(babel => {
43-
return require('@ava/babel-preset-transform-test-files')(babel, {powerAssert});
44-
});
71+
function resolveOptions(baseConfig, cache, optionsFile, verifierFile) {
72+
return configManager.fromConfig(baseConfig, {cache})
73+
.then(result => {
74+
fs.writeFileSync(optionsFile, result.generateModule());
75+
76+
return result.createVerifier()
77+
.then(verifier => {
78+
fs.writeFileSync(verifierFile, verifier.toBuffer());
79+
return verifier.cacheKeysForCurrentEnv();
80+
});
81+
});
4582
}
4683

47-
function build(babelConfig, powerAssert, filePath, code) {
48-
babelConfig = validate(babelConfig);
49-
50-
let options;
51-
52-
if (babelConfig === 'default') {
53-
options = {
54-
babelrc: false,
55-
presets: [stage4]
56-
};
57-
} else if (babelConfig === 'inherit') {
58-
options = {
59-
babelrc: true
60-
};
61-
} else {
62-
options = {
63-
babelrc: false
64-
};
65-
66-
Object.assign(options, babelConfig);
84+
function build(projectDir, cacheDir, userOptions, powerAssert) {
85+
// Compute a seed based on the Node.js version and the project directory.
86+
// Dependency hashes may vary based on the Node.js version, e.g. with the
87+
// @ava/stage-4 Babel preset. Sources and dependencies paths are absolute in
88+
// the generated module and verifier state. Those paths wouldn't necessarily
89+
// be valid if the project directory changes.
90+
const seed = md5Hex([process.versions.node, projectDir]);
91+
92+
// Ensure cacheDir exists
93+
mkdirp.sync(cacheDir);
94+
95+
// The file names predict where valid options may be cached, and thus should
96+
// include the seed.
97+
const optionsFile = path.join(cacheDir, `${seed}.babel-options.js`);
98+
const verifierFile = path.join(cacheDir, `${seed}.verifier.bin`);
99+
100+
const baseOptions = {
101+
babelrc: false,
102+
presets: [
103+
['@ava/transform-test-files', {powerAssert}]
104+
]
105+
};
106+
if (userOptions === 'default') {
107+
baseOptions.presets.unshift('@ava/stage-4');
67108
}
68109

69-
const sourceMap = getSourceMap(filePath, code);
70-
71-
Object.assign(options, {
72-
inputSourceMap: sourceMap,
73-
filename: filePath,
74-
sourceMaps: true,
75-
ast: false
110+
const baseConfig = configManager.createConfig({
111+
dir: AVA_DIR, // Presets are resolved relative to this directory
112+
hash: md5Hex(JSON.stringify(baseOptions)),
113+
json5: false,
114+
options: baseOptions,
115+
source: SOURCE
76116
});
77117

78-
if (!options.presets) {
79-
options.presets = [];
80-
}
81-
options.presets.push(makeTransformTestFiles(powerAssert));
82-
83-
return options;
84-
}
85-
86-
function getSourceMap(filePath, code) {
87-
let sourceMap = convertSourceMap.fromSource(code);
88-
89-
if (!sourceMap) {
90-
const dirPath = path.dirname(filePath);
91-
sourceMap = convertSourceMap.fromMapFileSource(code, dirPath);
92-
}
93-
94-
if (sourceMap) {
95-
sourceMap = sourceMap.toObject();
118+
if (userOptions !== 'default') {
119+
baseConfig.extend(configManager.createConfig({
120+
dir: projectDir,
121+
options: userOptions === 'inherit' ?
122+
{babelrc: true} :
123+
userOptions,
124+
source: path.join(projectDir, 'package.json') + '#ava.babel',
125+
hash: md5Hex(JSON.stringify(userOptions))
126+
}));
96127
}
97128

98-
return sourceMap;
129+
const cache = configManager.prepareCache();
130+
return verifyExistingOptions(verifierFile, baseConfig, cache)
131+
.then(cacheKeys => {
132+
if (cacheKeys) {
133+
return cacheKeys;
134+
}
135+
136+
return resolveOptions(baseConfig, cache, optionsFile, verifierFile);
137+
})
138+
.then(cacheKeys => ({
139+
getOptions: require(optionsFile).getOptions, // eslint-disable-line import/no-dynamic-require
140+
// Include the seed in the cache keys used to store compilation results.
141+
cacheKeys: Object.assign({seed}, cacheKeys)
142+
}));
99143
}
100144

101145
module.exports = {
102146
validate,
103-
build,
104-
presetHashes: [
105-
require('@ava/babel-preset-stage-4/package-hash'),
106-
require('@ava/babel-preset-transform-test-files/package-hash')
107-
]
147+
build
108148
};

lib/caching-precompiler.js

+35-13
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,29 @@ const packageHash = require('package-hash');
77
const stripBomBuf = require('strip-bom-buf');
88
const autoBind = require('auto-bind');
99
const md5Hex = require('md5-hex');
10-
const babelConfigHelper = require('./babel-config');
10+
11+
function getSourceMap(filePath, code) {
12+
let sourceMap = convertSourceMap.fromSource(code);
13+
14+
if (!sourceMap) {
15+
const dirPath = path.dirname(filePath);
16+
sourceMap = convertSourceMap.fromMapFileSource(code, dirPath);
17+
}
18+
19+
if (sourceMap) {
20+
sourceMap = sourceMap.toObject();
21+
}
22+
23+
return sourceMap;
24+
}
1125

1226
class CachingPrecompiler {
1327
constructor(options) {
1428
autoBind(this);
1529

16-
options = options || {};
17-
18-
this.babelConfig = babelConfigHelper.validate(options.babel);
30+
this.getBabelOptions = options.getBabelOptions;
31+
this.babelCacheKeys = options.babelCacheKeys;
1932
this.cacheDirPath = options.path;
20-
this.powerAssert = Boolean(options.powerAssert);
2133
this.fileHashes = {};
2234
this.transform = this._createTransform();
2335
}
@@ -37,8 +49,23 @@ class CachingPrecompiler {
3749
_transform(code, filePath, hash) {
3850
code = code.toString();
3951

40-
const options = babelConfigHelper.build(this.babelConfig, this.powerAssert, filePath, code);
41-
const result = this.babel.transform(code, options);
52+
let result;
53+
const originalBabelDisableCache = process.env.BABEL_DISABLE_CACHE;
54+
try {
55+
// Disable Babel's cache. AVA has good cache management already.
56+
process.env.BABEL_DISABLE_CACHE = '1';
57+
58+
result = this.babel.transform(code, Object.assign(this.getBabelOptions(), {
59+
inputSourceMap: getSourceMap(filePath, code),
60+
filename: filePath,
61+
sourceMaps: true,
62+
ast: false
63+
}));
64+
} finally {
65+
// Restore the original value. It is passed to workers, where users may
66+
// not want Babel's cache to be disabled.
67+
process.env.BABEL_DISABLE_CACHE = originalBabelDisableCache;
68+
}
4269

4370
// Save source map
4471
const mapPath = path.join(this.cacheDirPath, `${hash}.js.map`);
@@ -56,12 +83,7 @@ class CachingPrecompiler {
5683
const salt = packageHash.sync([
5784
require.resolve('../package.json'),
5885
require.resolve('babel-core/package.json')
59-
], {
60-
babelConfig: this.babelConfig,
61-
majorNodeVersion: process.version.split('.')[0],
62-
powerAssert: this.powerAssert,
63-
presetHashes: babelConfigHelper.presetHashes
64-
});
86+
], this.babelCacheKeys);
6587

6688
return cachingTransform({
6789
factory: this._init,

lib/cli.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const MiniReporter = require('./reporters/mini');
1515
const TapReporter = require('./reporters/tap');
1616
const Logger = require('./logger');
1717
const Watcher = require('./watcher');
18-
const babelConfig = require('./babel-config');
18+
const babelConfigHelper = require('./babel-config');
1919

2020
// Bluebird specific
2121
Promise.longStackTraces();
@@ -115,7 +115,7 @@ exports.run = () => {
115115
powerAssert: cli.flags.powerAssert !== false,
116116
explicitTitles: cli.flags.watch,
117117
match: arrify(cli.flags.match),
118-
babelConfig: babelConfig.validate(conf.babel),
118+
babelConfig: babelConfigHelper.validate(conf.babel),
119119
resolveTestsFrom: cli.input.length === 0 ? projectDir : process.cwd(),
120120
projectDir,
121121
timeout: cli.flags.timeout,

package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@
125125
"get-port": "^2.1.0",
126126
"globby": "^6.0.0",
127127
"has-flag": "^2.0.0",
128+
"hullabaloo-config-manager": "^0.2.0",
128129
"ignore-by-default": "^1.0.0",
129130
"indent-string": "^3.0.0",
130131
"is-ci": "^1.0.7",
@@ -143,11 +144,12 @@
143144
"max-timeout": "^1.0.0",
144145
"md5-hex": "^2.0.0",
145146
"meow": "^3.7.0",
147+
"mkdirp": "^0.5.1",
146148
"ms": "^0.7.1",
147149
"multimatch": "^2.1.0",
148150
"observable-to-promise": "^0.4.0",
149151
"option-chain": "^0.1.0",
150-
"package-hash": "^1.2.0",
152+
"package-hash": "^2.0.0",
151153
"pkg-conf": "^2.0.0",
152154
"plur": "^2.0.0",
153155
"pretty-ms": "^2.0.0",
@@ -175,7 +177,6 @@
175177
"inquirer": "^2.0.0",
176178
"is-array-sorted": "^1.0.0",
177179
"lolex": "^1.4.0",
178-
"mkdirp": "^0.5.1",
179180
"nyc": "^10.0.0",
180181
"pify": "^2.3.0",
181182
"proxyquire": "^1.7.4",

0 commit comments

Comments
 (0)