diff --git a/api.js b/api.js index 77f6d1ee5..cedd69db2 100644 --- a/api.js +++ b/api.js @@ -187,7 +187,7 @@ Api.prototype.run = function (files) { uniqueTempDir(); self.options.cacheDir = cacheDir; - self.precompiler = new CachingPrecompiler(cacheDir); + self.precompiler = new CachingPrecompiler(cacheDir, self.options.babelConfig); self.fileCount = files.length; self.base = path.relative('.', commonPathPrefix(files)) + path.sep; diff --git a/cli.js b/cli.js index 3ab2dfe9e..5c269b11a 100755 --- a/cli.js +++ b/cli.js @@ -35,7 +35,11 @@ var Api = require('./api'); // Bluebird specific Promise.longStackTraces(); -var conf = pkgConf.sync('ava'); +var conf = pkgConf.sync('ava', { + defaults: { + babel: 'default' + } +}); var cli = meow([ 'Usage', @@ -102,7 +106,8 @@ var api = new Api({ require: arrify(cli.flags.require), cacheEnabled: cli.flags.cache !== false, explicitTitles: cli.flags.watch, - match: arrify(cli.flags.match) + match: arrify(cli.flags.match), + babelConfig: conf.babel }); var reporter; diff --git a/lib/caching-precompiler.js b/lib/caching-precompiler.js index bdb5deb7f..8b6f63bac 100644 --- a/lib/caching-precompiler.js +++ b/lib/caching-precompiler.js @@ -3,20 +3,21 @@ var path = require('path'); var cachingTransform = require('caching-transform'); var md5Hex = require('md5-hex'); var stripBom = require('strip-bom'); +var objectAssign = require('object-assign'); module.exports = CachingPrecompiler; -function CachingPrecompiler(cacheDir) { +function CachingPrecompiler(cacheDir, babelConfig) { if (!(this instanceof CachingPrecompiler)) { throw new TypeError('Class constructor CachingPrecompiler cannot be invoked without \'new\''); } this.cacheDir = cacheDir; this.filenameToHash = {}; - this.transform = this._createTransform(); + this.transform = this._createTransform(babelConfig); } -CachingPrecompiler.prototype._factory = function (cacheDir) { +CachingPrecompiler.prototype._factory = function (babelConfig, cacheDir) { // This factory method is only called once per process, and only as needed, to defer loading expensive dependencies. var babel = require('babel-core'); var convertSourceMap = require('convert-source-map'); @@ -30,15 +31,27 @@ CachingPrecompiler.prototype._factory = function (cacheDir) { // Extract existing source maps from the code. var sourceMap = convertSourceMap.fromSource(code) || convertSourceMap.fromMapFileSource(code, path.dirname(filename)); - return { - presets: [presetStage2, presetES2015], - plugins: [powerAssert, transformRuntime], + var options = {babelrc: false}; + + if (!babelConfig || babelConfig === 'default') { + objectAssign(options, {presets: [presetStage2, presetES2015]}); + } else if (babelConfig === 'inherit') { + objectAssign(options, {babelrc: true}); + } else { + objectAssign(options, babelConfig); + } + + objectAssign(options, { + inputSourceMap: sourceMap && sourceMap.toObject(), filename: filename, sourceMaps: true, - ast: false, - babelrc: false, - inputSourceMap: sourceMap && sourceMap.toObject() - }; + ast: false + }); + + options.plugins = options.plugins || []; + options.plugins.push(powerAssert, transformRuntime); + + return options; } return function (code, filename, hash) { @@ -61,14 +74,15 @@ CachingPrecompiler.prototype._createEspowerPlugin = function (babel) { }); }; -CachingPrecompiler.prototype._createTransform = function () { +CachingPrecompiler.prototype._createTransform = function (babelConfig) { return cachingTransform({ - factory: this._factory.bind(this), + factory: this._factory.bind(this, babelConfig), cacheDir: this.cacheDir, salt: new Buffer(JSON.stringify({ 'babel-plugin-espower': require('babel-plugin-espower/package.json').version, 'ava': require('../package.json').version, - 'babel-core': require('babel-core/package.json').version + 'babel-core': require('babel-core/package.json').version, + 'babelConfig': babelConfig })), ext: '.js', hash: this._hash.bind(this) diff --git a/profile.js b/profile.js index 273d1ecf4..524d044ff 100644 --- a/profile.js +++ b/profile.js @@ -20,7 +20,11 @@ globals.setTimeout = setTimeout.bind(null); globals.clearTimeout = clearTimeout.bind(null); Promise.longStackTraces(); -var conf = pkgConf.sync('ava'); +var conf = pkgConf.sync('ava', { + defaults: { + babel: 'default' + } +}); // Define a minimal set of options from the main CLI. var cli = meow([ @@ -63,7 +67,7 @@ var opts = { require: arrify(cli.flags.require), tty: false, cacheDir: cacheDir, - precompiled: new CachingPrecompiler(cacheDir).generateHashForFile(file) + precompiled: new CachingPrecompiler(cacheDir, conf.babel).generateHashForFile(file) }; var events = new EventEmitter(); diff --git a/readme.md b/readme.md index 567b9c62a..56dab8438 100644 --- a/readme.md +++ b/readme.md @@ -154,7 +154,8 @@ All of the CLI options can be configured in the `ava` section of your `package.j "tap": true, "require": [ "babel-register" - ] + ], + "babel": "inherit" } } ``` @@ -423,6 +424,61 @@ AVA comes with builtin support for ES2015 through [Babel 6](https://babeljs.io). AVA includes typings for TypeScript. You have to setup transpilation yourself. When you set `module` to `commonjs` in your `tsconfig.json` file, TypeScript will automatically find the type definitions for AVA. You should set `target` to `es2015` to use Promises and async functions. +### Babel Configuration for Test Scripts + +If you want to customize the babel transpiler for test files, you can do so by adding a `"babel"` key to the `ava` section in your `package.json` file. + +```json +{ + "ava": { + "babel": { + "presets": [ + "es2015", + "stage-0", + "react" + ] + } + }, +} +``` + +In addition to specifying a custom Babel config, you can also use the special `"inherit"` keyword. When you do this, AVA will allow tests to be transpiled using the configuration defined in your `.babelrc` file or in package.json/babel. This way, your test files will be transpiled using the same options as your source files, but you won't have to define the options twice. + +```json +{ + "babel": { + "presets": [ + "es2015", + "stage-0", + "react" + ] + }, + "ava": { + "babel": "inherit", + }, +} +``` + +Note: When configuring Babel for tests manually, the espower and transform-runtime plugins will be +added for you. + +## Default Babel Configuration for Test Scripts + +If you don't explicitly configure Babel for your tests using the `"babel"` key in package.json, your tests will be transpiled using AVA's default Babel configuration, which is as follows: + +```json +{ + "presets": [ + "es2015", + "stage-0", + ], + "plugins": [ + "espower", + "transform-runtime" + ] +} +``` + #### Transpiling Imported Modules AVA currently only transpiles the tests you ask it to run. *It will not transpile modules you ```import``` from outside of the test.* While there are valid reasons for taking this approach, it may not be what you expect! diff --git a/test/api.js b/test/api.js index b08a2d8a1..980d202a7 100644 --- a/test/api.js +++ b/test/api.js @@ -5,6 +5,7 @@ var rimraf = require('rimraf'); var fs = require('fs'); var test = require('tap').test; var Api = require('../api'); +var testDoublerPlugin = require('./fixture/babel-plugin-test-doubler'); test('must be called with new', function (t) { t.throws(function () { @@ -684,3 +685,33 @@ test('verify test count', function (t) { t.is(api.todoCount, 1); }); }); + +test('Custom Babel Plugin Support', function (t) { + t.plan(1); + + var api = new Api({ + babelConfig: { + presets: ['es2015', 'stage-2'], + plugins: [testDoublerPlugin] + } + }); + + api.run([path.join(__dirname, 'fixture/es2015.js')]) + .then( + function () { + t.is(api.passCount, 2); + }, + t.threw + ); +}); + +test('Default babel config doesn\'t use .babelrc', function (t) { + t.plan(1); + + var api = new Api(); + + return api.run([path.join(__dirname, 'fixture/babelrc/test.js')]) + .then(function () { + t.is(api.passCount, 1); + }); +}); diff --git a/test/caching-precompiler.js b/test/caching-precompiler.js index 27b972911..b524ab3e1 100644 --- a/test/caching-precompiler.js +++ b/test/caching-precompiler.js @@ -3,6 +3,9 @@ var fs = require('fs'); var path = require('path'); var test = require('tap').test; var uniqueTempDir = require('unique-temp-dir'); +var sinon = require('sinon'); +var babel = require('babel-core'); +var transformRuntime = require('babel-plugin-transform-runtime'); var CachingPrecompiler = require('../lib/caching-precompiler'); @@ -18,9 +21,11 @@ function endsWithMap(filename) { return /\.js$/.test(filename); } +sinon.spy(babel, 'transform'); + test('creation with new', function (t) { var tempDir = uniqueTempDir(); - var precompiler = new CachingPrecompiler(tempDir); + var precompiler = new CachingPrecompiler(tempDir, null); t.is(precompiler.cacheDir, tempDir); t.end(); }); @@ -28,14 +33,14 @@ test('creation with new', function (t) { test('must be called with new', function (t) { t.throws(function () { var cachingPrecompiler = CachingPrecompiler; - cachingPrecompiler(uniqueTempDir()); + cachingPrecompiler(uniqueTempDir(), null); }, {message: 'Class constructor CachingPrecompiler cannot be invoked without \'new\''}); t.end(); }); test('adds files and source maps to the cache directory as needed', function (t) { var tempDir = uniqueTempDir(); - var precompiler = new CachingPrecompiler(tempDir); + var precompiler = new CachingPrecompiler(tempDir, null); t.false(fs.existsSync(tempDir), 'cache directory is not created before it is needed'); @@ -48,3 +53,67 @@ test('adds files and source maps to the cache directory as needed', function (t) t.is(files.filter(endsWithMap).length, 1, 'one .map file is saved to the cache'); t.end(); }); + +test('uses default babel options when babelConfig === "default"', function (t) { + var tempDir = uniqueTempDir(); + var precompiler = new CachingPrecompiler(tempDir, 'default'); + babel.transform.reset(); + + precompiler.precompileFile(fixture('es2015.js')); + + t.true(babel.transform.calledOnce); + var options = babel.transform.firstCall.args[1]; + + t.true('filename' in options); + t.true(options.sourceMaps); + t.false(options.ast); + t.true('inputSourceMap' in options); + t.false(options.babelrc); + t.true(Array.isArray(options.presets)); + t.true(Array.isArray(options.plugins)); + t.end(); +}); + +test('allows babel config from package.json/babel when babelConfig === "inherit"', function (t) { + var tempDir = uniqueTempDir(); + var precompiler = new CachingPrecompiler(tempDir, 'inherit'); + babel.transform.reset(); + + precompiler.precompileFile(fixture('es2015.js')); + + t.true(babel.transform.calledOnce); + var options = babel.transform.firstCall.args[1]; + + t.true('filename' in options); + t.true(options.sourceMaps); + t.false(options.ast); + t.true('inputSourceMap' in options); + t.true(options.babelrc); + t.end(); +}); + +test('uses babelConfig for babel options when babelConfig is an object', function (t) { + var tempDir = uniqueTempDir(); + var customPlugin = sinon.stub().returns({visitor: {}}); + var powerAssert = sinon.stub().returns({visitor: {}}); + var precompiler = new CachingPrecompiler(tempDir, { + presets: ['stage-2', 'es2015'], + plugins: [customPlugin] + }); + sinon.stub(precompiler, '_createEspowerPlugin').returns(powerAssert); + babel.transform.reset(); + + precompiler.precompileFile(fixture('es2015.js')); + + t.true(babel.transform.calledOnce); + var options = babel.transform.firstCall.args[1]; + + t.true('filename' in options); + t.true(options.sourceMaps); + t.false(options.ast); + t.true('inputSourceMap' in options); + t.false(options.babelrc); + t.same(options.presets, ['stage-2', 'es2015']); + t.same(options.plugins, [customPlugin, powerAssert, transformRuntime]); + t.end(); +});