Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve Babel options ahead of time 🎉 #1262

Merged
merged 5 commits into from
Mar 3, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -70,6 +70,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 @@ -94,6 +96,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 @@ -124,6 +124,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 @@ -142,11 +143,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 @@ -174,7 +176,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