diff --git a/api.js b/api.js index 058ab6523..27407a2dd 100644 --- a/api.js +++ b/api.js @@ -3,8 +3,6 @@ var EventEmitter = require('events').EventEmitter; var path = require('path'); var util = require('util'); var commonPathPrefix = require('common-path-prefix'); -var uniqueTempDir = require('unique-temp-dir'); -var findCacheDir = require('find-cache-dir'); var objectAssign = require('object-assign'); var resolveCwd = require('resolve-cwd'); var debounce = require('lodash.debounce'); @@ -14,9 +12,10 @@ var Promise = require('bluebird'); var getPort = require('get-port'); var arrify = require('arrify'); var ms = require('ms'); -var CachingPrecompiler = require('./lib/caching-precompiler'); var RunStatus = require('./lib/run-status'); var AvaError = require('./lib/ava-error'); +var babelBundle = require('./lib/babel/bundle'); +var babelConfig = require('./lib/babel/config'); var fork = require('./lib/fork'); function resolveModules(modules) { @@ -59,21 +58,14 @@ function Api(options) { }, options); this.options.require = resolveModules(this.options.require); + this.options.babel = babelConfig.build(options.babel); } util.inherits(Api, EventEmitter); module.exports = Api; Api.prototype._runFile = function (file, runStatus, execArgv) { - var hash = this.precompiler.precompileFile(file); - var precompiled = {}; - precompiled[file] = hash; - - var options = objectAssign({}, this.options, { - precompiled: precompiled - }); - - var emitter = fork(file, options, execArgv); + var emitter = fork(file, this.options, execArgv); runStatus.observeFork(emitter); return emitter; @@ -85,7 +77,10 @@ Api.prototype.run = function (files, options) { return new AvaFiles({cwd: this.options.resolveTestsFrom, files: files}) .findTestFiles() .then(function (files) { - return self._run(files, options); + return babelBundle.create(self.options.babel).then(function (bundle) { + self.options.bundle = bundle; + return self._run(files, options); + }); }); }; @@ -113,23 +108,6 @@ Api.prototype._cancelTimeout = function (runStatus) { runStatus._restartTimer.cancel(); }; -Api.prototype._setupPrecompiler = function (files) { - var isCacheEnabled = this.options.cacheEnabled !== false; - var cacheDir = uniqueTempDir(); - - if (isCacheEnabled) { - cacheDir = findCacheDir({ - name: 'ava', - files: files - }); - } - - this.options.cacheDir = cacheDir; - - var isPowerAssertEnabled = this.options.powerAssert !== false; - this.precompiler = new CachingPrecompiler(cacheDir, this.options.babelConfig, isPowerAssertEnabled); -}; - Api.prototype._run = function (files, options) { options = options || {}; @@ -148,8 +126,6 @@ Api.prototype._run = function (files, options) { return Promise.resolve(runStatus); } - this._setupPrecompiler(files); - if (this.options.timeout) { this._setupTimeout(runStatus); } diff --git a/cli.js b/cli.js index 22bcb26e4..95020b1ee 100755 --- a/cli.js +++ b/cli.js @@ -13,186 +13,16 @@ var localCLI = resolveCwd('ava/cli'); // see https://github.com/nodejs/node/issues/6624 if (localCLI && path.relative(localCLI, __filename) !== '') { debug('Using local install of AVA'); - require(localCLI); - return; -} - -if (debug.enabled) { - require('time-require'); -} - -var updateNotifier = require('update-notifier'); -var figures = require('figures'); -var arrify = require('arrify'); -var meow = require('meow'); -var Promise = require('bluebird'); -var pkgConf = require('pkg-conf'); -var isCi = require('is-ci'); -var hasFlag = require('has-flag'); -var colors = require('./lib/colors'); -var verboseReporter = require('./lib/reporters/verbose'); -var miniReporter = require('./lib/reporters/mini'); -var tapReporter = require('./lib/reporters/tap'); -var Logger = require('./lib/logger'); -var Watcher = require('./lib/watcher'); -var babelConfig = require('./lib/babel-config'); -var Api = require('./api'); - -// Bluebird specific -Promise.longStackTraces(); - -var conf = pkgConf.sync('ava'); - -var pkgDir = path.dirname(pkgConf.filepath(conf)); - -try { - conf.babel = babelConfig.validate(conf.babel); -} catch (err) { - console.log('\n ' + err.message); - process.exit(1); -} - -var cli = meow([ - 'Usage', - ' ava [ ...]', - '', - 'Options', - ' --init Add AVA to your project', - ' --fail-fast Stop after first test failure', - ' --serial, -s Run tests serially', - ' --tap, -t Generate TAP output', - ' --verbose, -v Enable verbose output', - ' --no-cache Disable the transpiler cache', - ' --no-power-assert Disable Power Assert', - ' --match, -m Only run tests with matching title (Can be repeated)', - ' --watch, -w Re-run tests when tests and source files change', - ' --source, -S Pattern to match source files so tests can be re-run (Can be repeated)', - ' --timeout, -T Set global timeout', - ' --concurrency, -c Maximum number of test files running at the same time (EXPERIMENTAL)', - '', - 'Examples', - ' ava', - ' ava test.js test2.js', - ' ava test-*.js', - ' ava test', - ' ava --init', - ' ava --init foo.js', - '', - 'Default patterns when no arguments:', - 'test.js test-*.js test/**/*.js **/__tests__/**/*.js **/*.test.js' -], { - string: [ - '_', - 'timeout', - 'source', - 'match', - 'concurrency' - ], - boolean: [ - 'fail-fast', - 'verbose', - 'serial', - 'tap', - 'watch' - ], - default: conf, - alias: { - t: 'tap', - v: 'verbose', - s: 'serial', - m: 'match', - w: 'watch', - S: 'source', - T: 'timeout', - c: 'concurrency' - } -}); - -updateNotifier({pkg: cli.pkg}).notify(); - -if (cli.flags.init) { - require('ava-init')(); - return; -} - -if ( - ((hasFlag('--watch') || hasFlag('-w')) && (hasFlag('--tap') || hasFlag('-t'))) || - (conf.watch && conf.tap) -) { - console.error(' ' + colors.error(figures.cross) + ' The TAP reporter is not available when using watch mode.'); - process.exit(1); -} - -if (hasFlag('--require') || hasFlag('-r')) { - console.error(' ' + colors.error(figures.cross) + ' The --require and -r flags are deprecated. Requirements should be configured in package.json - see documentation.'); - process.exit(1); -} - -var api = new Api({ - failFast: cli.flags.failFast, - serial: cli.flags.serial, - require: arrify(conf.require), - cacheEnabled: cli.flags.cache !== false, - powerAssert: cli.flags.powerAssert !== false, - explicitTitles: cli.flags.watch, - match: arrify(cli.flags.match), - babelConfig: conf.babel, - resolveTestsFrom: cli.input.length === 0 ? pkgDir : process.cwd(), - timeout: cli.flags.timeout, - concurrency: cli.flags.concurrency ? parseInt(cli.flags.concurrency, 10) : 0 -}); - -var reporter; - -if (cli.flags.tap && !cli.flags.watch) { - reporter = tapReporter(); -} else if (cli.flags.verbose || isCi) { - reporter = verboseReporter(); + require(localCLI); // eslint-disable-line import/no-dynamic-require } else { - reporter = miniReporter({watching: cli.flags.watch}); -} - -reporter.api = api; -var logger = new Logger(reporter); - -logger.start(); - -api.on('test-run', function (runStatus) { - reporter.api = runStatus; - runStatus.on('test', logger.test); - runStatus.on('error', logger.unhandledError); - - runStatus.on('stdout', logger.stdout); - runStatus.on('stderr', logger.stderr); -}); - -var files = cli.input.length ? cli.input : arrify(conf.files); + if (debug.enabled) { + require('time-require'); // eslint-disable-line import/no-unassigned-import + } -if (cli.flags.watch) { try { - var watcher = new Watcher(logger, api, files, arrify(cli.flags.source)); - watcher.observeStdin(process.stdin); + require('./lib/cli').run(); } catch (err) { - if (err.name === 'AvaError') { - // An AvaError may be thrown if chokidar is not installed. Log it nicely. - console.error(' ' + colors.error(figures.cross) + ' ' + err.message); - logger.exit(1); - } else { - // Rethrow so it becomes an uncaught exception. - throw err; - } + console.error('\n ' + err.message); + process.exit(1); } -} else { - api.run(files) - .then(function (runStatus) { - logger.finish(runStatus); - logger.exit(runStatus.failCount > 0 || runStatus.rejectionCount > 0 || runStatus.exceptionCount > 0 ? 1 : 0); - }) - .catch(function (err) { - // Don't swallow exceptions. Note that any expected error should already - // have been logged. - setImmediate(function () { - throw err; - }); - }); } diff --git a/lib/babel-config.js b/lib/babel-config.js deleted file mode 100644 index 1bfdafb7f..000000000 --- a/lib/babel-config.js +++ /dev/null @@ -1,152 +0,0 @@ -'use strict'; -var path = require('path'); -var chalk = require('chalk'); -var figures = require('figures'); -var convertSourceMap = require('convert-source-map'); -var objectAssign = require('object-assign'); -var semver = require('semver'); -var colors = require('./colors'); - -function validate(conf) { - if (conf === undefined || conf === null) { - conf = 'default'; - } - - // check for valid babel config shortcuts (can be either "default" or "inherit") - var isValidShortcut = conf === 'default' || conf === 'inherit'; - - if (!conf || (typeof conf === 'string' && !isValidShortcut)) { - var message = colors.error(figures.cross); - message += ' Unexpected Babel configuration for AVA. '; - message += 'See ' + chalk.underline('https://github.com/avajs/ava#es2015-support') + ' for allowed values.'; - - throw new Error(message); - } - - return conf; -} - -function lazy(initFn) { - var initialized = false; - var value; - - return function () { - if (!initialized) { - initialized = true; - value = initFn(); - } - - return value; - }; -} - -var defaultPresets = lazy(function () { - var esPreset = semver.satisfies(process.version, '>=4') ? - 'babel-preset-es2015-node4' : - 'babel-preset-es2015'; - - return [ - require('babel-preset-stage-2'), - require(esPreset) // eslint-disable-line import/no-dynamic-require - ]; -}); - -var rewritePlugin = lazy(function () { - var wrapListener = require('babel-plugin-detective/wrap-listener'); - - return wrapListener(rewriteBabelRuntimePaths, 'rewrite-runtime', { - generated: true, - require: true, - import: true - }); -}); - -function rewriteBabelRuntimePaths(path) { - var isBabelPath = /^babel-runtime[\\\/]?/.test(path.node.value); - - if (path.isLiteral() && isBabelPath) { - path.node.value = require.resolve(path.node.value); - } -} - -var espowerPlugin = lazy(function () { - var babel = require('babel-core'); - var createEspowerPlugin = require('babel-plugin-espower/create'); - - // initialize power-assert - return createEspowerPlugin(babel, { - embedAst: true, - patterns: require('./enhance-assert').PATTERNS - }); -}); - -var defaultPlugins = lazy(function () { - return [ - require('babel-plugin-ava-throws-helper'), - rewritePlugin(), - require('babel-plugin-transform-runtime') - ]; -}); - -function build(babelConfig, powerAssert, filePath, code) { - babelConfig = validate(babelConfig); - - var options; - - if (babelConfig === 'default') { - options = { - babelrc: false, - presets: defaultPresets() - }; - } else if (babelConfig === 'inherit') { - options = { - babelrc: true - }; - } else { - options = { - babelrc: false - }; - - objectAssign(options, babelConfig); - } - - var sourceMap = getSourceMap(filePath, code); - - objectAssign(options, { - inputSourceMap: sourceMap, - filename: filePath, - sourceMaps: true, - ast: false - }); - - options.plugins = (options.plugins || []) - .concat(powerAssert ? espowerPlugin() : []) - .concat(defaultPlugins()); - - return options; -} - -function getSourceMap(filePath, code) { - var sourceMap = convertSourceMap.fromSource(code); - - if (!sourceMap) { - var dirPath = path.dirname(filePath); - - sourceMap = convertSourceMap.fromMapFileSource(code, dirPath); - } - - if (sourceMap) { - sourceMap = sourceMap.toObject(); - } - - return sourceMap; -} - -module.exports = { - validate: validate, - build: build, - pluginPackages: [ - require.resolve('babel-core/package.json'), - require.resolve('babel-plugin-espower/package.json') - ] -}; diff --git a/lib/babel/bundle.js b/lib/babel/bundle.js new file mode 100644 index 000000000..704dad4c1 --- /dev/null +++ b/lib/babel/bundle.js @@ -0,0 +1,51 @@ +'use strict'; + +var fs = require('fs'); +var path = require('path'); + +var Promise = require('bluebird'); +var cml = require('cached-module-loader'); +var findCacheDir = require('find-cache-dir'); + +var WORKER = require.resolve('./worker'); + +function create(babelConfig) { + var cacheDir = findCacheDir({name: 'ava', create: true}); + var cachedDataFile = path.join(cacheDir, 'bundled-cachedData.js'); + var codeFile = path.join(cacheDir, 'bundled-code.js'); + + if (fs.existsSync(cachedDataFile) && fs.existsSync(codeFile)) { + return Promise.resolve({cachedDataFile: cachedDataFile, codeFile: codeFile}); + } + + var start = Date.now(); + var srcFile = path.join(cacheDir, 'faux-src.js'); + var testFile = path.join(cacheDir, 'faux-test.js'); + var stubFile = path.join(cacheDir, 'bundle-stub.js'); + + fs.writeFileSync(srcFile, ''); + fs.writeFileSync(testFile, 'require("./faux-src")'); + fs.writeFileSync(stubFile, [ + 'var babelWorker = require(' + JSON.stringify(WORKER) + ')', + 'babelWorker.runTest(' + JSON.stringify(testFile) + ', ' + JSON.stringify(babelConfig) + ')' + ].join('\n')); + + return cml.bundleDependencies(stubFile).then(function (bundle) { + fs.writeFileSync(cachedDataFile, bundle.cachedData); + fs.writeFileSync(codeFile, bundle.code); + console.error('bundling took', Date.now() - start); + return {cachedDataFile: cachedDataFile, codeFile: codeFile}; + }); +} +exports.create = create; + +function loadWorker(bundle) { + var start = Date.now(); + var worker = cml.loadInThisContext(WORKER, module, { + cachedData: new Buffer(bundle.cachedData, 'base64'), + code: new Buffer(bundle.code, 'base64') + }); + console.error('loading took', Date.now() - start); + return worker; +} +exports.loadWorker = loadWorker; diff --git a/lib/babel/cache-worker.js b/lib/babel/cache-worker.js new file mode 100644 index 000000000..df54fa639 --- /dev/null +++ b/lib/babel/cache-worker.js @@ -0,0 +1,34 @@ +'use strict'; +var fs = require('fs'); +var path = require('path'); +var js = require('default-require-extensions/js'); +var findCacheDir = require('find-cache-dir'); +var md5hex = require('md5-hex'); + +var register = require('./register'); + +function runTest(opts) { + var testPath = opts.testPath; + var code = fs.readFileSync(testPath); + var hash = md5hex(code); + + var cacheDir = findCacheDir({name: 'ava', create: true}); + var cachedFile = path.join(cacheDir, hash + '.js'); + if (!fs.existsSync(cachedFile)) { + return opts.loadWorker().runTest(testPath, opts.babelConfig); + } + + require.extensions['.js'] = function (module) { + register.install(); + + var oldCompile = module._compile; + module._compile = function () { + module._compile = oldCompile; + module._compile(fs.readFileSync(cachedFile, 'utf8'), testPath); + }; + + js(module, testPath); + }; + require(testPath); // eslint-disable-line import/no-dynamic-require +} +exports.runTest = runTest; diff --git a/lib/babel/config.js b/lib/babel/config.js new file mode 100644 index 000000000..1c7bb87e0 --- /dev/null +++ b/lib/babel/config.js @@ -0,0 +1,95 @@ +'use strict'; +var chalk = require('chalk'); +var figures = require('figures'); +var objectAssign = require('object-assign'); +var semver = require('semver'); + +var colors = require('../colors'); + +var ESPOWER = require.resolve('./plugin-espower'); +var REWRITE_RUNTIME = require.resolve('./plugin-rewrite-runtime'); + +var DEFAULT_PRESETS = (function () { + var esPreset = semver.satisfies(process.version, '>=4') ? + 'babel-preset-es2015-node4' : + 'babel-preset-es2015'; + + return [ + 'babel-preset-stage-2', + esPreset + ]; +})(); + +var REQUIRED_PLUGINS = [ + 'babel-plugin-ava-throws-helper', + REWRITE_RUNTIME, + 'babel-plugin-transform-runtime' +]; + +function validate(conf) { + if (conf === undefined || conf === null) { + conf = 'default'; + } + + // check for valid babel config shortcuts (can be either "default" or "inherit") + var isValidShortcut = conf === 'default' || conf === 'inherit'; + + if (!conf || (typeof conf === 'string' && !isValidShortcut)) { + var message = colors.error(figures.cross); + message += ' Unexpected Babel configuration for AVA. '; + message += 'See ' + chalk.underline('https://github.com/avajs/ava#es2015-support') + ' for allowed values.'; + + throw new Error(message); + } + + return conf; +} +exports.validate = validate; + +function resolve(plugin) { + return require.resolve(plugin); +} + +function build(conf, usePowerAssert) { + conf = validate(conf); + + var options; + + if (conf === 'default') { + options = { + babelrc: false, + presets: DEFAULT_PRESETS.map(resolve) + }; + } else if (conf === 'inherit') { + options = { + babelrc: true + }; + } else { + options = { + babelrc: false + }; + + objectAssign(options, conf); + } + + options.plugins = (options.plugins || []) + .concat(usePowerAssert ? [ESPOWER] : []) + .concat(REQUIRED_PLUGINS.map(resolve)); + + return options; +} +exports.build = build; + +exports.pluginPackages = DEFAULT_PRESETS + .concat( + REQUIRED_PLUGINS.filter(function (plugin) { + return plugin !== REWRITE_RUNTIME; + }) + ) + // Dependency of ESPOWER + .concat(['babel-plugin-espower']) + // Dependency of REWRITE_RUNTIME + .concat(['babel-plugin-detective']) + .map(function (plugin) { + return require.resolve(plugin + '/package.json'); + }); diff --git a/lib/babel/plugin-espower.js b/lib/babel/plugin-espower.js new file mode 100644 index 000000000..7e63a7655 --- /dev/null +++ b/lib/babel/plugin-espower.js @@ -0,0 +1,9 @@ +var babel = require('babel-core'); +var createEspowerPlugin = require('babel-plugin-espower/create'); +var patterns = require('../enhance-assert').PATTERNS; + +// initialize power-assert +module.exports = createEspowerPlugin(babel, { + embedAst: true, + patterns: patterns +}); diff --git a/lib/babel/plugin-rewrite-runtime.js b/lib/babel/plugin-rewrite-runtime.js new file mode 100644 index 000000000..1780ff9d3 --- /dev/null +++ b/lib/babel/plugin-rewrite-runtime.js @@ -0,0 +1,17 @@ +'use strict'; + +var wrapListener = require('babel-plugin-detective/wrap-listener'); + +module.exports = wrapListener(rewriteBabelRuntimePaths, 'rewrite-runtime', { + generated: true, + require: true, + import: true +}); + +function rewriteBabelRuntimePaths(path) { + var isBabelPath = /^babel-runtime[\\\/]?/.test(path.node.value); + + if (path.isLiteral() && isBabelPath) { + path.node.value = require.resolve(path.node.value); + } +} diff --git a/lib/babel/register.js b/lib/babel/register.js new file mode 100644 index 000000000..54925add6 --- /dev/null +++ b/lib/babel/register.js @@ -0,0 +1,46 @@ +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var js = require('default-require-extensions/js'); +var findCacheDir = require('find-cache-dir'); +var md5hex = require('md5-hex'); + +var babel; +function install() { + var cacheDir = findCacheDir({name: 'ava', create: true}); + + require.extensions['.js'] = function (module, filename) { + if (/node_modules/.test(filename)) { + return js(module, filename); + } + + var oldCompile = module._compile; + module._compile = function (code, filename) { + module._compile = oldCompile; + + var hash = md5hex(code); + var cachedFile = path.join(cacheDir, hash + '.js'); + if (fs.existsSync(cachedFile)) { + module._compile(fs.readFileSync(cachedFile, 'utf8'), filename); + } else { + if (!babel) { + // TODO: Load from the bundle instead! + babel = require('babel-core'); + } + + var result = babel.transform(code, { + filename: filename, + sourceMaps: true, + ast: false, + babelrc: true + }); + fs.writeFileSync(cachedFile, result.code); + module._compile(result.code, filename); + } + }; + + return js(module, filename); + }; +} +exports.install = install; diff --git a/lib/babel/worker.js b/lib/babel/worker.js new file mode 100644 index 000000000..8d487502d --- /dev/null +++ b/lib/babel/worker.js @@ -0,0 +1,44 @@ +'use strict'; +var fs = require('fs'); +var path = require('path'); + +var babel = require('babel-core'); +var js = require('default-require-extensions/js'); +var findCacheDir = require('find-cache-dir'); +var md5hex = require('md5-hex'); +var objectAssign = require('object-assign'); + +var register = require('./register'); + +function runTest(testPath, babelConfig) { + var options = objectAssign({ + filename: testPath, + sourceMaps: true, + ast: false, + babelrc: false + }, babelConfig); + + var start = Date.now(); + var code = fs.readFileSync(testPath, 'utf8'); + var result = babel.transform(code, options); + console.error('transformation took', Date.now() - start); + + var hash = md5hex(code); + var cacheDir = findCacheDir({name: 'ava', create: true}); + var cachedFile = path.join(cacheDir, hash + '.js'); + fs.writeFileSync(cachedFile, result.code); + + require.extensions['.js'] = function (module) { + register.install(); + + var oldCompile = module._compile; + module._compile = function () { + module._compile = oldCompile; + module._compile(result.code, testPath); + }; + + js(module, testPath); + }; + require(testPath); // eslint-disable-line import/no-dynamic-require +} +exports.runTest = runTest; diff --git a/lib/cli.js b/lib/cli.js new file mode 100644 index 000000000..701b7ccf6 --- /dev/null +++ b/lib/cli.js @@ -0,0 +1,169 @@ +'use strict'; +var path = require('path'); +var updateNotifier = require('update-notifier'); +var figures = require('figures'); +var arrify = require('arrify'); +var meow = require('meow'); +var Promise = require('bluebird'); +var pkgConf = require('pkg-conf'); +var isCi = require('is-ci'); +var hasFlag = require('has-flag'); +var Api = require('../api'); +var colors = require('./colors'); +var verboseReporter = require('./reporters/verbose'); +var miniReporter = require('./reporters/mini'); +var tapReporter = require('./reporters/tap'); +var Logger = require('./logger'); +var Watcher = require('./watcher'); +var babelConfig = require('./babel/config'); + +// Bluebird specific +Promise.longStackTraces(); + +exports.run = function () { + var conf = pkgConf.sync('ava'); + var pkgDir = path.dirname(pkgConf.filepath(conf)); + + var cli = meow([ + 'Usage', + ' ava [ ...]', + '', + 'Options', + ' --init Add AVA to your project', + ' --fail-fast Stop after first test failure', + ' --serial, -s Run tests serially', + ' --tap, -t Generate TAP output', + ' --verbose, -v Enable verbose output', + ' --no-cache Disable the transpiler cache', + ' --no-power-assert Disable Power Assert', + ' --match, -m Only run tests with matching title (Can be repeated)', + ' --watch, -w Re-run tests when tests and source files change', + ' --source, -S Pattern to match source files so tests can be re-run (Can be repeated)', + ' --timeout, -T Set global timeout', + ' --concurrency, -c Maximum number of test files running at the same time (EXPERIMENTAL)', + '', + 'Examples', + ' ava', + ' ava test.js test2.js', + ' ava test-*.js', + ' ava test', + ' ava --init', + ' ava --init foo.js', + '', + 'Default patterns when no arguments:', + 'test.js test-*.js test/**/*.js **/__tests__/**/*.js **/*.test.js' + ], { + string: [ + '_', + 'timeout', + 'source', + 'match', + 'concurrency' + ], + boolean: [ + 'fail-fast', + 'verbose', + 'serial', + 'tap', + 'watch' + ], + default: conf, + alias: { + t: 'tap', + v: 'verbose', + s: 'serial', + m: 'match', + w: 'watch', + S: 'source', + T: 'timeout', + c: 'concurrency' + } + }); + + updateNotifier({pkg: cli.pkg}).notify(); + + if (cli.flags.init) { + require('ava-init')(); + return; + } + + if ( + ((hasFlag('--watch') || hasFlag('-w')) && (hasFlag('--tap') || hasFlag('-t'))) || + (conf.watch && conf.tap) + ) { + throw new Error(colors.error(figures.cross) + ' The TAP reporter is not available when using watch mode.'); + } + + if (hasFlag('--require') || hasFlag('-r')) { + throw new Error(colors.error(figures.cross) + ' The --require and -r flags are deprecated. Requirements should be configured in package.json - see documentation.'); + } + + var api = new Api({ + failFast: cli.flags.failFast, + serial: cli.flags.serial, + require: arrify(conf.require), + cacheEnabled: cli.flags.cache !== false, + powerAssert: cli.flags.powerAssert !== false, + explicitTitles: cli.flags.watch, + match: arrify(cli.flags.match), + babelConfig: babelConfig.validate(conf.babel), + resolveTestsFrom: cli.input.length === 0 ? pkgDir : process.cwd(), + timeout: cli.flags.timeout, + concurrency: cli.flags.concurrency ? parseInt(cli.flags.concurrency, 10) : 0 + }); + + var reporter; + + if (cli.flags.tap && !cli.flags.watch) { + reporter = tapReporter(); + } else if (cli.flags.verbose || isCi) { + reporter = verboseReporter(); + } else { + reporter = miniReporter({watching: cli.flags.watch}); + } + + reporter.api = api; + var logger = new Logger(reporter); + + logger.start(); + + api.on('test-run', function (runStatus) { + reporter.api = runStatus; + runStatus.on('test', logger.test); + runStatus.on('error', logger.unhandledError); + + runStatus.on('stdout', logger.stdout); + runStatus.on('stderr', logger.stderr); + }); + + var files = cli.input.length ? cli.input : arrify(conf.files); + + if (cli.flags.watch) { + try { + var watcher = new Watcher(logger, api, files, arrify(cli.flags.source)); + watcher.observeStdin(process.stdin); + } catch (err) { + if (err.name === 'AvaError') { + // An AvaError may be thrown if chokidar is not installed. Log it nicely. + console.error(' ' + colors.error(figures.cross) + ' ' + err.message); + logger.exit(1); + } else { + // Rethrow so it becomes an uncaught exception. + throw err; + } + } + } else { + api.run(files) + .then(function (runStatus) { + logger.finish(runStatus); + logger.exit(runStatus.failCount > 0 || runStatus.rejectionCount > 0 || runStatus.exceptionCount > 0 ? 1 : 0); + }) + .catch(function (err) { + // Don't swallow exceptions. Note that any expected error should already + // have been logged. + setImmediate(function () { + throw err; + }); + }); + } +}; diff --git a/lib/logger.js b/lib/logger.js index c79012698..14d705d2e 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -98,6 +98,6 @@ Logger.prototype.exit = function (code) { // timeout required to correctly flush IO on Node.js 0.10 on Windows setTimeout(function () { - process.exit(code); // eslint-disable-line xo/no-process-exit + process.exit(code); // eslint-disable-line unicorn/no-process-exit }, process.env.AVA_APPVEYOR ? 500 : 0); }; diff --git a/lib/process-adapter.js b/lib/process-adapter.js index 1e73b591d..3528f4adf 100644 --- a/lib/process-adapter.js +++ b/lib/process-adapter.js @@ -1,9 +1,6 @@ 'use strict'; -var fs = require('fs'); var path = require('path'); var chalk = require('chalk'); -var sourceMapSupport = require('source-map-support'); -var installPrecompiler = require('require-precompiled'); var debug = require('debug')('ava'); @@ -16,7 +13,7 @@ if (!isForked) { console.log(); console.error('Test files must be run with the AVA CLI:\n\n ' + chalk.grey.dim('$') + ' ' + chalk.cyan('ava ' + fp) + '\n'); - process.exit(1); // eslint-disable-line xo/no-process-exit + process.exit(1); // eslint-disable-line unicorn/no-process-exit } exports.send = function (name, data) { @@ -62,37 +59,6 @@ if (debug.enabled) { require('time-require'); // eslint-disable-line import/no-unassigned-import } -var sourceMapCache = Object.create(null); -var cacheDir = opts.cacheDir; - -exports.installSourceMapSupport = function () { - sourceMapSupport.install({ - environment: 'node', - handleUncaughtExceptions: false, - retrieveSourceMap: function (source) { - if (sourceMapCache[source]) { - return { - url: source, - map: fs.readFileSync(sourceMapCache[source], 'utf8') - }; - } - } - }); -}; - -exports.installPrecompilerHook = function () { - installPrecompiler(function (filename) { - var precompiled = opts.precompiled[filename]; - - if (precompiled) { - sourceMapCache[filename] = path.join(cacheDir, precompiled + '.js.map'); - return fs.readFileSync(path.join(cacheDir, precompiled + '.js'), 'utf8'); - } - - return null; - }); -}; - exports.installDependencyTracking = function (dependencies, testPath) { Object.keys(require.extensions).forEach(function (ext) { var wrappedHandler = require.extensions[ext]; diff --git a/lib/reporters/mini.js b/lib/reporters/mini.js index c5d53415f..8c7d9fbe8 100644 --- a/lib/reporters/mini.js +++ b/lib/reporters/mini.js @@ -159,12 +159,8 @@ MiniReporter.prototype.finish = function (runStatus) { status += '\n ' + colors.error(runStatus.previousFailCount, 'previous', plur('failure', runStatus.previousFailCount), 'in test files that were not rerun'); } - var i = 0; - if (this.knownFailureCount > 0) { runStatus.knownFailures.forEach(function (test) { - i++; - var title = test.title; status += '\n\n ' + colors.title(title); @@ -179,8 +175,6 @@ MiniReporter.prototype.finish = function (runStatus) { return; } - i++; - var title = test.error ? test.title : 'Unhandled Error'; var description; var errorTitle = ' ' + test.error.message + '\n'; @@ -210,8 +204,6 @@ MiniReporter.prototype.finish = function (runStatus) { return; } - i++; - if (err.type === 'exception' && err.name === 'AvaError') { status += '\n\n ' + colors.error(cross + ' ' + err.message); } else { diff --git a/lib/test-worker.js b/lib/test-worker.js index 2428bf6d5..9d23c0d93 100644 --- a/lib/test-worker.js +++ b/lib/test-worker.js @@ -13,11 +13,12 @@ var Promise = require('bluebird'); // Bluebird specific Promise.longStackTraces(); -(opts.require || []).forEach(require); - -process.installSourceMapSupport(); +// Ignore, assume this only installs babel-register which is now built-in. +// (opts.require || []).forEach(require); +var fs = require('fs'); var currentlyUnhandled = require('currently-unhandled')(); +var cacheWorker = require('./babel/cache-worker'); var serializeError = require('./serialize-error'); var send = process.send; var throwsHelper = require('./throws-helper'); @@ -25,12 +26,19 @@ var throwsHelper = require('./throws-helper'); // check if test files required ava and show error, when they didn't exports.avaRequired = false; -process.installPrecompilerHook(); - var dependencies = []; process.installDependencyTracking(dependencies, testPath); -require(testPath); // eslint-disable-line import/no-dynamic-require +cacheWorker.runTest({ + testPath: testPath, + babelConfig: opts.babel, + loadWorker: function () { + return require('./babel/bundle').loadWorker({ + cachedData: fs.readFileSync(opts.bundle.cachedDataFile), + code: fs.readFileSync(opts.bundle.codeFile) + }); + } +}); process.on('unhandledRejection', throwsHelper); diff --git a/package.json b/package.json index f16a20568..70ed9ff33 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "node": ">=0.10.0" }, "scripts": { - "test": "xo && nyc --cache --reporter=lcov --reporter=text tap --no-cov --timeout=150 test/*.js test/reporters/*.js", + "test": "scripts/xo.js && nyc --cache --reporter=lcov --reporter=text tap --no-cov --timeout=150 test/*.js test/reporters/*.js", "test-win": "tap --no-cov --reporter=classic --timeout=150 test/*.js test/reporters/*.js", "visual": "node test/visual/run-visual-tests.js", "prepublish": "npm run make-ts", @@ -104,6 +104,7 @@ "babel-preset-stage-2": "^6.17.0", "babel-runtime": "^6.11.6", "bluebird": "^3.0.0", + "cached-module-loader": "0.0.2", "caching-transform": "^1.0.0", "chalk": "^1.0.0", "chokidar": "^1.4.2", @@ -117,6 +118,7 @@ "core-assert": "^0.2.0", "currently-unhandled": "^0.4.1", "debug": "^2.2.0", + "default-require-extensions": "^1.0.0", "empower-core": "^0.6.1", "figures": "^1.4.0", "find-cache-dir": "^0.1.1", @@ -184,7 +186,7 @@ "source-map-fixtures": "^2.1.0", "tap": "^7.1.2", "touch": "^1.0.0", - "xo": "^0.16.0", + "xo": "^0.17.0", "zen-observable": "^0.3.0" }, "xo": { diff --git a/profile.js b/profile.js index 3cda7afef..1662b062a 100644 --- a/profile.js +++ b/profile.js @@ -106,7 +106,7 @@ events.on('results', function (data) { if (process.exit) { // Delay is For Node 0.10 which emits uncaughtExceptions async. setTimeout(function () { - process.exit(data.stats.failCount + uncaughtExceptionCount); // eslint-disable-line xo/no-process-exit + process.exit(data.stats.failCount + uncaughtExceptionCount); // eslint-disable-line unicorn/no-process-exit }, 20); } }); @@ -133,5 +133,5 @@ if (console.profile) { } setImmediate(function () { - require('./lib/test-worker'); + require('./lib/test-worker'); // eslint-disable-line import/no-unassigned-import }); diff --git a/scripts/xo.js b/scripts/xo.js new file mode 100755 index 000000000..e06078278 --- /dev/null +++ b/scripts/xo.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node +/* eslint-disable import/no-unassigned-import */ +'use strict'; + +var major = Number(process.version.match(/^v(\d+)/)[1]); +if (major >= 4) { + require('xo/cli'); +} else { + console.warn('Linting requires Node.js >=4'); +} diff --git a/test/cli.js b/test/cli.js index 3982a5ea9..11e2e736c 100644 --- a/test/cli.js +++ b/test/cli.js @@ -68,7 +68,7 @@ function execCli(args, opts, cb) { } test('disallow invalid babel config shortcuts', function (t) { - execCli('es2015.js', {dirname: 'fixture/invalid-babel-config'}, function (err, stdout) { + execCli('es2015.js', {dirname: 'fixture/invalid-babel-config'}, function (err, stdout, stderr) { t.ok(err); var expectedOutput = '\n '; @@ -76,7 +76,7 @@ test('disallow invalid babel config shortcuts', function (t) { expectedOutput += ' See ' + chalk.underline('https://github.com/avajs/ava#es2015-support') + ' for allowed values.'; expectedOutput += '\n'; - t.is(stdout, expectedOutput); + t.is(stderr, expectedOutput); t.end(); }); }); @@ -361,7 +361,7 @@ test('prefers local version of ava', function (t) { } proxyquire('../cli', { - 'debug': debugStub, + debug: debugStub, 'resolve-cwd': resolveCwdStub }); diff --git a/test/visual/lorem-ipsum.js b/test/visual/lorem-ipsum.js index 4512179f6..74f2cb387 100644 --- a/test/visual/lorem-ipsum.js +++ b/test/visual/lorem-ipsum.js @@ -1,7 +1,7 @@ 'use strict'; var delay = require('delay'); var test = require('../../'); -require('./print-lorem-ipsum'); +require('./print-lorem-ipsum'); // eslint-disable-line import/no-unassigned-import async function testFn(t) { await delay(40); diff --git a/test/visual/run-visual-tests.js b/test/visual/run-visual-tests.js index 7ab61de8f..67f8d9292 100644 --- a/test/visual/run-visual-tests.js +++ b/test/visual/run-visual-tests.js @@ -1,5 +1,5 @@ 'use strict'; -require('loud-rejection'); +require('loud-rejection'); // eslint-disable-line import/no-unassigned-import var path = require('path'); var childProcess = require('child_process'); var chalk = require('chalk'); diff --git a/test/watcher.js b/test/watcher.js index b03e9edd1..c18340f3d 100644 --- a/test/watcher.js +++ b/test/watcher.js @@ -53,8 +53,8 @@ group('chokidar', function (beforeEach, test, group) { function proxyWatcher(opts) { return proxyquire.noCallThru().load('../lib/watcher', opts || { - 'chokidar': chokidar, - 'debug': function (name) { + chokidar: chokidar, + debug: function (name) { return function () { var args = [name]; args.push.apply(args, arguments);