diff --git a/api.js b/api.js index 3668d1d4f..6bd14efe6 100644 --- a/api.js +++ b/api.js @@ -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'); @@ -105,13 +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 Promise.resolve(); + 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 = {}; diff --git a/docs/recipes/babelrc.md b/docs/recipes/babelrc.md index ff6cd4978..317f4ff67 100644 --- a/docs/recipes/babelrc.md +++ b/docs/recipes/babelrc.md @@ -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/). @@ -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. diff --git a/lib/babel-config.js b/lib/babel-config.js index 3ef31a9e1..95f40d1f6 100644 --- a/lib/babel-config.js +++ b/lib/babel-config.js @@ -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) { @@ -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 }; diff --git a/lib/caching-precompiler.js b/lib/caching-precompiler.js index 796377f81..65e67aee3 100644 --- a/lib/caching-precompiler.js +++ b/lib/caching-precompiler.js @@ -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(); } @@ -37,8 +49,12 @@ 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); + const result = this.babel.transform(code, Object.assign(this.getBabelOptions(), { + inputSourceMap: getSourceMap(filePath, code), + filename: filePath, + sourceMaps: true, + ast: false + })); // Save source map const mapPath = path.join(this.cacheDirPath, `${hash}.js.map`); @@ -56,12 +72,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, diff --git a/package.json b/package.json index 1989e20ca..2f92a72bd 100644 --- a/package.json +++ b/package.json @@ -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", @@ -142,6 +143,7 @@ "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", @@ -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", diff --git a/profile.js b/profile.js index 68351f343..2a65587a9 100644 --- a/profile.js +++ b/profile.js @@ -13,6 +13,7 @@ const findCacheDir = require('find-cache-dir'); const uniqueTempDir = require('unique-temp-dir'); const arrify = require('arrify'); const resolveCwd = require('resolve-cwd'); +const babelConfigHelper = require('./lib/babel-config'); const CachingPrecompiler = require('./lib/caching-precompiler'); const globals = require('./lib/globals'); @@ -75,81 +76,85 @@ const cacheDir = findCacheDir({ files: [file] }) || uniqueTempDir(); -const precompiler = new CachingPrecompiler({ - path: cacheDir, - babel: conf.babel -}); - -const precompiled = {}; -precompiled[file] = precompiler.precompileFile(file); - -const opts = { - file, - failFast: cli.flags.failFast, - serial: cli.flags.serial, - tty: false, - cacheDir, - precompiled, - require: resolveModules(conf.require) -}; - -const events = new EventEmitter(); -let uncaughtExceptionCount = 0; - -// Mock the behavior of a parent process -process.send = data => { - if (data && data.ava) { - const name = data.name.replace(/^ava-/, ''); - - if (events.listeners(name).length > 0) { - events.emit(name, data.data); - } else { - console.log('UNHANDLED AVA EVENT:', name, data.data); +babelConfigHelper.build(process.cwd(), cacheDir, conf.babel, true) + .then(result => { + const precompiler = new CachingPrecompiler({ + path: cacheDir, + getBabelOptions: result.getOptions, + babelCacheKeys: result.cacheKeys + }); + + const precompiled = {}; + precompiled[file] = precompiler.precompileFile(file); + + const opts = { + file, + failFast: cli.flags.failFast, + serial: cli.flags.serial, + tty: false, + cacheDir, + precompiled, + require: resolveModules(conf.require) + }; + + const events = new EventEmitter(); + let uncaughtExceptionCount = 0; + + // Mock the behavior of a parent process + process.send = data => { + if (data && data.ava) { + const name = data.name.replace(/^ava-/, ''); + + if (events.listeners(name).length > 0) { + events.emit(name, data.data); + } else { + console.log('UNHANDLED AVA EVENT:', name, data.data); + } + + return; + } + + console.log('NON AVA EVENT:', data); + }; + + events.on('test', data => { + console.log('TEST:', data.title, data.error); + }); + + events.on('results', data => { + if (console.profileEnd) { + console.profileEnd(); + } + + console.log('RESULTS:', data.stats); + + if (process.exit) { + process.exit(data.stats.failCount + uncaughtExceptionCount); // eslint-disable-line unicorn/no-process-exit + } + }); + + events.on('stats', () => { + setImmediate(() => { + process.emit('ava-run', {}); + }); + }); + + events.on('uncaughtException', data => { + uncaughtExceptionCount++; + let stack = data && data.exception && data.exception.stack; + stack = stack || data; + console.log(stack); + }); + + // `test-worker` will read process.argv[2] for options + process.argv[2] = JSON.stringify(opts); + process.argv.length = 3; + + if (console.profile) { + console.profile('AVA test-worker process'); } - return; - } - - console.log('NON AVA EVENT:', data); -}; - -events.on('test', data => { - console.log('TEST:', data.title, data.error); -}); - -events.on('results', data => { - if (console.profileEnd) { - console.profileEnd(); - } - - console.log('RESULTS:', data.stats); - - if (process.exit) { - process.exit(data.stats.failCount + uncaughtExceptionCount); // eslint-disable-line unicorn/no-process-exit - } -}); - -events.on('stats', () => { - setImmediate(() => { - process.emit('ava-run', {}); + setImmediate(() => { + require('./lib/test-worker'); // eslint-disable-line import/no-unassigned-import + }); }); -}); - -events.on('uncaughtException', data => { - uncaughtExceptionCount++; - let stack = data && data.exception && data.exception.stack; - stack = stack || data; - console.log(stack); -}); - -// `test-worker` will read process.argv[2] for options -process.argv[2] = JSON.stringify(opts); -process.argv.length = 3; - -if (console.profile) { - console.profile('AVA test-worker process'); -} - -setImmediate(() => { - require('./lib/test-worker'); // eslint-disable-line import/no-unassigned-import -}); diff --git a/test/api.js b/test/api.js index b8a905f99..1d17cd24a 100644 --- a/test/api.js +++ b/test/api.js @@ -5,12 +5,13 @@ const figures = require('figures'); const rimraf = require('rimraf'); const test = require('tap').test; const Api = require('../api'); -const testCapitalizerPlugin = require('./fixture/babel-plugin-test-capitalizer'); +const testCapitalizerPlugin = require.resolve('./fixture/babel-plugin-test-capitalizer'); const ROOT_DIR = path.join(__dirname, '..'); function apiCreator(options) { options = options || {}; + options.babelConfig = options.babelConfig || 'default'; options.powerAssert = true; options.projectDir = options.projectDir || ROOT_DIR; options.resolveTestsFrom = options.resolveTestsFrom || options.projectDir; @@ -667,11 +668,16 @@ function generateTests(prefix, apiCreator) { return api.run([path.join(__dirname, 'fixture/caching/test.js')]) .then(() => { const files = fs.readdirSync(path.join(__dirname, 'fixture/caching/node_modules/.cache/ava')); - t.is(files.length, 2); - t.is(files.filter(endsWithJs).length, 1); + t.ok(files.length, 4); + t.is(files.filter(endsWithBin).length, 1); + t.is(files.filter(endsWithJs).length, 2); t.is(files.filter(endsWithMap).length, 1); }); + function endsWithBin(filename) { + return /\.bin$/.test(filename); + } + function endsWithJs(filename) { return /\.js$/.test(filename); } @@ -805,7 +811,8 @@ function generateTests(prefix, apiCreator) { presets: ['@ava/stage-4'], plugins: [testCapitalizerPlugin] }, - cacheEnabled: false + cacheEnabled: false, + projectDir: __dirname }); api.on('test-run', runStatus => { @@ -823,7 +830,9 @@ function generateTests(prefix, apiCreator) { test(`${prefix} Default babel config doesn't use .babelrc`, t => { t.plan(2); - const api = apiCreator(); + const api = apiCreator({ + projectDir: path.join(__dirname, 'fixture/babelrc') + }); api.on('test-run', runStatus => { runStatus.on('test', data => { @@ -831,7 +840,7 @@ function generateTests(prefix, apiCreator) { }); }); - return api.run([path.join(__dirname, 'fixture/babelrc/test.js')]) + return api.run() .then(result => { t.is(result.passCount, 1); }); @@ -842,7 +851,8 @@ function generateTests(prefix, apiCreator) { const api = apiCreator({ babelConfig: 'inherit', - cacheEnabled: false + cacheEnabled: false, + projectDir: path.join(__dirname, 'fixture/babelrc') }); api.on('test-run', runStatus => { @@ -851,7 +861,7 @@ function generateTests(prefix, apiCreator) { }); }); - return api.run([path.join(__dirname, 'fixture/babelrc/test.js')]) + return api.run() .then(result => { t.is(result.passCount, 2); }); @@ -862,16 +872,17 @@ function generateTests(prefix, apiCreator) { const api = apiCreator({ babelConfig: {babelrc: true}, - cacheEnabled: false + cacheEnabled: false, + projectDir: path.join(__dirname, 'fixture/babelrc') }); api.on('test-run', runStatus => { runStatus.on('test', data => { - t.ok((data.title === 'foo') || (data.title === 'repeated test: foo')); + t.ok(data.title === 'foo' || data.title === 'repeated test: foo'); }); }); - return api.run([path.join(__dirname, 'fixture/babelrc/test.js')]) + return api.run() .then(result => { t.is(result.passCount, 2); }); @@ -885,41 +896,43 @@ function generateTests(prefix, apiCreator) { plugins: [testCapitalizerPlugin], babelrc: true }, - cacheEnabled: false + cacheEnabled: false, + projectDir: path.join(__dirname, 'fixture/babelrc') }); api.on('test-run', runStatus => { runStatus.on('test', data => { - t.ok((data.title === 'FOO') || /^repeated test:/.test(data.title)); + t.ok(data.title === 'FOO' || data.title === 'repeated test: FOO'); }); }); - return api.run([path.join(__dirname, 'fixture/babelrc/test.js')]) + return api.run() .then(result => { t.is(result.passCount, 2); }); }); test(`${prefix} babelConfig:{extends:path, plugins:[...]} merges plugins with .babelrc`, t => { - t.plan(2); + t.plan(3); const api = apiCreator({ babelConfig: { plugins: [testCapitalizerPlugin], extends: path.join(__dirname, 'fixture/babelrc/.alt-babelrc') }, - cacheEnabled: false + cacheEnabled: false, + projectDir: path.join(__dirname, 'fixture/babelrc') }); api.on('test-run', runStatus => { runStatus.on('test', data => { - t.is(data.title, 'BAR'); + t.ok(data.title === 'BAR' || data.title === 'repeated test: BAR'); }); }); - return api.run([path.join(__dirname, 'fixture/babelrc/test.js')]) + return api.run() .then(result => { - t.is(result.passCount, 1); + t.is(result.passCount, 2); }); }); diff --git a/test/babel-config.js b/test/babel-config.js index ad2177ab3..a12063518 100644 --- a/test/babel-config.js +++ b/test/babel-config.js @@ -2,139 +2,195 @@ const fs = require('fs'); const path = require('path'); const test = require('tap').test; -const sinon = require('sinon'); -const proxyquire = require('proxyquire').noCallThru(); +const mkdirp = require('mkdirp'); +const uniqueTempDir = require('unique-temp-dir'); +const configManager = require('hullabaloo-config-manager'); -const fixture = name => path.join(__dirname, 'fixture', name); - -function setUp() { - const customPlugin = sinon.stub().returns({visitor: {}}); - const stage4 = sinon.stub().returns({plugins: []}); - const transformTestfiles = sinon.stub().returns({plugins: []}); - - return { - customPlugin, - stage4, - transformTestfiles - }; -} - -test('uses stage-4 preset when babelConfig is "default"', t => { - const setup = setUp(); - - const babelConfigHelper = proxyquire('../lib/babel-config', { - '@ava/babel-preset-stage-4': setup.stage4, - '@ava/babel-preset-transform-test-files': setup.transformTestfiles - }); - - const babelConfig = 'default'; +const babelConfigHelper = require('../lib/babel-config'); - const fixturePath = fixture('es2015.js'); - const fixtureSource = fs.readFileSync(fixturePath, 'utf8'); +const fixture = name => path.join(__dirname, 'fixture', name); +test('uses default presets when userOptions is "default"', t => { + const userOptions = 'default'; const powerAssert = true; - const options = babelConfigHelper.build(babelConfig, powerAssert, fixturePath, fixtureSource); - - t.true('filename' in options); - t.true(options.sourceMaps); - t.false(options.ast); - t.true('inputSourceMap' in options); - t.false(options.babelrc); - const babel = {}; - t.strictEqual(options.presets[0](babel), setup.stage4()); - options.presets[1](babel); - t.strictDeepEqual(setup.transformTestfiles.args[0], [babel, {powerAssert}]); - t.end(); + + const projectDir = uniqueTempDir(); + const cacheDir = path.join(projectDir, 'cache'); + return babelConfigHelper.build(projectDir, cacheDir, userOptions, powerAssert) + .then(result => { + const options = result.getOptions(); + + t.false(options.babelrc); + t.same(options.presets, [ + require.resolve('@ava/babel-preset-stage-4'), + [ + require.resolve('@ava/babel-preset-transform-test-files'), + {powerAssert} + ] + ]); + }); }); -test('uses babelConfig for babel options when babelConfig is an object', t => { - const setup = setUp(); - const customPlugin = setup.customPlugin; +test('uses options from babelrc when userOptions is "inherit"', t => { + const userOptions = 'inherit'; + const powerAssert = true; - const babelConfigHelper = proxyquire('../lib/babel-config', { - '@ava/babel-preset-stage-4': setup.stage4, - '@ava/babel-preset-transform-test-files': setup.transformTestfiles - }); + const projectDir = fixture('babelrc'); + const cacheDir = path.join(uniqueTempDir(), 'cache'); + return babelConfigHelper.build(projectDir, cacheDir, userOptions, powerAssert) + .then(result => { + const options = result.getOptions(); + + t.false(options.babelrc); + t.same(options.plugins, [require.resolve(fixture('babel-plugin-test-doubler'))]); + t.same(options.presets, [require.resolve('@ava/babel-preset-stage-4')]); + const envOptions = options.env[configManager.currentEnv()]; + t.same(envOptions, { + presets: [ + [ + require.resolve('@ava/babel-preset-transform-test-files'), + {powerAssert} + ] + ] + }); + }); +}); - const babelConfig = { - presets: ['stage-2'], - plugins: [customPlugin] +test('uses userOptions for babel options when userOptions is an object', t => { + const custom = require.resolve(fixture('empty')); + const userOptions = { + presets: [custom], + plugins: [custom] }; - - const fixturePath = fixture('es2015.js'); - const fixtureSource = fs.readFileSync(fixturePath, 'utf8'); - const powerAssert = true; - const options = babelConfigHelper.build(babelConfig, powerAssert, fixturePath, fixtureSource); - - t.true('filename' in options); - t.true(options.sourceMaps); - t.false(options.ast); - t.true('inputSourceMap' in options); - t.false(options.babelrc); - t.strictDeepEqual(options.presets.slice(0, 1), ['stage-2']); - const babel = {}; - options.presets[1](babel); - t.strictDeepEqual(setup.transformTestfiles.args[0], [babel, {powerAssert}]); - t.strictDeepEqual(options.plugins, [customPlugin]); - t.end(); -}); -test('should reuse existing source maps', t => { - const setup = setUp(); - const customPlugin = setup.customPlugin; + const projectDir = uniqueTempDir(); + const cacheDir = path.join(projectDir, 'cache'); + return babelConfigHelper.build(projectDir, cacheDir, userOptions, powerAssert) + .then(result => { + const options = result.getOptions(); + + t.false(options.babelrc); + t.same(options.presets, userOptions.presets); + t.same(options.plugins, userOptions.plugins); + t.same(options.env.development.presets, [ + [ + require.resolve('@ava/babel-preset-transform-test-files'), + {powerAssert} + ] + ]); + }); +}); - const babelConfigHelper = proxyquire('../lib/babel-config', { - '@ava/babel-preset-stage-4': setup.stage4, - '@ava/babel-preset-transform-test-files': setup.transformTestfiles - }); +test('should disable power-assert when powerAssert is false', t => { + const userOptions = 'default'; + const powerAssert = false; - const babelConfig = { - presets: ['stage-2'], - plugins: [customPlugin] - }; + const projectDir = uniqueTempDir(); + const cacheDir = path.join(projectDir, 'cache'); + return babelConfigHelper.build(projectDir, cacheDir, userOptions, powerAssert) + .then(result => { + const options = result.getOptions(); + + t.false(options.babelrc); + t.same(options.presets, [ + require.resolve('@ava/babel-preset-stage-4'), + [ + require.resolve('@ava/babel-preset-transform-test-files'), + {powerAssert} + ] + ]); + }); +}); - const fixturePath = fixture('es2015-source-maps.js'); - const fixtureSource = fs.readFileSync(fixturePath, 'utf8'); +test('caches and uses results', t => { + const projectDir = uniqueTempDir(); + const cacheDir = path.join(projectDir, 'cache'); + return babelConfigHelper.build(projectDir, cacheDir, 'default', true) + .then(result => { + const files = fs.readdirSync(cacheDir); + t.is(files.length, 2); + t.is(files.filter(f => /\.babel-options\.js$/.test(f)).length, 1); + t.is(files.filter(f => /\.verifier\.bin$/.test(f)).length, 1); + + const firstCacheKeys = result.cacheKeys; + const stats = files.map(f => fs.statSync(path.join(cacheDir, f))); + delete stats[0].atime; + delete stats[1].atime; + + return babelConfigHelper.build(projectDir, cacheDir, 'default', true) + .then(result => { + const newStats = files.map(f => fs.statSync(path.join(cacheDir, f))); + delete newStats[0].atime; + delete newStats[1].atime; + + t.same(newStats, stats); + t.same(result.cacheKeys, firstCacheKeys); + }); + }); +}); - const powerAssert = true; - const options = babelConfigHelper.build(babelConfig, powerAssert, fixturePath, fixtureSource); - - t.true('filename' in options); - t.true(options.sourceMaps); - t.false(options.ast); - t.true('inputSourceMap' in options); - t.strictDeepEqual(options.presets.slice(0, 1), ['stage-2']); - const babel = {}; - options.presets[1](babel); - t.strictDeepEqual(setup.transformTestfiles.args[0], [babel, {powerAssert}]); - t.strictDeepEqual(options.plugins, [customPlugin]); - t.end(); +test('discards cache if userOptions change', t => { + const projectDir = uniqueTempDir(); + const cacheDir = path.join(projectDir, 'cache'); + const userOptions = {}; + return babelConfigHelper.build(projectDir, cacheDir, userOptions, true) + .then(result => { + const files = fs.readdirSync(cacheDir); + const contents = files.map(f => fs.readFileSync(path.join(cacheDir, f), 'utf8')); + const firstCacheKeys = result.cacheKeys; + + userOptions.foo = 'bar'; + return babelConfigHelper.build(projectDir, cacheDir, userOptions, true) + .then(result => { + t.notSame(files.map(f => fs.readFileSync(path.join(cacheDir, f), 'utf8')), contents); + t.notSame(result.cacheKeys, firstCacheKeys); + }); + }); }); -test('should disable power-assert when powerAssert is false', t => { - const setup = setUp(); - const customPlugin = setup.customPlugin; +test('updates cached verifier if dependency hashes change', t => { + const projectDir = uniqueTempDir(); + const cacheDir = path.join(projectDir, 'cache'); + const depFile = path.join(projectDir, 'plugin.js'); - const babelConfigHelper = proxyquire('../lib/babel-config', { - '@ava/babel-preset-stage-4': setup.stage4, - '@ava/babel-preset-transform-test-files': setup.transformTestfiles - }); + mkdirp.sync(cacheDir); + fs.writeFileSync(depFile, 'foo'); - const babelConfig = { - presets: ['stage-2'], - plugins: [customPlugin] + const userOptions = { + plugins: ['./plugin.js'] }; + return babelConfigHelper.build(projectDir, cacheDir, userOptions, true) + .then(result => { + const verifierFile = fs.readdirSync(cacheDir).find(f => /\.verifier\.bin$/.test(f)); + const contents = fs.readFileSync(path.join(cacheDir, verifierFile), 'utf8'); + const firstCacheKeys = result.cacheKeys; + + fs.writeFileSync(depFile, 'bar'); + return babelConfigHelper.build(projectDir, cacheDir, userOptions, true) + .then(result => { + t.notSame(contents, fs.readFileSync(path.join(cacheDir, verifierFile), 'utf8')); + t.notSame(result.cacheKeys.dependencies, firstCacheKeys.dependencies); + t.same(result.cacheKeys.sources, firstCacheKeys.sources); + }); + }); +}); - const fixturePath = fixture('es2015.js'); - const fixtureSource = fs.readFileSync(fixturePath, 'utf8'); - - const powerAssert = false; - const options = babelConfigHelper.build(babelConfig, powerAssert, fixturePath, fixtureSource); - - t.strictDeepEqual(options.presets.slice(0, 1), ['stage-2']); - const babel = {}; - options.presets[1](babel); - t.strictDeepEqual(setup.transformTestfiles.args[0], [babel, {powerAssert}]); - t.end(); +test('crashes if cached files cannot be read', t => { + const projectDir = uniqueTempDir(); + const cacheDir = path.join(projectDir, 'cache'); + + t.plan(1); + return babelConfigHelper.build(projectDir, cacheDir, 'default', true) + .then(() => { + for (const f of fs.readdirSync(cacheDir)) { + fs.unlinkSync(path.join(cacheDir, f)); + fs.mkdirSync(path.join(cacheDir, f)); + } + + return babelConfigHelper.build(projectDir, cacheDir, 'default', true) + .catch(err => { + t.is(err.code, 'EISDIR'); + }); + }); }); diff --git a/test/caching-precompiler.js b/test/caching-precompiler.js index ce8cd7fd1..a35822a50 100644 --- a/test/caching-precompiler.js +++ b/test/caching-precompiler.js @@ -12,18 +12,26 @@ const fixture = name => path.join(__dirname, 'fixture', name); const endsWithJs = filename => /\.js$/.test(filename); const endsWithMap = filename => /\.js\.map$/.test(filename); +function getBabelOptions() { + return { + babelrc: false + }; +} + +const babelCacheKeys = {}; + sinon.spy(babel, 'transform'); test('creation with new', t => { const tempDir = uniqueTempDir(); - const precompiler = new CachingPrecompiler({path: tempDir}); + const precompiler = new CachingPrecompiler({path: tempDir, getBabelOptions, babelCacheKeys}); t.is(precompiler.cacheDirPath, tempDir); t.end(); }); test('adds files and source maps to the cache directory as needed', t => { const tempDir = uniqueTempDir(); - const precompiler = new CachingPrecompiler({path: tempDir}); + const precompiler = new CachingPrecompiler({path: tempDir, getBabelOptions, babelCacheKeys}); t.false(fs.existsSync(tempDir), 'cache directory is not created before it is needed'); @@ -39,7 +47,7 @@ test('adds files and source maps to the cache directory as needed', t => { test('adds a map file comment to the cached files', t => { const tempDir = uniqueTempDir(); - const precompiler = new CachingPrecompiler({path: tempDir}); + const precompiler = new CachingPrecompiler({path: tempDir, getBabelOptions, babelCacheKeys}); precompiler.precompileFile(fixture('es2015.js')); @@ -63,59 +71,12 @@ test('adds a map file comment to the cached files', t => { t.end(); }); -test('uses default babel options when babelConfig === "default"', t => { +test('should reuse existing source maps', t => { const tempDir = uniqueTempDir(); - const precompiler = new CachingPrecompiler({ - path: tempDir, - babel: 'default' - }); - - babel.transform.reset(); - - precompiler.precompileFile(fixture('es2015.js')); + const precompiler = new CachingPrecompiler({path: tempDir, getBabelOptions, babelCacheKeys}); - t.true(babel.transform.calledOnce); - const 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.end(); -}); - -test('allows babel config from package.json/babel when babelConfig === "inherit"', t => { - const tempDir = uniqueTempDir(); - const precompiler = new CachingPrecompiler({ - path: tempDir, - babel: 'inherit' - }); - - babel.transform.reset(); - - precompiler.precompileFile(fixture('es2015.js')); - - t.true(babel.transform.calledOnce); - const 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('does not modify plugins array in babelConfig', t => { - const plugins = []; - const precompiler = new CachingPrecompiler({ - path: uniqueTempDir(), - plugins - }); - - precompiler.precompileFile(fixture('es2015.js')); - t.strictDeepEqual(plugins, []); + precompiler.precompileFile(fixture('es2015-source-maps.js')); + const options = babel.transform.lastCall.args[1]; + t.ok(options.inputSourceMap); t.end(); }); diff --git a/test/fork.js b/test/fork.js index 256e65fca..b22d9981f 100644 --- a/test/fork.js +++ b/test/fork.js @@ -5,7 +5,16 @@ const _fork = require('../lib/fork.js'); const CachingPrecompiler = require('../lib/caching-precompiler'); const cacheDir = path.join(__dirname, '../node_modules/.cache/ava'); -const precompiler = new CachingPrecompiler({path: cacheDir}); +const precompiler = new CachingPrecompiler({ + babelCacheKeys: {}, + getBabelOptions() { + return { + babelrc: false, + presets: [require.resolve('@ava/babel-preset-stage-4')] + }; + }, + path: cacheDir +}); function fork(testPath) { const hash = precompiler.precompileFile(testPath); diff --git a/test/hooks.js b/test/hooks.js index 7bdc9f1fa..0981396a5 100644 --- a/test/hooks.js +++ b/test/hooks.js @@ -6,7 +6,16 @@ const _fork = require('../lib/fork.js'); const CachingPrecompiler = require('../lib/caching-precompiler'); const cacheDir = path.join(__dirname, '../node_modules/.cache/ava'); -const precompiler = new CachingPrecompiler({path: cacheDir}); +const precompiler = new CachingPrecompiler({ + babelCacheKeys: {}, + getBabelOptions() { + return { + babelrc: false, + presets: [require.resolve('@ava/babel-preset-stage-4')] + }; + }, + path: cacheDir +}); function fork(testPath) { const hash = precompiler.precompileFile(testPath);