Skip to content

Commit 3db164c

Browse files
committed
Manage Babel config using hullabaloo
Fixes #707
1 parent d86d6fd commit 3db164c

File tree

8 files changed

+357
-276
lines changed

8 files changed

+357
-276
lines changed

api.js

+9-7
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,13 +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-
});
113-
114-
return Promise.resolve();
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+
});
115117
}
116118
_precompileHelpers() {
117119
this._precompiledHelpers = {};

docs/recipes/babelrc.md

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

7171
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.
7272

73+
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.
74+
7375
## Extend your source transpilation configuration
7476

7577
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/).
@@ -94,6 +96,8 @@ When specifying the Babel config for your tests, you can set the `babelrc` optio
9496

9597
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.
9698

99+
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.
100+
97101
## Extend an alternate config file.
98102

99103

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

+24-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,12 @@ 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+
const result = this.babel.transform(code, Object.assign(this.getBabelOptions(), {
53+
inputSourceMap: getSourceMap(filePath, code),
54+
filename: filePath,
55+
sourceMaps: true,
56+
ast: false
57+
}));
4258

4359
// Save source map
4460
const mapPath = path.join(this.cacheDirPath, `${hash}.js.map`);
@@ -56,12 +72,7 @@ class CachingPrecompiler {
5672
const salt = packageHash.sync([
5773
require.resolve('../package.json'),
5874
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-
});
75+
], this.babelCacheKeys);
6576

6677
return cachingTransform({
6778
factory: this._init,

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
"get-port": "^2.1.0",
125125
"globby": "^6.0.0",
126126
"has-flag": "^2.0.0",
127+
"hullabaloo-config-manager": "^0.2.0",
127128
"ignore-by-default": "^1.0.0",
128129
"indent-string": "^3.0.0",
129130
"is-ci": "^1.0.7",
@@ -142,6 +143,7 @@
142143
"max-timeout": "^1.0.0",
143144
"md5-hex": "^2.0.0",
144145
"meow": "^3.7.0",
146+
"mkdirp": "^0.5.1",
145147
"ms": "^0.7.1",
146148
"multimatch": "^2.1.0",
147149
"observable-to-promise": "^0.4.0",
@@ -174,7 +176,6 @@
174176
"inquirer": "^2.0.0",
175177
"is-array-sorted": "^1.0.0",
176178
"lolex": "^1.4.0",
177-
"mkdirp": "^0.5.1",
178179
"nyc": "^10.0.0",
179180
"pify": "^2.3.0",
180181
"proxyquire": "^1.7.4",

0 commit comments

Comments
 (0)