├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── demo ├── ava-1-test.js └── ava-2-test.js ├── index.js ├── karma.conf.js ├── lib ├── ava-bundler.js ├── babel-transform.js ├── browser-adapter.js ├── cache-middleware.js ├── child-process-middleware.js ├── externalize-bundle.js ├── main.js ├── preprocessor-main.js ├── preprocessor.js ├── promisify-bundle.js ├── replace-transform.js └── test-bundler.js ├── license ├── package.json ├── readme.md └── test ├── _utils.js ├── babel-transform.js ├── cache-middleware.js ├── child-process-middleware.js ├── fixtures ├── foo.js └── test │ ├── _es2015.js │ ├── es2015.js │ ├── foo.js │ └── has-syntax-errors.js └── test-bundler.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [{package.json,*.yml}] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | coverage 4 | temp 5 | /main.js 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | - '5' 5 | - '4' 6 | - '0.12' 7 | - '0.10' 8 | -------------------------------------------------------------------------------- /demo/ava-1-test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; // eslint-disable-line ava/no-ignored-test-files 2 | 3 | test('foo', t => { 4 | t.is(1, 1); 5 | }); 6 | 7 | test('bar', t => { 8 | t.is(1, 1); 9 | }); 10 | -------------------------------------------------------------------------------- /demo/ava-2-test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; // eslint-disable-line ava/no-ignored-test-files 2 | 3 | test('foo', t => { 4 | t.is(1, 1); 5 | }); 6 | 7 | test('bar', t => { 8 | var a = {foo: 'foo'}; 9 | var b = {bar: 'bar'}; 10 | t.is(a.foo, b.bar); 11 | }); 12 | 13 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var express = require('express'); 4 | var cacheMiddleware = require('./lib/cache-middleware'); 5 | var childProcessMiddleware = require('./lib/child-process-middleware'); 6 | 7 | function createPattern(path) { 8 | return { 9 | pattern: path, 10 | included: true, 11 | served: true, 12 | watched: false 13 | }; 14 | } 15 | 16 | function framework(config) { 17 | var mainPath = path.join(__dirname, 'lib', 'main.js'); 18 | 19 | config.preprocessors = config.preprocessors || {}; 20 | 21 | config.preprocessors[mainPath] = ['ava-main']; 22 | 23 | config.files.forEach(function (file) { 24 | config.preprocessors[file.pattern] = ['ava']; 25 | }); 26 | 27 | config.files.unshift( 28 | createPattern(mainPath) 29 | ); 30 | 31 | // This will not work until `karma@1.1.0` - users should manually add `ava` until then. 32 | config.middleware = config.middleware || []; 33 | 34 | if (config.middleware.indexOf('ava') === -1) { 35 | config.middleware.push('ava'); 36 | } 37 | } 38 | 39 | framework.$inject = ['config']; 40 | 41 | function middlewareFactory(cwd) { 42 | return express() 43 | .use('/karma-ava/', cacheMiddleware('karma-ava', cwd)) 44 | .use('/karma-ava/child/', childProcessMiddleware); 45 | } 46 | 47 | middlewareFactory.$inject = ['config.basedir']; 48 | 49 | module.exports = { 50 | 'ava-test-bundler': ['factory', require('./lib/test-bundler')], 51 | 'framework:ava': ['factory', framework], 52 | 'preprocessor:ava': ['factory', require('./lib/preprocessor')], 53 | 'preprocessor:ava-main': ['factory', require('./lib/preprocessor-main')], 54 | 'middleware:ava': ['factory', middlewareFactory] 55 | }; 56 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | frameworks: ['ava'], 4 | basePath: 'demo', 5 | files: ['ava-*-test.js'], 6 | browsers: [ 7 | 'Chrome' 8 | ], 9 | plugins: [ 10 | require('karma-chrome-launcher'), 11 | require('./') 12 | ], 13 | singleRun: false, 14 | autoRun: true 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/ava-bundler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var browserify = require('browserify'); 3 | var resolveFrom = require('resolve-from'); 4 | var replaceTransform = require('./replace-transform'); 5 | 6 | module.exports = function (cwd, debug, browserAdapterPath) { 7 | var avaBundler = browserify({debug: debug, basedir: cwd, fullPaths: true}); 8 | var processAdapterPath = resolveFrom(cwd, 'ava/lib/process-adapter'); 9 | var avaPath = resolveFrom(cwd, 'ava'); 10 | avaBundler.avaProcessAdapterPath = processAdapterPath; 11 | 12 | avaBundler.require(processAdapterPath); 13 | 14 | avaBundler.require(avaPath); 15 | 16 | avaBundler.transform(replaceTransform(processAdapterPath, browserAdapterPath), {global: true}); 17 | 18 | return avaBundler; 19 | }; 20 | -------------------------------------------------------------------------------- /lib/babel-transform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var stream = require('stream'); 3 | var util = require('util'); 4 | var babel = require('babel-core'); 5 | var babelConfigBuilder = require('ava/lib/babel-config'); 6 | 7 | function makeTransform(opts) { 8 | var babelConfig = babelConfigBuilder.validate(opts.babel); 9 | 10 | return function (filename) { 11 | if (opts.file !== filename) { 12 | return new stream.PassThrough(); 13 | } 14 | 15 | return new Babelify(filename, babelConfig); 16 | }; 17 | } 18 | 19 | module.exports = makeTransform; 20 | 21 | // copied from Babelify - modified so we can embed source maps 22 | function Babelify(filename, babelConfig) { 23 | stream.Transform.call(this); 24 | this._data = ''; 25 | this._filename = filename; 26 | this._babelConfig = babelConfig; 27 | } 28 | 29 | util.inherits(Babelify, stream.Transform); 30 | 31 | Babelify.prototype._transform = function (buf, enc, callback) { 32 | this._data += buf; 33 | callback(); 34 | }; 35 | 36 | Babelify.prototype._flush = function (callback) { 37 | try { 38 | var babelOpts = babelConfigBuilder.build(this._babelConfig, this._filename, this._data); 39 | babelOpts.sourceMaps = 'inline'; 40 | var result = babel.transform(this._data, babelOpts); 41 | this.emit('babelify', result, this._filename); 42 | var code = result.code; 43 | this.push(code); 44 | } catch (err) { 45 | this.emit('error', err); 46 | return; 47 | } 48 | 49 | callback(); 50 | }; 51 | -------------------------------------------------------------------------------- /lib/browser-adapter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var EventEmitter = require('events').EventEmitter; 3 | var globals = require('ava/lib/globals'); 4 | 5 | // Chrome gets upset when the `this` value is non-null for these functions. 6 | globals.setTimeout = setTimeout.bind(null); 7 | globals.clearTimeout = clearTimeout.bind(null); 8 | 9 | var process = new EventEmitter(); 10 | 11 | window.addEventListener('message', function (event) { 12 | if (event.source !== window) { 13 | event = event.data; 14 | process.emit(event.name, event.data); 15 | } 16 | }); 17 | 18 | // Mock the behavior of a parent process. 19 | process.send = function (name, data) { 20 | window.postMessage({ 21 | name: 'ava-' + name, 22 | data: data, 23 | ava: true 24 | }, '*'); 25 | }; 26 | 27 | process.env = {}; 28 | process.opts = { 29 | file: 'test-file' 30 | }; 31 | 32 | process.installSourceMapSupport = function () {}; 33 | process.installPrecompilerHook = function () {}; 34 | process.installDependencyTracking = function () {}; 35 | process.exit = function () {}; 36 | 37 | module.exports = process; 38 | 39 | setTimeout(function () { 40 | require('ava/lib/test-worker'); 41 | }); 42 | -------------------------------------------------------------------------------- /lib/cache-middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var findCacheDir = require('find-cache-dir'); 3 | var express = require('express'); 4 | 5 | module.exports = function (cacheName, cwd) { 6 | var cacheDir = findCacheDir({name: cacheName, cwd: cwd}); 7 | return express.static(cacheDir); 8 | }; 9 | -------------------------------------------------------------------------------- /lib/child-process-middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var express = require('express'); 3 | 4 | function middleware(req, res) { 5 | res.set('Content-Type', 'text/html'); 6 | res.send([ 7 | '', 8 | '
', 9 | ' ', 10 | ' ', 11 | ' ', 12 | '' 13 | ].join('\n')); 14 | } 15 | 16 | module.exports = express().get('/:hash1/:hash2', middleware); 17 | -------------------------------------------------------------------------------- /lib/externalize-bundle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var builtins = require('browserify/lib/builtins'); 3 | var browserResolve = require('browser-resolve'); 4 | 5 | // Adapted from browserify.prototype.external. 6 | // This could be made much simpler with some help from browserify. 7 | // See: https://github.com/substack/node-browserify/pull/1577 8 | module.exports = function (bundler) { 9 | var externalIds = []; 10 | 11 | bundler.on('label', function (prev, id) { 12 | externalIds.push(prev); 13 | 14 | if (prev !== id) { 15 | externalIds.push(id); 16 | } 17 | }); 18 | 19 | function rewriteIndex(row) { 20 | Object.keys(row.deps).forEach(function (key) { 21 | var resolved = browserResolve.sync(key, { 22 | filename: row.file, 23 | modules: builtins 24 | }); 25 | 26 | row.deps[key] = resolved; 27 | 28 | if (row.indexDeps) { 29 | row.indexDeps[key] = resolved; 30 | } 31 | }); 32 | } 33 | 34 | return function externalize(otherBundler) { 35 | externalIds.forEach(function (id) { 36 | otherBundler._external.push(id); 37 | }); 38 | 39 | otherBundler.on('dep', rewriteIndex); 40 | 41 | return otherBundler; 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var EventEmitter = require('events').EventEmitter; 3 | var Promise = require('bluebird'); 4 | 5 | var tests = []; 6 | 7 | function addFile(avaHash, fileHash, prefix) { 8 | var ee = new EventEmitter(); 9 | 10 | var iframe = document.createElement('iframe'); 11 | iframe.style.display = 'none'; 12 | iframe.src = '/karma-ava/child/' + avaHash + '/' + fileHash; 13 | document.body.appendChild(iframe); 14 | var subWindow = iframe.contentWindow; 15 | 16 | ee.send = function (name, data) { 17 | subWindow.postMessage({ 18 | name: 'ava-' + name, 19 | data: data, 20 | ava: true 21 | }, '*'); 22 | }; 23 | 24 | function listener(event) { 25 | if (event.source !== subWindow || !event.data.ava) { 26 | return; 27 | } 28 | 29 | event = event.data; 30 | 31 | var name = event.name.replace(/^ava\-/, ''); 32 | 33 | ee.emit(name, event.data); 34 | } 35 | 36 | subWindow.addEventListener('message', listener, false); 37 | 38 | var started = false; 39 | 40 | var statsPromise = new Promise(function (resolve) { 41 | ee.once('stats', resolve); 42 | }); 43 | 44 | var resultsPromise = new Promise(function (resolve) { 45 | ee.once('results', resolve); 46 | }); 47 | 48 | resultsPromise.then(function () { 49 | subWindow.removeEventListener('message', listener, false); 50 | document.body.removeChild(iframe); 51 | }); 52 | 53 | function start(opts) { 54 | if (started) { 55 | return; 56 | } 57 | 58 | started = true; 59 | 60 | statsPromise.then(function () { 61 | // TODO: Handle default opts in AVA 62 | ee.send('run', opts || {}); 63 | }); 64 | } 65 | 66 | // TODO: uncaughtExceptions and unhandledRejections 67 | ee.on('test', function (test) { 68 | window.__karma__.result({ 69 | id: '', 70 | description: prefix + test.title, 71 | suite: [], 72 | log: [test.error && test.error.message], 73 | success: !test.error, 74 | skipped: test.skip || test.todo 75 | }); 76 | }); 77 | 78 | tests.push({ 79 | start: start, 80 | stats: statsPromise, 81 | results: resultsPromise 82 | }); 83 | } 84 | 85 | window.__AVA__ = { 86 | addFile: addFile 87 | }; 88 | 89 | window.__karma__.start = function start() { 90 | Promise.map(tests, function (test) { 91 | return test.stats; 92 | }).then(function (stats) { 93 | window.__karma__.info({ 94 | total: stats.reduce(function (previous, stats) { 95 | return previous + stats.testCount; 96 | }, 0) 97 | }); 98 | 99 | tests.forEach(function (test) { 100 | test.start(); 101 | }); 102 | }); 103 | 104 | Promise.map(tests, function (test) { 105 | return test.results; 106 | }).then(function () { 107 | window.__karma__.complete({}); 108 | }); 109 | }; 110 | -------------------------------------------------------------------------------- /lib/preprocessor-main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var browserify = require('browserify'); 4 | 5 | var mainPath = path.join(__dirname, 'main.js'); 6 | 7 | // This is a convenience while things are still under active development. 8 | // TODO: main.js should be built and shipped as part of the publish process 9 | function factory() { 10 | return function (content, file, done) { 11 | if (file.originalPath !== mainPath) { 12 | done(null, content); 13 | return; 14 | } 15 | 16 | var fileCache = {}; 17 | fileCache[mainPath] = content; 18 | 19 | var bundler = browserify({debug: true, basedir: __dirname, fileCache: fileCache}); 20 | bundler.add(mainPath, {basedir: __dirname}); 21 | bundler.bundle(done); 22 | }; 23 | } 24 | 25 | factory.$inject = []; 26 | 27 | module.exports = factory; 28 | -------------------------------------------------------------------------------- /lib/preprocessor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var pify = require('pify'); 5 | var Promise = require('bluebird'); 6 | var findCacheDir = require('find-cache-dir'); 7 | var md5hex = require('md5-hex'); 8 | var figures = require('figures'); 9 | var prefixTitle = require('ava/lib/prefix-title'); 10 | 11 | var fsP = pify(fs, Promise); 12 | var separator = ' ' + figures.pointerSmall + ' '; 13 | 14 | var api = ['config.basePath', 'ava-test-bundler']; 15 | function preprocessorFactory(cwd, bundler) { 16 | var cacheDir = findCacheDir({ 17 | name: 'karma-ava', 18 | cwd: cwd, 19 | create: true, 20 | thunk: true 21 | }); 22 | 23 | function hashAndCache(bundleSource) { 24 | var hash = md5hex(bundleSource); 25 | return fsP.writeFile(cacheDir(hash + '.js'), bundleSource).thenReturn(hash); 26 | } 27 | 28 | var ava = bundler.ava.then(hashAndCache); 29 | 30 | return function avaPreprocessor(content, file, done) { 31 | var prefix = prefixTitle(file.originalPath, cwd + path.sep, separator); 32 | 33 | ava.then(function (avaHash) { 34 | bundler.bundleFile(file.originalPath, content) 35 | .then(bundler.appendRequire) 36 | .then(hashAndCache) 37 | .then(function (fileHash) { 38 | done(null, 'window.__AVA__.addFile("' + avaHash + '", "' + fileHash + '", "' + prefix + '");\n'); 39 | }) 40 | .catch(function (err) { 41 | // TODO: Should fail the build 42 | console.log(err); 43 | done(null, ' '); 44 | }); 45 | }); 46 | }; 47 | } 48 | 49 | preprocessorFactory.$inject = api; 50 | 51 | module.exports = preprocessorFactory; 52 | -------------------------------------------------------------------------------- /lib/promisify-bundle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Promise = require('bluebird'); 3 | var pify = require('pify'); 4 | 5 | module.exports = function (bundler) { 6 | var promise = pify(bundler.bundle.bind(bundler), Promise)(); 7 | promise.on = bundler.on.bind(bundler); 8 | return promise; 9 | }; 10 | -------------------------------------------------------------------------------- /lib/replace-transform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var fs = require('fs'); 3 | var stream = require('stream'); 4 | var pify = require('pify'); 5 | var Promise = require('bluebird'); 6 | 7 | // Replace the contents of one file with another. 8 | // Like the browserify `browser` in package.json field, but can be used to replace things inside `node_modules`. 9 | // This should be the first transform in the chain. 10 | module.exports = function (from, to) { 11 | return function (filename) { 12 | if (filename !== from) { 13 | return new stream.PassThrough(); 14 | } 15 | 16 | var transform = new stream.Transform(); 17 | var replacement = pify(fs, Promise).readFile(to, 'utf8'); 18 | 19 | transform._transform = function (buf, enc, cb) { 20 | // ignore incoming file 21 | cb(); 22 | }; 23 | 24 | transform._flush = function (cb) { 25 | replacement.then(function (replacement) { 26 | transform.push(replacement); 27 | cb(); 28 | }).catch(cb); 29 | }; 30 | 31 | return transform; 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /lib/test-bundler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var browserify = require('browserify'); 4 | var externalizeBundle = require('./externalize-bundle'); 5 | var getBundlePromise = require('./promisify-bundle'); 6 | var babelTransform = require('./babel-transform'); 7 | var makeAvaBundler = require('./ava-bundler'); 8 | 9 | function bundlerFactory(cwd) { 10 | var browserAdapterPath = path.join(__dirname, 'browser-adapter.js'); 11 | var avaBundler = makeAvaBundler(cwd, true, browserAdapterPath); 12 | 13 | var externalizeAva = externalizeBundle(avaBundler); 14 | var avaP = getBundlePromise(avaBundler); 15 | 16 | function bundleFile(file, src) { 17 | return avaP.then(function () { 18 | var fileCache = {}; 19 | 20 | if (src) { 21 | fileCache[file] = src; 22 | } 23 | 24 | var bundler = browserify({debug: true, basedir: cwd, fileCache: fileCache, fullPaths: true}); 25 | 26 | bundler.transform(babelTransform({ 27 | cwd: cwd, 28 | file: file 29 | })); 30 | 31 | externalizeAva(bundler); 32 | 33 | bundler.require(file, {expose: 'test-file', basedir: cwd}); 34 | 35 | return getBundlePromise(bundler); 36 | }); 37 | } 38 | 39 | var requireCode = ';\nrequire("' + avaBundler.avaProcessAdapterPath + '");'; 40 | 41 | function appendRequire(code) { 42 | return code + requireCode; 43 | } 44 | 45 | return { 46 | ava: avaP, 47 | bundleFile: bundleFile, 48 | requireCode: requireCode, 49 | appendRequire: appendRequire 50 | }; 51 | } 52 | 53 | bundlerFactory.$inject = ['config.basePath']; 54 | 55 | module.exports = bundlerFactory; 56 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) James Talmage