diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a5b68c..df1f744 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 4.7.2 +* fix: directly require factor-bundle. #61 +* internal: fix watch test not finishing + ## 4.7.1 * internal: linter cleanup diff --git a/README.md b/README.md index 3406232..ca344da 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ atomify-js is a tool that makes it easy to create small, atomic modules of clien - [opts.require](#optsrequire) - [opts.external](#optsexternal) - [opts.assets](#optsassets) + - [opts.streams](#optsstreams) - [Callback](#callback) - [Events](#events) - [browserify `( bundle)`](#browserify-browserifyinstance-bundle) @@ -72,7 +73,7 @@ Path or paths that will be provided to Browserify as entry points. For convenien #### opts.common If you have multiple entries, you can set this to `true` to enable [factor-bundle](https://github.com/substack/factor-bundle), which will take the common dependencies of all entries and move them to a common bundle. If you use this option, the api changes a little bit. -If using a callback, you're passed an object that with keys of each entry file and values of the compiled JS file. You'll also have a `common` key. +If a callback is defined, then it will be passed an object whose keys are filenames (stripped of path and file extension) and whose values are the associated compiled JS (as strings). If [opts.streams](#optsstreams) was valid, then this object will only contain a `common` key; else it will also contain one key for every file in [opts.entries](#optsentries). ```js var js = require('atomify-js') @@ -88,7 +89,7 @@ js({ }) ``` -If piping the response, you'll be pipped the common bundle. You'll need to listen to the `'entry'` event to get the compiled entry files. +If piping the response, only the common bundle is piped out. If [opts.streams](#optsstreams) was valid, the other compiled bundles will be piped to the generated streams; otherwise you'll need to listen to the `'entry'` event to get the compiled entry files. ```js var js = require('atomify-js') @@ -172,6 +173,9 @@ and a copy of logo.png will now exist at `dist/assets/4314d804f81c8510.png` You may also provide any valid [browserify bundle options](https://github.com/substack/node-browserify#bbundleopts-cb) in the `opts` object as well, and they will be passed directly to Browserify. +#### opts.streams +A function that will be called (upon bundling) once per entry file, with two arguments `entry` and `index` (respectively indicating the value and index of the associated element from [opts.entries](#optsentries)); it should return the stream to which the compiled entry bundle should be piped. Only meaningful if [opts.common](#opts.common) is `true`. + ### Callback Standard Browserify bundle callback with `cb(err, src)` signature. Not called if `opts.output` is specifed. If `callback` is provided as a string rather than function reference it will be used as the `opts.output` file path. diff --git a/index.js b/index.js index 6c54b53..a911037 100644 --- a/index.js +++ b/index.js @@ -34,7 +34,7 @@ ctor = module.exports = function atomifyJs (opts, cb) { , assets , outputs , b - , w + , streamGenerator if (Array.isArray(opts)) opts = {entries: opts} if (typeof opts === 'string') opts = {entries: [opts]} @@ -104,20 +104,20 @@ ctor = module.exports = function atomifyJs (opts, cb) { emitter.emit('browserify', b) if (opts.watch) { - w = watchify(b) - emitter.emit('watchify', w) + b = watchify(b) + emitter.emit('watchify', b) } if (opts.watch) { - w.on('update', function onUpdate (ids) { + b.on('update', function onUpdate (ids) { ids.forEach(function eachId (id) { emitter.emit('changed', id) }) - w.bundle(cb) + rebundle() }) - w.on('time', function onTime (time) { + b.on('time', function onTime (time) { emitter.emit('bundle', time) }) } @@ -199,7 +199,6 @@ ctor = module.exports = function atomifyJs (opts, cb) { b.external(opts.external) } - // if we've got the common option, we want to use factor bundle if (opts.common === true){ if (opts.entries.length < 2) { // TODO: we should do this, but it casues tape to falsely see the error event on itself!? @@ -207,49 +206,76 @@ ctor = module.exports = function atomifyJs (opts, cb) { return void cb(new Error('the `common` option requires an `entries` option with more than one entry')) } - // for each entry, we're going to create a stream-able buffer to put the factored bundle into - outputs = {} - opts.entries.forEach(function createOutputs (entry) { - outputs[path.basename(entry).replace(path.extname(entry), '')] = new streamBuffer.WritableStreamBuffer({ - // these values are arbitrary, but we don't want to require huge buffers - // start as 1 kilobytes. - initialSize: 1 * 1024 - // grow by 1 kilobytes each time buffer overflows. - , incrementAmount: 1 * 1024 - }) - }) - - // setup factor bundle, pass in our streamable-buffers as the output source - b.plugin(factorBundle, { - o: _.values(outputs) - }) - - // we need to wrap the callback to output an object with all the bundles - return b.bundle(function bundledWithCommon (err, common) { - var hasCallback = _.isFunction(cb) - , out = {} - - if (err && hasCallback) return void cb(err) - - // turn the stream-able buffers into plain buffers - out = _.mapValues(outputs, function convertStreamToBuffer (stream, entryName) { - var entryBuffer = stream.getContents() - - // for those using the streaming interface, emit an event with the entry - // do this here so that we don't have to itterate through the outputs twice - emitter.emit('entry', entryBuffer, entryName) + // we need a function to generate streams + // users can optionally provide one, else fallback to generating streamable buffers + streamGenerator = _.isFunction(opts.streams) + ? opts.streams + : function() { + return new streamBuffer.WritableStreamBuffer({ + // these values are arbitrary, but we don't want to require huge buffers + // start as 1 kilobytes. + initialSize: 1 * 1024 + // grow by 1 kilobytes each time buffer overflows. + , incrementAmount: 1 * 1024 + }) + } + } - return entryBuffer + return rebundle() + + function rebundle() { + // if we've got the common option, we want to use factor bundle + if (opts.common === true){ + // generate the output streams + outputs = opts.entries.reduce(function(acc, entry, ix){ + var s = streamGenerator(entry, ix) + + // sanity check + if (_.isObject(s) && _.isFunction(s.pipe)) { + acc.push(s) + return acc + } else { + return void cb(new Error('if provided, the `streams` option must be a function that returns a stream')) + } + },[]) + + // setup factor bundle + b.plugin(factorBundle, { + // passing in generated streams to receive the output + o: outputs }) - // add in the common bundle - out.common = common - - if (hasCallback) cb(err, out) - }) + // we need to wrap the callback to output an object with all the bundles + return b.bundle(function bundledWithCommon (err, common) { + var hasCallback = _.isFunction(cb) + , out = {} + + if (err && hasCallback) return void cb(err) + + if (!_.isFunction(opts.streams)) { + // turn the stream-able buffers into plain buffers + out = opts.entries.reduce(function(acc, entry, ix) { + var entryBuffer = outputs[ix].getContents() + , entryName = path.basename(entry).replace(path.extname(entry), '') + + // for those using the streaming interface, emit an event with the entry + // do this here so that we don't have to itterate through the outputs twice + emitter.emit('entry', entryBuffer, entryName) + + acc[entryName] = entryBuffer + return acc + }, {}) + } + + // add in the common bundle + out.common = common + + if (hasCallback) cb(err, out) + }) + } + // if we don't need to use factor bundle, just browserify! + else return b.bundle(cb) } - // if we don't need to use factor bundle, just browserify! - else return b.bundle(cb) } ctor.emitter = emitter diff --git a/package.json b/package.json index 5aadaee..24ccb4e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "atomify-js", - "version": "4.7.1", + "version": "4.7.2", "description": "Atomic JavaScript - Reusable front-end modules using Browserify, transforms, and templates", "main": "index.js", "scripts": { diff --git a/test/factorBundle.js b/test/factorBundle.js index 56f99f5..8f13a06 100644 --- a/test/factorBundle.js +++ b/test/factorBundle.js @@ -136,6 +136,72 @@ test('opts.common: pipes the common bundle', function (t){ }) }) +test('opts.common: streams all bundles', function (t){ + t.plan(11) + + var commonStreamBuffer = new streamBuffer.WritableStreamBuffer() + var buffers = [] + var entries = [ + path.join(prefix, 'entry-1.js') + , path.join(prefix, 'entry-2.js') + ] + + js({ + entries: entries + , common: true + , streams: function(entry, index) { + t.ok( + entries[index] == entry + , 'buffer generator called with correct arguments' + ) + return buffers[index] = new streamBuffer.WritableStreamBuffer() + } + }) + .on('end', function (){ + var common = commonStreamBuffer.getContents().toString() + // the common dep + t.ok( + common.indexOf(dep1) < 0 + , 'common does not contain dep 1' + ) + t.ok( + common.indexOf(dep2) < 0 + , 'common does not contain dep 2' + ) + t.ok( + common.indexOf(depCommon) > -1 + , 'common contains the common dep' + ) + // the first dep + t.ok( + buffers[0].getContents().toString().indexOf(dep1) > -1 + , 'entry-1 contains dep 1' + ) + t.ok( + buffers[0].getContents().toString().indexOf(dep2) < 0 + , 'entry-1 does not contain dep 2' + ) + t.ok( + buffers[0].getContents().toString().indexOf(depCommon) < 0 + , 'entry-1 does not contain the common dep' + ) + // the second dep + t.ok( + buffers[1].getContents().toString().indexOf(dep2) > -1 + , 'entry-2 contains dep 2' + ) + t.ok( + buffers[1].getContents().toString().indexOf(dep1) < 0 + , 'entry-1 does not contain dep 1' + ) + t.ok( + buffers[1].getContents().toString().indexOf(depCommon) < 0 + , 'entry-2 does not contain the common dep' + ) + }) + .pipe(commonStreamBuffer) +}) + test('opts.common: passing the common option without entries', function (t){ t.plan(1) diff --git a/test/watch.js b/test/watch.js index de09ce9..a059e34 100644 --- a/test/watch.js +++ b/test/watch.js @@ -9,14 +9,14 @@ var test = require('tape') , changerPath = path.join(outputPath, 'changer.js') , now = Date.now() , mkdirp = require('mkdirp') - , setup = function setup(){ + , setup = function setup () { var file = 'module.exports = ' + now mkdirp.sync(outputPath) fs.writeFileSync(changerPath, file) } -test('opts.watch', function(t){ +test('opts.watch', function (t) { var callbackCallCount = 0 , callback , w @@ -24,12 +24,12 @@ test('opts.watch', function(t){ setup() // get the watchify instance so that we can close it an end the test - lib.emitter.once('watchify', function assignWatchify(watchify){ + lib.emitter.once('watchify', function assignWatchify (watchify) { w = watchify }) // callback will be called each time the changer file is changed - callback = function callback(err, bundle){ + callback = function callback (err, bundle) { var src = bundle.toString() , changerContents2 = 'module.exports = changed' @@ -44,8 +44,11 @@ test('opts.watch', function(t){ , 'contains the watched dep on inital callback' ) - // commit a change to the file so that we trigger the callback again - fs.writeFile(changerPath, changerContents2) + // wait for a bit to write the file because watchify has a delay + setTimeout(function () { + // commit a change to the file so that we trigger the callback again + fs.writeFile(changerPath, changerContents2) + }, 800) } else { // close all file handlers ← important so that tests exit @@ -66,7 +69,7 @@ test('opts.watch', function(t){ // run the lib with watchify enabled // wait just a bit to ensure all the file watchers are in place - setTimeout(function(){ + setTimeout(function () { lib({watch: true, entry: path.join(entryPath, 'index.js')}, callback) }, 50) })