Skip to content
Open
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `(<browserifyInstance> bundle)`](#browserify-browserifyinstance-bundle)
Expand Down Expand Up @@ -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')
Expand All @@ -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')
Expand Down Expand Up @@ -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.
Expand Down
118 changes: 72 additions & 46 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]}
Expand Down Expand Up @@ -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)
})
}
Expand Down Expand Up @@ -199,57 +199,83 @@ 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!?
// emitter.emit('error', error)
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
66 changes: 66 additions & 0 deletions test/factorBundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
17 changes: 10 additions & 7 deletions test/watch.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,27 @@ 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

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'

Expand All @@ -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
Expand All @@ -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)
})