diff --git a/.gitignore b/.gitignore index 2d2b47d..57beb2c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea -node_modules \ No newline at end of file +node_modules +coverage/ diff --git a/README.md b/README.md index 86cd9e9..38d189e 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ The config object should be serializable. ###Options -- cache +- cacheFolder - Relative path to folder where browserify-incremental cache will be stored - Default: `false` - parallel @@ -59,7 +59,7 @@ The config object should be serializable. - options - Browserify options for this bundle - exclude - - Modules to exclude from bundle. + - Modules to exclude from bundle. - Default: all modules included by bundles specified in shared bundles - The property name is the name of the bundle - Default: `{}` @@ -105,4 +105,4 @@ The config object should be serializable. - uglify - Options to pass to uglify. If this is set to an object, all bundles will be minified. - Default `null` - + diff --git a/index.js b/index.js index 251fc34..f6b6e0d 100644 --- a/index.js +++ b/index.js @@ -4,9 +4,13 @@ var createBuilder = require('./src/create-builder'); var createParallelBuilder = require('./src/parallel-runner.js'); module.exports = function createParallelOrNormalBuilder(config, onDone) { + if (!config) { + return; + } + if (config.parallel && !config.watch) { return createParallelBuilder(config, onDone); - } else { - return createBuilder(config, onDone); } + + return createBuilder(config, onDone); }; diff --git a/package.json b/package.json index 223d832..d96efb7 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "browserify-builder", - "version": "1.0.1", + "version": "1.0.2", "description": "A tool for configuring complicated applications with Browserify", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "mocha", + "coverage": "istanbul cover _mocha" }, "repository": { "type": "git", @@ -16,13 +17,22 @@ "author": "Buildium", "license": "MIT", "dependencies": { - "lodash": "^4.11.1", - "mkdirp": "^0.5.1" + "browserify-cache-api": "3.0.1", + "lodash": "4.17.4", + "mkdirp": "0.5.1" }, "peerDependencies": { "browserify": "^13.0.0", "browserify-incremental": "^3.1.1", "watchify": "^3.0.0", "uglify-js": "^2.6.2" + }, + "devDependencies": { + "chai": "3.5.0", + "chai-deep-match": "1.0.2", + "istanbul": "0.4.5", + "mocha": "3.2.0", + "sinon": "1.17.7", + "sinon-chai": "2.8.0" } } diff --git a/src/browserify-utils.js b/src/browserify-utils.js index a83668e..60d794a 100644 --- a/src/browserify-utils.js +++ b/src/browserify-utils.js @@ -10,9 +10,6 @@ var incrementalWatch = require('./incremental-watch.js'); var uglify = require('uglify-js'); var mkdirp = require('mkdirp'); -var BUNDLE_COMPLETE_EVENT = 'builderComplete'; - - function uglifyFile(path, options, callback) { var result = uglify.minify(path, options); fs.writeFile(path, result.code, function(err) { @@ -21,20 +18,27 @@ function uglifyFile(path, options, callback) { } exports.writeBundle = function writeBundle(config) { - mkdirp.sync(path.dirname(config.path)); - return config.bundle.bundle().pipe(fs.createWriteStream(config.path)).on('finish', function() { - console.log('Built ' + config.name); - if (config.uglify) { - uglifyFile(config.path, config.uglify, function(err) { - if (!err) { - console.log('Minified ' + config.name); - } - config.bundle.emit(BUNDLE_COMPLETE_EVENT); - }); - } else { - config.bundle.emit(BUNDLE_COMPLETE_EVENT); - } + var promise = new Promise(function(resolve) { + mkdirp(path.dirname(config.path), function(err) { + config.bundle.bundle() + .pipe(fs.createWriteStream(config.path)) + .on('finish', function() { + console.log('Built ' + config.name); + if (config.uglify) { + uglifyFile(config.path, config.uglify, function(err) { + if (!err) { + console.log('Minified ' + config.name); + } + resolve(); + }); + } else { + resolve(); + } + }); + }); }); + + return promise; }; exports.watchBundle = function watchBundle(config) { @@ -84,7 +88,9 @@ exports.configureAppBundle = function configureAppBundle(bundler, entry, exclude }; exports.configureSharedBundle = function configureSharedBundle(bundler, include, exclude, entry) { - bundler.external(exclude); + if (exclude) { + bundler.external(exclude); + } bundler.require(include); if (entry) { bundler.add(entry); diff --git a/src/create-builder.js b/src/create-builder.js index cf17392..6dda1c6 100644 --- a/src/create-builder.js +++ b/src/create-builder.js @@ -24,17 +24,15 @@ var defaultConfig = { var getPath = function getPathFn(pattern, mod) { if (mod.path) { return mod.path; - } else { - return pattern.replace('[name]', mod.name); } + return pattern.replace('[name]', mod.name); }; var getModuleFromTarget = curry(function getTargetFn(config, target) { if (config.shared[target]) { return extend({name: target, type: 'shared'}, config.shared[target]); - } else { - return extend({name: target, type: 'app'}, config.apps[target]); } + return extend({name: target, type: 'app'}, config.apps[target]); }); var getSharedLibs = flow(values, map('include'), flatten); @@ -49,6 +47,9 @@ var addFilesToBundle = curry(function addFilesToBundleFn(config, mod) { }); var addUglify = curry(function addUglifyFn(config, mod) { + if (!config.uglify) { + return mod; + } return extend(mod, { uglify: config.uglify }); }); @@ -81,18 +82,6 @@ var configureModule = curry(function configureModuleFn(config, mod) { )(mod); }); -var allEventsFinished = curry(function onAllFinished(event, callback, emitters) { - var emitterCount = 0; - emitters.forEach(function(emitter) { - emitter.on(event, function() { - emitterCount++; - if (emitterCount === emitters.length) { - callback(); - } - }); - }); -}); - function createConfigureFlow(config) { return flow( getModuleFromTarget(config), @@ -109,7 +98,6 @@ module.exports = function bundler(userConfig, onDone) { var config = extend(defaultConfig, userConfig); var targets = getTargetList(config); var configureTarget = createConfigureFlow(config); - var execOnDoneAfterFinished = allEventsFinished('builderComplete', onDone); if (config.cacheFolder) { mkdirp.sync(config.cacheFolder); @@ -119,12 +107,16 @@ module.exports = function bundler(userConfig, onDone) { if (config.watch) { bundles.forEach(bundlerTools.watchBundle); } else { - if (onDone) { - execOnDoneAfterFinished(map('bundle', bundles)); - } - bundles.forEach(bundlerTools.writeBundle); + var promise = Promise.resolve(); + + bundles.forEach(function(config) { + promise = promise.then(function() { + return bundlerTools.writeBundle(config); + }); + }); + + promise.then(onDone, onDone); } - return bundles; } exports.buildAll = function buildAll() { @@ -135,17 +127,17 @@ module.exports = function bundler(userConfig, onDone) { }; exports.buildSingle = function buildSingle(userTarget) { - return build( + build( targets .filter(function(item) { return item === userTarget; }) .map(configureTarget) - )[0]; + ); }; exports.buildMulti = function buildMulti(userTargets) { - return build( + build( targets .filter(function(item) { return includes(userTargets, item); diff --git a/src/parallel-runner-child.js b/src/parallel-runner-child.js index 3893792..3a42318 100644 --- a/src/parallel-runner-child.js +++ b/src/parallel-runner-child.js @@ -2,14 +2,12 @@ var createBuilder = require('./create-builder.js'); -var BUILD_COMPLETE_EVENT = 'builderComplete'; - process.on('message', function(message) { var options = JSON.parse(message); - var builder = createBuilder(options.config); - - builder.buildSingle(options.target).bundle.on(BUILD_COMPLETE_EVENT, function() { + var builder = createBuilder(options.config, function() { process.send('done'); }); + + builder.buildSingle(options.target); }); diff --git a/test/browserify-builder.test.js b/test/browserify-builder.test.js new file mode 100644 index 0000000..2c79594 --- /dev/null +++ b/test/browserify-builder.test.js @@ -0,0 +1,68 @@ +var chai = require('chai'); +var sinon = require('sinon'); +chai.should(); +chai.use(require('sinon-chai')); + +require('../src/create-builder'); +require('../src/parallel-runner'); + +describe('browserify-builder', function() { + var sandbox, + browserifyBuilder, + createBuilder, + createParallelBuilder; + + before(function() { + sandbox = sinon.sandbox.create(); + createBuilder = sandbox.stub( require.cache[ require.resolve( '../src/create-builder' ) ], 'exports'); + createParallelBuilder = sandbox.stub( require.cache[ require.resolve( '../src/parallel-runner' ) ], 'exports'); + browserifyBuilder = require('../index'); + }); + + beforeEach(function() { + createBuilder.reset(); + createParallelBuilder.reset(); + }); + + after(function() { + sandbox.restore(); + }); + + it('should be a noop if no config is provided', function() { + browserifyBuilder(); + createBuilder.should.have.not.been.called; + createParallelBuilder.should.have.not.been.called; + }); + + it('should create a builder', function() { + var config = {}; + browserifyBuilder(config); + createBuilder.should.have.been.calledWith(config); + }); + + it('should pass callback to builder if provided', function() { + var config = {}; + var callback = function() {}; + browserifyBuilder(config, callback); + createBuilder.should.have.been.calledWith(config, callback); + }); + + it('should create a parallel builder if the parallel is true', function() { + var config = { parallel: true }; + browserifyBuilder(config); + createParallelBuilder.should.have.been.calledWith(config); + }); + + it('should not create a parallel builder if watch is true', function() { + var config = { parallel: true, watch: true }; + browserifyBuilder(config); + createParallelBuilder.should.not.have.been.called; + }); + + it('should pass callback to parallel builder if provided', function() { + var config = { parallel: true }; + var callback = function() {}; + browserifyBuilder(config, callback); + createParallelBuilder.should.have.been.calledWith(config, callback); + }); +}); \ No newline at end of file diff --git a/test/create-builder.test.js b/test/create-builder.test.js new file mode 100644 index 0000000..717b824 --- /dev/null +++ b/test/create-builder.test.js @@ -0,0 +1,375 @@ +var chai = require('chai'); +var sinon = require('sinon'); +chai.should(); +chai.use(require('sinon-chai')); +chai.use(require('chai-deep-match')); + +var bundlerTools = require('../src/browserify-utils'); +var createBuilder = require('../src/create-builder'); + +var range = require('lodash/range'); +var uniqueId = require('lodash/uniqueId'); +var random = require('lodash/random'); + +describe('create-builder', function() { + var config, + sandbox; + + // ref: https://github.com/mochajs/mocha/issues/1128 + var rejectionHandler = function (reason) { throw reason; } + + before(function() { + process.on('unhandledRejection', rejectionHandler); + }); + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function() { + sandbox.restore(); + }); + + after(function() { + process.removeListener('unhandledRejection', rejectionHandler); + }); + + beforeEach(function() { + config = { + watch: false, + parallel: false, + outputFilePattern: '[name].js', + apps: {}, + shared: {}, + plugins: [], + transforms: [] + }; + }) + + it('should create a builder', function() { + var builder = createBuilder(config); + builder.should.be.defined; + builder.should.respondTo('buildAll'); + builder.should.respondTo('buildSingle'); + builder.should.respondTo('buildMulti'); + builder.should.respondTo('getBundles'); + }); + + describe('getBundles', function() { + it('should return an array of bundle configurations', function() { + var builder = createBuilder(config); + builder.getBundles().should.be.instanceof(Array); + }); + + it('should return bundle configurations for each app config given', function() { + config.apps = { + hello: { + entry: 'hello.js' + }, + world: { + entry: 'world.js' + } + }; + + var bundles = createBuilder(config).getBundles(); + + bundles.should.deep.match([{ + type: 'app', + entry: 'hello.js', + name: 'hello' + }, { + type: 'app', + entry: 'world.js', + name: 'world' + }]); + }); + + it('should return bundle configurations for each shared config given', function() { + config.shared = { + hello: { + entry: 'hello.js' + }, + world: { + entry: 'world.js' + } + }; + + var bundles = createBuilder(config).getBundles(); + + bundles.should.deep.match([{ + type: 'shared', + entry: 'hello.js', + name: 'hello' + }, { + type: 'shared', + entry: 'world.js', + name: 'world' + }]); + }); + + it('should call createBundle for each config given', function() { + config.apps = { + hello: { + entry: 'hello.js' + } + }; + + sandbox.spy(bundlerTools, 'createBundle'); + + var bundles = createBuilder(config).getBundles(); + bundlerTools.createBundle.should.have.been.calledOnce; + + var args = bundlerTools.createBundle.args[0]; + args[2].should.equal('hello'); + }); + + it('should call addPlugins for each config given', function() { + config.apps = { + hello: { + entry: 'hello.js' + } + }; + + sandbox.spy(bundlerTools, 'addPlugins'); + + var bundles = createBuilder(config).getBundles(); + var hello = bundles[0]; + bundlerTools.addPlugins.should.have.been.calledOnce; + bundlerTools.addPlugins.should.have.been.calledWith(hello.bundle, config); + }); + + it('should call addTransforms for each config given', function() { + config.apps = { + hello: { + entry: 'hello.js' + } + }; + + sandbox.spy(bundlerTools, 'addTransforms'); + + var bundles = createBuilder(config).getBundles(); + var hello = bundles[0]; + bundlerTools.addTransforms.should.have.been.calledOnce; + bundlerTools.addTransforms.should.have.been.calledWith(hello.bundle, config); + }); + + describe('addFilesToBundle', function() { + beforeEach(function() { + sandbox.spy(bundlerTools, 'configureAppBundle'); + sandbox.spy(bundlerTools, 'configureSharedBundle'); + }); + + it('should call configureAppBundle for each app config given', function() { + config.apps = { + hello: { + entry: 'hello.js' + } + }; + + var bundles = createBuilder(config).getBundles(); + var hello = bundles[0]; + bundlerTools.configureAppBundle.should.have.been.calledOnce; + bundlerTools.configureAppBundle.should.have.been.calledWith(hello.bundle, hello.entry, []); + }); + + it('should call configureAppBundle with any excludes provided', function() { + config.apps = { + hello: { + entry: 'hello.js', + exclude: ['foo', 'bar'] + } + }; + + var bundles = createBuilder(config).getBundles(); + var hello = bundles[0]; + bundlerTools.configureAppBundle.should.have.been.calledOnce; + + var args = bundlerTools.configureAppBundle.args[0]; + args[2].should.eql(['foo', 'bar']); + }); + + it('should call configureAppBundle with shared modules excluded by default', function() { + config.apps = { + hello: { + entry: 'hello.js' + } + }; + + config.shared = { + world: { + include: ['foo'] + }, + other: { + include: ['bar'] + } + }; + + var bundles = createBuilder(config).getBundles(); + var hello = bundles[0]; + bundlerTools.configureAppBundle.should.have.been.calledOnce; + + var args = bundlerTools.configureAppBundle.args[0]; + args[2].should.eql(['foo', 'bar']); + }); + + it('should call configureAppBundle with the exclude option given preference over shared modules', function() { + config.apps = { + hello: { + entry: 'hello.js', + exclude: ['foo'] + } + }; + + config.shared = { + world: { + include: ['bar'] + } + }; + + var bundles = createBuilder(config).getBundles(); + var hello = bundles[0]; + bundlerTools.configureAppBundle.should.have.been.calledOnce; + + var args = bundlerTools.configureAppBundle.args[0]; + args[2].should.eql(['foo']); + }); + + it('should call configureSharedBundle for each shared config given', function() { + config.shared = { + hello: { + entry: 'hello.js', + include: ['foo'], + exclude: ['bar'] + } + }; + + var bundles = createBuilder(config).getBundles(); + var hello = bundles[0]; + bundlerTools.configureSharedBundle.should.have.been.calledOnce; + bundlerTools.configureSharedBundle.should.have.been.calledWith(hello.bundle, hello.include, hello.exclude, hello.entry); + }); + }); + + describe('setBundlePath', function() { + it('should set the bundle path', function() { + config.apps = { + hello: { + entry: 'hello.js' + } + }; + + var bundles = createBuilder(config).getBundles(); + var hello = bundles[0]; + hello.path.should.equal('hello.js'); + }); + + it('should set the bundle path based on outputFilePattern', function() { + var testCases = [ + ['world.js', 'world.js'], + ['[name]-world.js', 'hello-world.js'], + ['foo-[name].js', 'foo-hello.js'] + ]; + + testCases.forEach(function(testCase) { + config.apps = { + hello: { + entry: 'hello.js' + } + }; + + config.outputFilePattern = testCase[0]; + + var bundles = createBuilder(config).getBundles(); + var hello = bundles[0]; + hello.path.should.equal(testCase[1]); + }); + }); + + it('should set the bundle path to the given config path if provided', function() { + config.apps = { + hello: { + entry: 'hello.js', + path: 'foobar.js' + } + }; + + config.outputFilePattern = '[name]-world.js'; + + var bundles = createBuilder(config).getBundles(); + var hello = bundles[0]; + hello.path.should.equal('foobar.js'); + }); + }); + + it('should add uglify options to the bundle if provided', function() { + config.apps = { + hello: { + entry: 'hello.js' + } + }; + + config.uglify = { foo: 'bar' }; + + var bundles = createBuilder(config).getBundles(); + var hello = bundles[0]; + hello.uglify.should.eql(config.uglify); + }); + }); + + describe('buildAll', function() { + it('should call the builder callback if provided', function(done) { + config.apps = { + one: { entry: 'one.js' }, + two: { entry: 'two.js' }, + three: { entry: 'three.js' } + }; + config.outputFilePattern = 'test/data/generated/[name].js'; + + sandbox.stub(bundlerTools, 'writeBundle', function() { + return Promise.resolve(); + }); + + var onDone = function() { done(); }; + createBuilder(config, onDone).buildAll(); + }); + + it('should call writeBundle for each app', function(done) { + config.apps = { + hello: { entry: 'world.js' }, + foo: { entry: 'bar.js' } + }; + + sandbox.stub(bundlerTools, 'writeBundle', sandbox.spy()); + + createBuilder(config, function() { + bundlerTools.writeBundle.should.have.been.calledTwice; + done(); + }).buildAll(); + }); + + it('should call writeBundle sequentially for each app', function(done) { + config.apps = range(5).map(uniqueId).reduce(function(accumulator, current) { + accumulator[current] = { entry: 'foobar.js' }; + return accumulator; + }, {}); + + var expected = Object.keys(config.apps); + var actual = []; + + sandbox.stub(bundlerTools, 'writeBundle', function(bundle) { + return new Promise(function(resolve) { + setTimeout(function() { + actual.push(bundle.name); + resolve(); + }, random(1, 10)); + }); + }); + + createBuilder(config, function() { + // chai 4.x.x => expected.should.have.same.ordered.members(actual); + expected.toString().should.equal(actual.toString()); + done(); + }).buildAll(); + }); + }); +}); \ No newline at end of file