Skip to content

Commit

Permalink
Resolve Babel options ahead of time 🎉 (#1262)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
novemberborn authored and sindresorhus committed Mar 3, 2017
1 parent bd5ed60 commit 0464b14
Show file tree
Hide file tree
Showing 12 changed files with 494 additions and 350 deletions.
19 changes: 11 additions & 8 deletions api.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const Promise = require('bluebird');
const getPort = require('get-port');
const arrify = require('arrify');
const ms = require('ms');
const babelConfigHelper = require('./lib/babel-config');
const CachingPrecompiler = require('./lib/caching-precompiler');
const RunStatus = require('./lib/run-status');
const AvaError = require('./lib/ava-error');
Expand Down Expand Up @@ -105,11 +106,14 @@ class Api extends EventEmitter {
this.options.cacheDir = cacheDir;

const isPowerAssertEnabled = this.options.powerAssert !== false;
this.precompiler = new CachingPrecompiler({
path: cacheDir,
babel: this.options.babelConfig,
powerAssert: isPowerAssertEnabled
});
return babelConfigHelper.build(this.options.projectDir, cacheDir, this.options.babelConfig, isPowerAssertEnabled)
.then(result => {
this.precompiler = new CachingPrecompiler({
path: cacheDir,
getBabelOptions: result.getOptions,
babelCacheKeys: result.cacheKeys
});
});
}
_precompileHelpers() {
this._precompiledHelpers = {};
Expand Down Expand Up @@ -144,9 +148,8 @@ class Api extends EventEmitter {
return Promise.resolve(runStatus);
}

this._setupPrecompiler(files);

return this._precompileHelpers()
return this._setupPrecompiler(files)
.then(() => this._precompileHelpers())
.then(() => {
if (this.options.timeout) {
this._setupTimeout(runStatus);
Expand Down
4 changes: 4 additions & 0 deletions docs/recipes/babelrc.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ Using the `"inherit"` shortcut will cause your tests to be transpiled the same a

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.

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.

## Extend your source transpilation configuration

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/).
Expand All @@ -93,6 +95,8 @@ When specifying the Babel config for your tests, you can set the `babelrc` optio

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.

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.

## Extend an alternate config file.


Expand Down
174 changes: 107 additions & 67 deletions lib/babel-config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
'use strict';
const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
const figures = require('figures');
const convertSourceMap = require('convert-source-map');
const configManager = require('hullabaloo-config-manager');
const md5Hex = require('md5-hex');
const mkdirp = require('mkdirp');
const colors = require('./colors');

function validate(conf) {
Expand All @@ -24,85 +27,122 @@ function validate(conf) {
return conf;
}

function lazy(buildPreset) {
let preset;

return babel => {
if (!preset) {
preset = buildPreset(babel);
const SOURCE = '(AVA) Base Babel config';
const AVA_DIR = path.join(__dirname, '..');

function verifyExistingOptions(verifierFile, baseConfig, cache) {
return new Promise((resolve, reject) => {
try {
resolve(fs.readFileSync(verifierFile));
} catch (err) {
if (err && err.code === 'ENOENT') {
resolve(null);
} else {
reject(err);
}
}

return preset;
};
})
.then(buffer => {
if (!buffer) {
return null;
}

const verifier = configManager.restoreVerifier(buffer);
const fixedSourceHashes = new Map();
fixedSourceHashes.set(baseConfig.source, baseConfig.hash);
if (baseConfig.extends) {
fixedSourceHashes.set(baseConfig.extends.source, baseConfig.extends.hash);
}
return verifier.verifyCurrentEnv({sources: fixedSourceHashes}, cache)
.then(result => {
if (!result.cacheKeys) {
return null;
}

if (result.dependenciesChanged) {
fs.writeFileSync(verifierFile, result.verifier.toBuffer());
}

return result.cacheKeys;
});
});
}

const stage4 = lazy(() => require('@ava/babel-preset-stage-4')());

function makeTransformTestFiles(powerAssert) {
return lazy(babel => {
return require('@ava/babel-preset-transform-test-files')(babel, {powerAssert});
});
function resolveOptions(baseConfig, cache, optionsFile, verifierFile) {
return configManager.fromConfig(baseConfig, {cache})
.then(result => {
fs.writeFileSync(optionsFile, result.generateModule());

return result.createVerifier()
.then(verifier => {
fs.writeFileSync(verifierFile, verifier.toBuffer());
return verifier.cacheKeysForCurrentEnv();
});
});
}

function build(babelConfig, powerAssert, filePath, code) {
babelConfig = validate(babelConfig);

let options;

if (babelConfig === 'default') {
options = {
babelrc: false,
presets: [stage4]
};
} else if (babelConfig === 'inherit') {
options = {
babelrc: true
};
} else {
options = {
babelrc: false
};

Object.assign(options, babelConfig);
function build(projectDir, cacheDir, userOptions, powerAssert) {
// Compute a seed based on the Node.js version and the project directory.
// Dependency hashes may vary based on the Node.js version, e.g. with the
// @ava/stage-4 Babel preset. Sources and dependencies paths are absolute in
// the generated module and verifier state. Those paths wouldn't necessarily
// be valid if the project directory changes.
const seed = md5Hex([process.versions.node, projectDir]);

// Ensure cacheDir exists
mkdirp.sync(cacheDir);

// The file names predict where valid options may be cached, and thus should
// include the seed.
const optionsFile = path.join(cacheDir, `${seed}.babel-options.js`);
const verifierFile = path.join(cacheDir, `${seed}.verifier.bin`);

const baseOptions = {
babelrc: false,
presets: [
['@ava/transform-test-files', {powerAssert}]
]
};
if (userOptions === 'default') {
baseOptions.presets.unshift('@ava/stage-4');
}

const sourceMap = getSourceMap(filePath, code);

Object.assign(options, {
inputSourceMap: sourceMap,
filename: filePath,
sourceMaps: true,
ast: false
const baseConfig = configManager.createConfig({
dir: AVA_DIR, // Presets are resolved relative to this directory
hash: md5Hex(JSON.stringify(baseOptions)),
json5: false,
options: baseOptions,
source: SOURCE
});

if (!options.presets) {
options.presets = [];
}
options.presets.push(makeTransformTestFiles(powerAssert));

return options;
}

function getSourceMap(filePath, code) {
let sourceMap = convertSourceMap.fromSource(code);

if (!sourceMap) {
const dirPath = path.dirname(filePath);
sourceMap = convertSourceMap.fromMapFileSource(code, dirPath);
}

if (sourceMap) {
sourceMap = sourceMap.toObject();
if (userOptions !== 'default') {
baseConfig.extend(configManager.createConfig({
dir: projectDir,
options: userOptions === 'inherit' ?
{babelrc: true} :
userOptions,
source: path.join(projectDir, 'package.json') + '#ava.babel',
hash: md5Hex(JSON.stringify(userOptions))
}));
}

return sourceMap;
const cache = configManager.prepareCache();
return verifyExistingOptions(verifierFile, baseConfig, cache)
.then(cacheKeys => {
if (cacheKeys) {
return cacheKeys;
}

return resolveOptions(baseConfig, cache, optionsFile, verifierFile);
})
.then(cacheKeys => ({
getOptions: require(optionsFile).getOptions, // eslint-disable-line import/no-dynamic-require
// Include the seed in the cache keys used to store compilation results.
cacheKeys: Object.assign({seed}, cacheKeys)
}));
}

module.exports = {
validate,
build,
presetHashes: [
require('@ava/babel-preset-stage-4/package-hash'),
require('@ava/babel-preset-transform-test-files/package-hash')
]
build
};
48 changes: 35 additions & 13 deletions lib/caching-precompiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,29 @@ const packageHash = require('package-hash');
const stripBomBuf = require('strip-bom-buf');
const autoBind = require('auto-bind');
const md5Hex = require('md5-hex');
const babelConfigHelper = require('./babel-config');

function getSourceMap(filePath, code) {
let sourceMap = convertSourceMap.fromSource(code);

if (!sourceMap) {
const dirPath = path.dirname(filePath);
sourceMap = convertSourceMap.fromMapFileSource(code, dirPath);
}

if (sourceMap) {
sourceMap = sourceMap.toObject();
}

return sourceMap;
}

class CachingPrecompiler {
constructor(options) {
autoBind(this);

options = options || {};

this.babelConfig = babelConfigHelper.validate(options.babel);
this.getBabelOptions = options.getBabelOptions;
this.babelCacheKeys = options.babelCacheKeys;
this.cacheDirPath = options.path;
this.powerAssert = Boolean(options.powerAssert);
this.fileHashes = {};
this.transform = this._createTransform();
}
Expand All @@ -37,8 +49,23 @@ class CachingPrecompiler {
_transform(code, filePath, hash) {
code = code.toString();

const options = babelConfigHelper.build(this.babelConfig, this.powerAssert, filePath, code);
const result = this.babel.transform(code, options);
let result;
const originalBabelDisableCache = process.env.BABEL_DISABLE_CACHE;
try {
// Disable Babel's cache. AVA has good cache management already.
process.env.BABEL_DISABLE_CACHE = '1';

result = this.babel.transform(code, Object.assign(this.getBabelOptions(), {
inputSourceMap: getSourceMap(filePath, code),
filename: filePath,
sourceMaps: true,
ast: false
}));
} finally {
// Restore the original value. It is passed to workers, where users may
// not want Babel's cache to be disabled.
process.env.BABEL_DISABLE_CACHE = originalBabelDisableCache;
}

// Save source map
const mapPath = path.join(this.cacheDirPath, `${hash}.js.map`);
Expand All @@ -56,12 +83,7 @@ class CachingPrecompiler {
const salt = packageHash.sync([
require.resolve('../package.json'),
require.resolve('babel-core/package.json')
], {
babelConfig: this.babelConfig,
majorNodeVersion: process.version.split('.')[0],
powerAssert: this.powerAssert,
presetHashes: babelConfigHelper.presetHashes
});
], this.babelCacheKeys);

return cachingTransform({
factory: this._init,
Expand Down
4 changes: 2 additions & 2 deletions lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const MiniReporter = require('./reporters/mini');
const TapReporter = require('./reporters/tap');
const Logger = require('./logger');
const Watcher = require('./watcher');
const babelConfig = require('./babel-config');
const babelConfigHelper = require('./babel-config');

// Bluebird specific
Promise.longStackTraces();
Expand Down Expand Up @@ -115,7 +115,7 @@ exports.run = () => {
powerAssert: cli.flags.powerAssert !== false,
explicitTitles: cli.flags.watch,
match: arrify(cli.flags.match),
babelConfig: babelConfig.validate(conf.babel),
babelConfig: babelConfigHelper.validate(conf.babel),
resolveTestsFrom: cli.input.length === 0 ? projectDir : process.cwd(),
projectDir,
timeout: cli.flags.timeout,
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
"get-port": "^2.1.0",
"globby": "^6.0.0",
"has-flag": "^2.0.0",
"hullabaloo-config-manager": "^0.2.0",
"ignore-by-default": "^1.0.0",
"indent-string": "^3.0.0",
"is-ci": "^1.0.7",
Expand All @@ -143,11 +144,12 @@
"max-timeout": "^1.0.0",
"md5-hex": "^2.0.0",
"meow": "^3.7.0",
"mkdirp": "^0.5.1",
"ms": "^0.7.1",
"multimatch": "^2.1.0",
"observable-to-promise": "^0.4.0",
"option-chain": "^0.1.0",
"package-hash": "^1.2.0",
"package-hash": "^2.0.0",
"pkg-conf": "^2.0.0",
"plur": "^2.0.0",
"pretty-ms": "^2.0.0",
Expand Down Expand Up @@ -175,7 +177,6 @@
"inquirer": "^2.0.0",
"is-array-sorted": "^1.0.0",
"lolex": "^1.4.0",
"mkdirp": "^0.5.1",
"nyc": "^10.0.0",
"pify": "^2.3.0",
"proxyquire": "^1.7.4",
Expand Down
Loading

0 comments on commit 0464b14

Please sign in to comment.