├── .gitignore ├── .travis.yml ├── test ├── dedupe │ ├── x.js │ ├── a.js │ ├── b.js │ ├── d.js │ ├── e.js │ └── c.js ├── deps │ ├── w.js │ ├── z.js │ ├── y.js │ └── x.js ├── files │ ├── a.js │ ├── z.js │ ├── w.js │ ├── x.js │ ├── t.js │ └── y.js ├── rel │ ├── w.js │ ├── z.js │ ├── y.js │ └── x.js ├── common-deps.js ├── exposed-deps.js ├── plugin-dedupe.js ├── rel.js ├── lift.js ├── deps.js ├── plugin.js ├── outputs.js └── files.js ├── example ├── files │ ├── w.js │ ├── z.js │ ├── y.js │ └── x.js ├── minify.sh ├── api.js └── stream.js ├── bin ├── usage.txt └── cmd.js ├── LICENSE ├── package.json ├── index.js └── readme.markdown /.gitignore: -------------------------------------------------------------------------------- 1 | example/bundle 2 | node_modules 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /test/dedupe/x.js: -------------------------------------------------------------------------------- 1 | module.exports = function(n) { return n * 5; }; 2 | -------------------------------------------------------------------------------- /test/deps/w.js: -------------------------------------------------------------------------------- 1 | module.exports = function (n) { return n * 50 } 2 | -------------------------------------------------------------------------------- /test/deps/z.js: -------------------------------------------------------------------------------- 1 | module.exports = function (n) { return n * 111 } 2 | -------------------------------------------------------------------------------- /test/files/a.js: -------------------------------------------------------------------------------- 1 | module.exports = function (n) { return n + 10 } 2 | -------------------------------------------------------------------------------- /test/files/z.js: -------------------------------------------------------------------------------- 1 | module.exports = function (n) { return n * 111 } 2 | -------------------------------------------------------------------------------- /test/rel/w.js: -------------------------------------------------------------------------------- 1 | module.exports = function (n) { return n * 50 } 2 | -------------------------------------------------------------------------------- /test/rel/z.js: -------------------------------------------------------------------------------- 1 | module.exports = function (n) { return n * 111 } 2 | -------------------------------------------------------------------------------- /example/files/w.js: -------------------------------------------------------------------------------- 1 | module.exports = function (n) { return n * 50 } 2 | -------------------------------------------------------------------------------- /example/files/z.js: -------------------------------------------------------------------------------- 1 | module.exports = function (n) { return n * 111 } 2 | -------------------------------------------------------------------------------- /test/dedupe/a.js: -------------------------------------------------------------------------------- 1 | var e = require('./e'); 2 | console.log(e(2) * 111); 3 | -------------------------------------------------------------------------------- /test/dedupe/b.js: -------------------------------------------------------------------------------- 1 | var e = require('./e'); 2 | console.log(e(3) * 11); 3 | -------------------------------------------------------------------------------- /test/rel/y.js: -------------------------------------------------------------------------------- 1 | var z = require('./z.js'); 2 | console.log(z(2) + 111); 3 | -------------------------------------------------------------------------------- /example/files/y.js: -------------------------------------------------------------------------------- 1 | var z = require('./z.js'); 2 | console.log(z(2) + 111); 3 | -------------------------------------------------------------------------------- /test/deps/y.js: -------------------------------------------------------------------------------- 1 | var z = require('./z.js'); 2 | console.log(z(2) + 111); 3 | -------------------------------------------------------------------------------- /test/dedupe/d.js: -------------------------------------------------------------------------------- 1 | var x = require('./x'); 2 | module.exports = function(n) { return x(n) * 7; }; 3 | -------------------------------------------------------------------------------- /test/dedupe/e.js: -------------------------------------------------------------------------------- 1 | var x = require('./x'); 2 | module.exports = function(n) { return x(n) * 7; }; 3 | -------------------------------------------------------------------------------- /test/files/w.js: -------------------------------------------------------------------------------- 1 | var a = require('./a.js'); 2 | module.exports = function (n) { return n * a(40) } 3 | -------------------------------------------------------------------------------- /test/dedupe/c.js: -------------------------------------------------------------------------------- 1 | var d = require('./d'); 2 | var x = require('./x'); 3 | console.log(d(2) * x(3) * 11); 4 | -------------------------------------------------------------------------------- /test/deps/x.js: -------------------------------------------------------------------------------- 1 | var z = require('./z.js'); 2 | var w = require('./w.js'); 3 | console.log(z(5) * w(2)); 4 | -------------------------------------------------------------------------------- /test/files/x.js: -------------------------------------------------------------------------------- 1 | var z = require('./z.js'); 2 | var w = require('./w.js'); 3 | console.log(z(5) * w(2)); 4 | -------------------------------------------------------------------------------- /test/rel/x.js: -------------------------------------------------------------------------------- 1 | var z = require('./z.js'); 2 | var w = require('./w.js'); 3 | console.log(z(5) * w(2)); 4 | -------------------------------------------------------------------------------- /example/files/x.js: -------------------------------------------------------------------------------- 1 | var z = require('./z.js'); 2 | var w = require('./w.js'); 3 | console.log(z(5) * w(2)); 4 | -------------------------------------------------------------------------------- /test/files/t.js: -------------------------------------------------------------------------------- 1 | var w = require('./w.js'); 2 | var a = require('./a.js'); 3 | 4 | console.log(w(5) + a(40)); 5 | -------------------------------------------------------------------------------- /test/files/y.js: -------------------------------------------------------------------------------- 1 | var a = require('./a.js'); 2 | var z = require('./z.js'); 3 | console.log(z(2) + a(101)); 4 | -------------------------------------------------------------------------------- /example/minify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | browserify files/*.js \ 4 | -p [ ../ -o 'uglifyjs -cm | gzip > bundle/`basename $FILE`.gz' ] \ 5 | | uglifyjs -cm | gzip > bundle/common.js.gz 6 | -------------------------------------------------------------------------------- /example/api.js: -------------------------------------------------------------------------------- 1 | var browserify = require('browserify'); 2 | var fs = require('fs'); 3 | 4 | var files = [ './files/x.js', './files/y.js' ]; 5 | var b = browserify(files); 6 | b.plugin('../', { o: [ 'bundle/x.js', 'bundle/y.js' ] }); 7 | b.bundle().pipe(fs.createWriteStream('bundle/common.js')); 8 | -------------------------------------------------------------------------------- /example/stream.js: -------------------------------------------------------------------------------- 1 | var browserify = require('browserify'); 2 | var concat = require('concat-stream'); 3 | 4 | var files = [ './files/x.js', './files/y.js' ]; 5 | var b = browserify(files); 6 | 7 | b.plugin('../', { outputs: [ write('x'), write('y') ] }); 8 | b.bundle().pipe(write('common')); 9 | 10 | function write (name) { 11 | return concat(function (body) { 12 | console.log('// ----- ' + name + ' -----'); 13 | console.log(body.toString('utf8')); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /bin/usage.txt: -------------------------------------------------------------------------------- 1 | usage: factor-bundle [ x.js -o bundle/x.js ... ] > bundle/common.js 2 | 3 | Read `module-deps` json output from stdin, factoring each entry file out into 4 | the corresponding output file (-o). 5 | 6 | -o FILE, -o CMD 7 | 8 | Write output to FILE or CMD. CMD is distinguished from FILE by the presence 9 | of one or more `>` or `|` characters. For example, use: 10 | 11 | factor-bundle browser/a.js browser/b.js \ 12 | -o bundle/a.js -o bundle/b.js 13 | 14 | to write to a FILE. And do something like: 15 | 16 | factor-bundle browser/*.js \ 17 | -o 'uglifyjs -cm | gzip > bundle/`basename $FILE`.gz' 18 | 19 | to use a CMD. In the CMD, $FILE is set to the corresponding input file path. 20 | 21 | If there is a trailing unpaired `-o`, that file will be used for the common 22 | bundle output. Otherwise, the final bundle is written to stdout. 23 | 24 | -------------------------------------------------------------------------------- /bin/cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var factor = require('../'); 6 | var minimist = require('minimist'); 7 | var pack = require('browser-pack'); 8 | var Transform = require('stream').Transform; 9 | 10 | var argv = minimist(process.argv.slice(2)); 11 | if (argv.h || argv.help || argv._.length === 0) { 12 | return fs.createReadStream(__dirname + '/usage.txt').pipe(process.stdout); 13 | } 14 | 15 | var basedir = argv.basedir || process.cwd(); 16 | 17 | var fr = factor(argv._, { raw: true, root: basedir }); 18 | var files = argv._.reduce(function (acc, x, ix) { 19 | acc[path.resolve(basedir, argv._[ix])] = argv.o[ix]; 20 | return acc; 21 | }, {}); 22 | 23 | var output = argv.o.length > files.length && argv.o[files.length] !== '-' 24 | ? fs.createWriteStream(argv.o[files.length]) : process.stdout 25 | ; 26 | 27 | fr.on('stream', function (bundle) { 28 | var ws = fs.createWriteStream(files[bundle.file]); 29 | bundle.pipe(pack({ raw: true })).pipe(ws); 30 | }); 31 | process.stdin.pipe(fr).pipe(pack({ raw: true })).pipe(output); 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is released under the MIT license: 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /test/common-deps.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var through = require('through'); 3 | var factor = require('../'); 4 | 5 | var ROWS = { 6 | A: { id: '/a.js', deps: { '/c.js': '/c.js' } }, 7 | B: { id: '/b.js', deps: { '/c.js': '/c.js' } }, 8 | C: { id: '/c.js', deps: {} } 9 | }; 10 | 11 | var expected = {}; 12 | expected.common = [ ROWS.A, ROWS.C ].sort(cmp); 13 | expected['/b.js'] = [ ROWS.B ].sort(cmp); 14 | 15 | test('lift singly-shared dependencies', function (t) { 16 | t.plan(2); 17 | 18 | var files = [ '/b.js' ]; 19 | var fr = factor(files, { objectMode: true, raw: true }); 20 | fr.on('stream', function (bundle) { 21 | bundle.pipe(rowsOf(function (rows) { 22 | console.log('----- ' + bundle.file + ' ROWS -----'); 23 | console.log(rows); 24 | t.deepEqual(rows.sort(cmp), expected[bundle.file]); 25 | })); 26 | }); 27 | 28 | fr.pipe(rowsOf(function (rows) { 29 | console.log('----- COMMON ROWS -----'); 30 | console.log(rows); 31 | t.deepEqual(rows.sort(cmp), expected.common); 32 | })); 33 | 34 | fr.write(ROWS.A); 35 | fr.write(ROWS.B); 36 | fr.write(ROWS.C); 37 | fr.end(); 38 | }); 39 | 40 | function rowsOf (cb) { 41 | var rows = []; 42 | return through(write, end); 43 | 44 | function write (row) { rows.push(row) } 45 | function end () { cb(rows) } 46 | } 47 | 48 | function cmp (a, b) { 49 | return a.id < b.id ? -1 : 1; 50 | } 51 | -------------------------------------------------------------------------------- /test/exposed-deps.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var through = require('through'); 3 | var factor = require('../'); 4 | 5 | var ROWS = { 6 | A: { id: '/a.js', deps: { '/c.js': '/c.js' } }, 7 | B: { id: '/b.js', deps: { '/c.js': '/c.js' } }, 8 | C: { id: '/c.js', deps: { 'd': undefined } } 9 | }; 10 | 11 | var expected = {}; 12 | expected.common = [ ROWS.A, ROWS.C ].sort(cmp); 13 | expected['/b.js'] = [ ROWS.B ].sort(cmp); 14 | 15 | test('Copes with missing dependencies', function (t) { 16 | t.plan(2); 17 | 18 | var files = [ '/b.js' ]; 19 | var fr = factor(files, { objectMode: true, raw: true }); 20 | fr.on('stream', function (bundle) { 21 | bundle.pipe(rowsOf(function (rows) { 22 | console.log('----- ' + bundle.file + ' ROWS -----'); 23 | console.log(rows); 24 | t.deepEqual(rows.sort(cmp), expected[bundle.file]); 25 | })); 26 | }); 27 | 28 | fr.pipe(rowsOf(function (rows) { 29 | console.log('----- COMMON ROWS -----'); 30 | console.log(rows); 31 | t.deepEqual(rows.sort(cmp), expected.common); 32 | })); 33 | 34 | fr.write(ROWS.A); 35 | fr.write(ROWS.B); 36 | fr.write(ROWS.C); 37 | fr.end(); 38 | }); 39 | 40 | function rowsOf (cb) { 41 | var rows = []; 42 | return through(write, end); 43 | 44 | function write (row) { rows.push(row) } 45 | function end () { cb(rows) } 46 | } 47 | 48 | function cmp (a, b) { 49 | return a.id < b.id ? -1 : 1; 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "factor-bundle", 3 | "version": "2.5.0", 4 | "description": "factor browser-pack bundles into common shared bundles", 5 | "main": "index.js", 6 | "bin": "bin/cmd.js", 7 | "dependencies": { 8 | "JSONStream": "^1.3.2", 9 | "browser-pack": "^6.0.4", 10 | "defined": "^1.0.0", 11 | "deps-topo-sort": "~0.2.1", 12 | "inherits": "^2.0.1", 13 | "isarray": "^2.0.4", 14 | "labeled-stream-splicer": "^2.0.1", 15 | "minimist": "^1.2.0", 16 | "nub": "^1.0.0", 17 | "outpipe": "^1.1.0", 18 | "reversepoint": "~0.2.0", 19 | "stream-combiner": "~0.2.1", 20 | "through2": "^2.0.3", 21 | "xtend": "^4.0.0" 22 | }, 23 | "devDependencies": { 24 | "browser-unpack": "^1.1.1", 25 | "browserify": "^11.0.1", 26 | "concat-stream": "^1.4.6", 27 | "mkdirp": "~0.5.0", 28 | "module-deps": "^3.9.0", 29 | "osenv": "^0.1.0", 30 | "tape": "^4.0.1", 31 | "through": "^2.3.4" 32 | }, 33 | "scripts": { 34 | "test": "tape test/*.js" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git://github.com/substack/factor-bundle.git" 39 | }, 40 | "homepage": "https://github.com/substack/factor-bundle", 41 | "keywords": [ 42 | "browser-pack", 43 | "browserify", 44 | "browserify-plugin", 45 | "browserify-tool", 46 | "bundle" 47 | ], 48 | "author": { 49 | "email": "mail@substack.net", 50 | "name": "James Halliday", 51 | "url": "http://substack.net" 52 | }, 53 | "license": "MIT" 54 | } 55 | -------------------------------------------------------------------------------- /test/plugin-dedupe.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var fs = require('fs'); 3 | var vm = require('vm'); 4 | var tmp = require('osenv').tmpdir; 5 | var mkdirp = require('mkdirp'); 6 | var spawn = require('child_process').spawn; 7 | var browserify = require('browserify'); 8 | var factor = require('../'); 9 | var concat = require('concat-stream'); 10 | 11 | var files = [ 12 | __dirname + '/dedupe/a.js', 13 | __dirname + '/dedupe/b.js', 14 | __dirname + '/dedupe/c.js' 15 | ]; 16 | var tmpdir = tmp() + '/factor-bundle-' + Math.random(); 17 | mkdirp.sync(tmpdir); 18 | 19 | test('browserify plugin handles deduped modules', function (t) { 20 | t.plan(4); 21 | var args = files.concat('-p', '[', 22 | __dirname + '/..', 23 | '-o', tmpdir + '/a.js', '-o', tmpdir + '/b.js', '-o', tmpdir + '/c.js', 24 | ']', '-o', tmpdir + '/common.js' 25 | ); 26 | var ps = spawn('browserify', args); 27 | ps.on('exit', function (code) { 28 | t.equal(code, 0); 29 | 30 | var common = fs.readFileSync(tmpdir + '/common.js', 'utf8'); 31 | var a = fs.readFileSync(tmpdir + '/a.js', 'utf8'); 32 | var b = fs.readFileSync(tmpdir + '/b.js', 'utf8'); 33 | var c = fs.readFileSync(tmpdir + '/c.js', 'utf8'); 34 | 35 | vm.runInNewContext(common + a, { console: { log: function (msg) { 36 | t.equal(msg, 7770); 37 | } } }); 38 | 39 | vm.runInNewContext(common + b, { console: { log: function (msg) { 40 | t.equal(msg, 1155); 41 | } } }); 42 | 43 | vm.runInNewContext(common + c, { console: { log: function (msg) { 44 | t.equal(msg, 11550); 45 | } } }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/rel.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var tmp = require('osenv').tmpdir; 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var vm = require('vm'); 6 | var mkdirp = require('mkdirp'); 7 | var browserify = require('browserify'); 8 | var factor = require('../'); 9 | var concat = require('concat-stream'); 10 | 11 | var tmpdir = tmp() + '/factor-bundle-' + Math.random(); 12 | mkdirp.sync(tmpdir); 13 | 14 | var cwd = process.cwd(); 15 | process.chdir(__dirname); 16 | 17 | var files = [ './rel/x.js', './rel/y.js' ]; 18 | var outputs = [ 19 | path.join(tmpdir, 'x.js'), 20 | path.join(tmpdir, 'y.js') 21 | ]; 22 | 23 | test('relative entry paths', function (t) { 24 | t.plan(2); 25 | t.on('end', function () { process.chdir(cwd) }); 26 | 27 | var sources = {}; 28 | var pending = 3; 29 | var b = browserify(files); 30 | 31 | b.plugin(factor, { 32 | o: [ 33 | concat(function(data) { 34 | sources.x = data.toString('utf8'); 35 | done(); 36 | }), 37 | concat(function(data) { 38 | sources.y = data.toString('utf8'); 39 | done(); 40 | }) 41 | ] 42 | }); 43 | 44 | b.bundle().pipe(concat(function (data) { 45 | sources.common = data.toString('utf8'); 46 | done(); 47 | })); 48 | 49 | function done () { 50 | if (--pending !== 0) return; 51 | var x = sources.common + sources.x; 52 | var y = sources.common + sources.y; 53 | 54 | vm.runInNewContext(x, { console: { log: function (msg) { 55 | t.equal(msg, 55500); 56 | } } }); 57 | 58 | vm.runInNewContext(y, { console: { log: function (msg) { 59 | t.equal(msg, 333); 60 | } } }); 61 | } 62 | }); 63 | -------------------------------------------------------------------------------- /test/lift.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var through = require('through'); 3 | var factor = require('../'); 4 | 5 | var ROWS = { 6 | A: { id: '/a.js', deps: { '/c.js': '/c.js', '/z.js': '/z.js' } }, 7 | B: { id: '/b.js', deps: { '/c.js': '/c.js', '/x.js': '/x.js' } }, 8 | C: { id: '/c.js', deps: { '/d.js': '/d.js' } }, 9 | D: { id: '/d.js', deps: { '/e.js': '/e.js' } }, 10 | E: { id: '/e.js', deps: {} }, 11 | X: { id: '/x.js', deps: { '/y.js': '/y.js' } }, 12 | Y: { id: '/y.js', deps: { '/z.js': '/z.js' } }, 13 | Z: { id: '/z.js', deps: { '/c.js': '/c.js' } } 14 | }; 15 | 16 | var expected = {}; 17 | expected.common = [ ROWS.C, ROWS.D, ROWS.E, ROWS.Z ].sort(cmp); 18 | expected['/a.js'] = [ ROWS.A ].sort(cmp); 19 | expected['/b.js'] = [ ROWS.B, ROWS.X, ROWS.Y ].sort(cmp); 20 | 21 | test('lift singly-shared dependencies', function (t) { 22 | t.plan(3); 23 | 24 | var files = [ '/a.js', '/b.js' ]; 25 | var fr = factor(files, { objectMode: true, raw: true }); 26 | fr.on('stream', function (bundle) { 27 | bundle.pipe(rowsOf(function (rows) { 28 | console.log('----- ' + bundle.file + ' ROWS -----'); 29 | console.log(rows); 30 | t.deepEqual(rows.sort(cmp), expected[bundle.file]); 31 | })); 32 | }); 33 | 34 | fr.pipe(rowsOf(function (rows) { 35 | console.log('----- COMMON ROWS -----'); 36 | console.log(rows); 37 | t.deepEqual(rows.sort(cmp), expected.common); 38 | })); 39 | 40 | fr.write(ROWS.A); 41 | fr.write(ROWS.B); 42 | fr.write(ROWS.C); 43 | fr.write(ROWS.D); 44 | fr.write(ROWS.E); 45 | fr.write(ROWS.X); 46 | fr.write(ROWS.Y); 47 | fr.write(ROWS.Z); 48 | fr.end(); 49 | }); 50 | 51 | function rowsOf (cb) { 52 | var rows = []; 53 | return through(write, end); 54 | 55 | function write (row) { rows.push(row) } 56 | function end () { cb(rows) } 57 | } 58 | 59 | function cmp (a, b) { 60 | return a.id < b.id ? -1 : 1; 61 | } 62 | -------------------------------------------------------------------------------- /test/deps.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var factor = require('../'); 3 | var mdeps = require('module-deps'); 4 | var through = require('through'); 5 | var path = require('path'); 6 | var fs = require('fs'); 7 | var pack = require('browser-pack'); 8 | var concat = require('concat-stream'); 9 | var vm = require('vm'); 10 | 11 | var files = [ 'x.js', 'y.js' ].map(function (file) { 12 | return path.join(__dirname, 'deps', file); 13 | }); 14 | var expected = { 15 | common: [ read('z.js') ], 16 | 'x.js': [ 17 | read('x.js', { 18 | entry: true, 19 | deps: { './z.js': norm('z.js'), './w.js': norm('w.js') } 20 | }), 21 | read('w.js') 22 | ], 23 | 'y.js': [ 24 | read('y.js', { 25 | entry: true, 26 | deps: { './z.js': norm('z.js') } 27 | }) 28 | ] 29 | }; 30 | 31 | test('trust but verify', function (t) { 32 | t.plan(5); 33 | var packs = { 34 | common: pack({ raw: true }), 35 | 'x.js': pack({ raw: true }), 36 | 'y.js': pack({ raw: true }) 37 | }; 38 | 39 | var pending = 3; 40 | 41 | var sources = {}; 42 | packs.common.pipe(concat(function (src) { 43 | sources.common = src; 44 | done(); 45 | })); 46 | packs['x.js'].pipe(concat(function (src) { 47 | sources['x.js'] = src; 48 | done(); 49 | })); 50 | packs['y.js'].pipe(concat(function (src) { 51 | sources['y.js'] = src; 52 | done(); 53 | })); 54 | 55 | function done () { 56 | if (--pending !== 0) return; 57 | var srcx = 'require=' + sources.common 58 | + ';require=' + sources['x.js'] 59 | ; 60 | function logx (msg) { t.equal(msg, 55500) } 61 | vm.runInNewContext(srcx, { console: { log: logx } }); 62 | 63 | var srcy = 'require=' + sources.common 64 | + ';require=' + sources['y.js'] 65 | ; 66 | function logy (msg) { t.equal(msg, 333) } 67 | vm.runInNewContext(srcy, { console: { log: logy } }); 68 | } 69 | 70 | var rows = []; 71 | var fr = factor(files, { objectMode: true, raw: true }); 72 | fr.on('stream', function (bundle) { 73 | var name = path.basename(bundle.file); 74 | bundle.pipe(rowsOf(function (rows) { 75 | t.deepEqual(rows, expected[name]); 76 | })); 77 | bundle.pipe(packs[name]); 78 | }); 79 | var md = mdeps(); 80 | md.pipe(fr); 81 | fr.pipe(rowsOf(function (rows) { 82 | t.deepEqual(rows, expected.common); 83 | })); 84 | fr.pipe(packs.common); 85 | files.forEach(function (file) { md.write({ file: file }) }); 86 | md.end(); 87 | }); 88 | 89 | function rowsOf (cb) { 90 | var rows = []; 91 | return through(write, end); 92 | 93 | function write (row) { rows.push(row) } 94 | function end () { cb(rows) } 95 | } 96 | 97 | function read (name, ref) { 98 | if (!ref) ref = {}; 99 | var file = norm(name); 100 | ref.id = file; 101 | ref.file = file; 102 | ref.source = fs.readFileSync(file, 'utf8'); 103 | if (!ref.deps) ref.deps = {}; 104 | return ref; 105 | } 106 | 107 | function norm (file) { 108 | return path.normalize(__dirname + '/deps/' + file); 109 | } 110 | -------------------------------------------------------------------------------- /test/plugin.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var tmp = require('osenv').tmpdir; 3 | var fs = require('fs'); 4 | var vm = require('vm'); 5 | var mkdirp = require('mkdirp'); 6 | var spawn = require('child_process').spawn; 7 | var browserify = require('browserify'); 8 | var factor = require('../'); 9 | var concat = require('concat-stream'); 10 | 11 | var files = [ 12 | __dirname + '/deps/x.js', 13 | __dirname + '/deps/y.js' 14 | ]; 15 | var tmpdir = tmp() + '/factor-bundle-' + Math.random(); 16 | mkdirp.sync(tmpdir); 17 | 18 | test('browserify plugin', function (t) { 19 | t.plan(3); 20 | var args = files.concat('-p', '[', 21 | __dirname + '/..', 22 | '-o', tmpdir + '/x.js', '-o', tmpdir + '/y.js', 23 | ']', '-o', tmpdir + '/common.js' 24 | ); 25 | var ps = spawn('browserify', args); 26 | ps.on('exit', function (code) { 27 | t.equal(code, 0); 28 | 29 | var common = fs.readFileSync(tmpdir + '/common.js', 'utf8'); 30 | var x = fs.readFileSync(tmpdir + '/x.js', 'utf8'); 31 | var y = fs.readFileSync(tmpdir + '/y.js', 'utf8'); 32 | 33 | vm.runInNewContext(common + x, { console: { log: function (msg) { 34 | t.equal(msg, 55500); 35 | } } }); 36 | 37 | vm.runInNewContext(common + y, { console: { log: function (msg) { 38 | t.equal(msg, 333); 39 | } } }); 40 | }); 41 | }); 42 | 43 | test('browserify plugin streams', function(t) { 44 | t.plan(2); 45 | 46 | var b = browserify(files); 47 | var sources = {}; 48 | b.plugin(factor, { 49 | o: [ 50 | concat(function(data) { sources.x = data }), 51 | concat(function(data) { sources.y = data }) 52 | ] 53 | }); 54 | 55 | b.bundle().pipe(concat(function(data) { 56 | var common = data.toString('utf8'); 57 | var x = sources.x.toString('utf8'); 58 | var y = sources.y.toString('utf8'); 59 | 60 | vm.runInNewContext(common + x, { console: { log: function (msg) { 61 | t.equal(msg, 55500); 62 | } } }); 63 | 64 | vm.runInNewContext(common + y, { console: { log: function (msg) { 65 | t.equal(msg, 333); 66 | } } }); 67 | })); 68 | }); 69 | 70 | test('browserify plugin multiple bundle calls', function(t) { 71 | t.plan(4); 72 | 73 | var b = browserify(files); 74 | var sources = {}; 75 | b.on('factor.pipeline', function(id, pipeline) { 76 | pipeline.pipe(concat(function(data) { 77 | if (/x\.js$/.test(id)) sources.x = data; 78 | else sources.y = data; 79 | })); 80 | }); 81 | b.plugin(factor); 82 | 83 | b.bundle().pipe(concat(function(data) { 84 | checkBundle(data); 85 | 86 | b.bundle().pipe(concat(checkBundle)); 87 | })); 88 | 89 | function checkBundle(data) { 90 | var common = data.toString('utf8'); 91 | var x = sources.x.toString('utf8'); 92 | var y = sources.y.toString('utf8'); 93 | 94 | vm.runInNewContext(common + x, { console: { log: function (msg) { 95 | t.equal(msg, 55500); 96 | } } }); 97 | 98 | vm.runInNewContext(common + y, { console: { log: function (msg) { 99 | t.equal(msg, 333); 100 | } } }); 101 | } 102 | }); 103 | -------------------------------------------------------------------------------- /test/outputs.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var tmp = require('osenv').tmpdir; 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var vm = require('vm'); 6 | var mkdirp = require('mkdirp'); 7 | var browserify = require('browserify'); 8 | var factor = require('../'); 9 | var concat = require('concat-stream'); 10 | var through = require('through2'); 11 | 12 | var files = [ 13 | __dirname + '/deps/x.js', 14 | __dirname + '/deps/y.js' 15 | ]; 16 | 17 | test('file outputs', function (t) { 18 | var tmpdir = tmp() + '/factor-bundle-' + Math.random(); 19 | mkdirp.sync(tmpdir); 20 | 21 | t.plan(2); 22 | var b = browserify(files); 23 | b.plugin(factor, { 24 | outputs: [ 25 | path.join(tmpdir, 'x.js'), 26 | path.join(tmpdir, 'y.js') 27 | ] 28 | }); 29 | var w = fs.createWriteStream(path.join(tmpdir, 'common.js')); 30 | b.bundle().pipe(w); 31 | 32 | w.on('finish', function () { 33 | var common = fs.readFileSync(tmpdir + '/common.js', 'utf8'); 34 | var x = fs.readFileSync(tmpdir + '/x.js', 'utf8'); 35 | var y = fs.readFileSync(tmpdir + '/y.js', 'utf8'); 36 | 37 | vm.runInNewContext(common + x, { console: { log: function (msg) { 38 | t.equal(msg, 55500); 39 | } } }); 40 | 41 | vm.runInNewContext(common + y, { console: { log: function (msg) { 42 | t.equal(msg, 333); 43 | } } }); 44 | }); 45 | }); 46 | 47 | test('stream outputs', function (t) { 48 | var tmpdir = tmp() + '/factor-bundle-' + Math.random(); 49 | mkdirp.sync(tmpdir); 50 | t.plan(2); 51 | var sources = {}, pending = 3; 52 | function write (key) { 53 | return concat(function (body) { 54 | sources[key] = body.toString('utf8'); 55 | if (-- pending === 0) done(); 56 | }); 57 | } 58 | 59 | var b = browserify(files); 60 | b.plugin(factor, { outputs: [ write('x'), write('y') ] }); 61 | b.bundle().pipe(write('common')); 62 | 63 | function done () { 64 | var common = sources.common, x = sources.x, y = sources.y; 65 | 66 | vm.runInNewContext(common + x, { console: { log: function (msg) { 67 | t.equal(msg, 55500); 68 | } } }); 69 | 70 | vm.runInNewContext(common + y, { console: { log: function (msg) { 71 | t.equal(msg, 333); 72 | } } }); 73 | } 74 | }); 75 | 76 | test('function outputs', function (t) { 77 | var tmpdir = tmp() + '/factor-bundle-' + Math.random(); 78 | mkdirp.sync(tmpdir); 79 | 80 | t.plan(2); 81 | var b = browserify(files); 82 | b.plugin(factor, { 83 | output: function() { return ' cat > ' + tmpdir + '/`basename $FILE`'; } 84 | }); 85 | var w = fs.createWriteStream(path.join(tmpdir, 'common.js')); 86 | b.bundle().pipe(w); 87 | 88 | w.on('finish', function () { 89 | var common = fs.readFileSync(tmpdir + '/common.js', 'utf8'); 90 | var x = fs.readFileSync(tmpdir + '/x.js', 'utf8'); 91 | var y = fs.readFileSync(tmpdir + '/y.js', 'utf8'); 92 | 93 | vm.runInNewContext(common + x, { console: { log: function (msg) { 94 | t.equal(msg, 55500); 95 | } } }); 96 | 97 | vm.runInNewContext(common + y, { console: { log: function (msg) { 98 | t.equal(msg, 333); 99 | } } }); 100 | }); 101 | }); 102 | 103 | test('bundle twice', function (t) { 104 | var tmpdir = tmp() + '/factor-bundle-' + Math.random(); 105 | mkdirp.sync(tmpdir); 106 | 107 | t.plan(4); 108 | var b = browserify(files); 109 | var outputs = [ 110 | path.join(tmpdir, 'x.js'), 111 | path.join(tmpdir, 'y.js') 112 | ]; 113 | b.on('reset', function(){ 114 | b.pipeline.get('deps').push(through.obj(function(data, enc, next){ 115 | if (data.file.indexOf('x.js') !== -1) { 116 | data.source = data.source.replace('z(5)', 'z(6)'); 117 | } 118 | this.push(data); 119 | next(); 120 | })); 121 | }); 122 | b.plugin(factor, { 123 | outputs: outputs 124 | }); 125 | validate(55500, 333, function(){ 126 | validate(66600, 333); 127 | }); 128 | function validate (xVal, yVal, cb) { 129 | var w = fs.createWriteStream(path.join(tmpdir, 'common.js')); 130 | b.bundle().pipe(w); 131 | w.on('finish', function(){ 132 | var common = fs.readFileSync(tmpdir + '/common.js', 'utf8'); 133 | var x = fs.readFileSync(tmpdir + '/x.js', 'utf8'); 134 | var y = fs.readFileSync(tmpdir + '/y.js', 'utf8'); 135 | 136 | vm.runInNewContext(common + x, { console: { log: function (msg) { 137 | t.equal(msg, xVal); 138 | } } }); 139 | 140 | vm.runInNewContext(common + y, { console: { log: function (msg) { 141 | t.equal(msg, yVal); 142 | } } }); 143 | if (cb) cb(); 144 | }); 145 | } 146 | }); 147 | 148 | test('outpipe outputs', function (t) { 149 | var tmpdir = tmp() + '/factor-bundle-' + Math.random(); 150 | mkdirp.sync(tmpdir); 151 | 152 | t.plan(2); 153 | var b = browserify(files); 154 | b.plugin(factor, { 155 | output: ' cat > ' + tmpdir + '/`basename $FILE`' 156 | }); 157 | var w = fs.createWriteStream(path.join(tmpdir, 'common.js')); 158 | b.bundle().pipe(w); 159 | 160 | w.on('finish', function () { 161 | var common = fs.readFileSync(tmpdir + '/common.js', 'utf8'); 162 | var x = fs.readFileSync(tmpdir + '/x.js', 'utf8'); 163 | var y = fs.readFileSync(tmpdir + '/y.js', 'utf8'); 164 | 165 | vm.runInNewContext(common + x, { console: { log: function (msg) { 166 | t.equal(msg, 55500); 167 | } } }); 168 | 169 | vm.runInNewContext(common + y, { console: { log: function (msg) { 170 | t.equal(msg, 333); 171 | } } }); 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Transform = require('stream').Transform; 2 | var through = require('through2'); 3 | var Readable = require('stream').Readable; 4 | var inherits = require('inherits'); 5 | var path = require('path'); 6 | var JSONStream = require('JSONStream'); 7 | var combine = require('stream-combiner'); 8 | var nub = require('nub'); 9 | var depsTopoSort = require('deps-topo-sort'); 10 | var reverse = require('reversepoint'); 11 | var fs = require('fs'); 12 | var pack = require('browser-pack'); 13 | var xtend = require('xtend'); 14 | var defined = require('defined'); 15 | var splicer = require('labeled-stream-splicer'); 16 | var outpipe = require('outpipe'); 17 | var isarray = require('isarray'); 18 | 19 | module.exports = function f (b, opts) { 20 | if (!opts) opts = {}; 21 | if (typeof b === 'string' || Array.isArray(b)) { 22 | return createStream(b, opts) 23 | } 24 | 25 | var files = [].concat(opts.entries).concat(opts.e) 26 | .concat(opts._).filter(Boolean); 27 | 28 | var needRecords = !files.length; 29 | 30 | var outopt = defined(opts.outputs, opts.output, opts.o); 31 | 32 | opts.objectMode = true; 33 | opts.raw = true; 34 | opts.rmap = {}; 35 | 36 | var cwd = defined(opts.basedir, b._options.basedir, process.cwd()), 37 | packOpts = xtend(b._options, { 38 | raw: true, 39 | hasExports: true 40 | }); 41 | 42 | b.on('reset', addHooks); 43 | addHooks(); 44 | 45 | function addHooks () { 46 | b.pipeline.get('record').push(through.obj(function(row, enc, next) { 47 | if (row.file && needRecords) { 48 | files.push(row.file); 49 | } 50 | next(null, row); 51 | }, function(next) { 52 | var outopt = defined(opts.outputs, opts.output, opts.o); 53 | 54 | if (typeof outopt === 'function') { 55 | outopt = outopt(); 56 | } 57 | 58 | var outputs; 59 | if (isarray(outopt)) { 60 | outputs = outopt.map(function (o) { 61 | if (isStream(o)) return o; 62 | return fs.createWriteStream(o); 63 | }); 64 | } else { 65 | outputs = []; 66 | } 67 | 68 | function moreOutputs (file) { 69 | if (isarray(outopt)) return []; 70 | if (!outopt) return []; 71 | var xopts = { env: xtend(process.env, { FILE: file }) }; 72 | return [ outpipe(outopt, xopts) ]; 73 | } 74 | 75 | var pipelines = files.reduce(function (acc, x, ix) { 76 | var pipeline = splicer.obj([ 77 | 'pack', [ pack(packOpts) ], 78 | 'wrap', [] 79 | ]); 80 | 81 | if (ix >= outputs.length) { 82 | outputs.push.apply(outputs, moreOutputs(x)); 83 | } 84 | if (outputs[ix]) pipeline.pipe(outputs[ix]); 85 | 86 | acc[path.resolve(cwd, x)] = pipeline; 87 | return acc; 88 | }, {}); 89 | 90 | // Force browser-pack to wrap the common bundle 91 | b._bpack.hasExports = true; 92 | 93 | Object.keys(pipelines).forEach(function (id) { 94 | b.emit('factor.pipeline', id, pipelines[id]); 95 | }); 96 | 97 | var s = createStream(files, opts); 98 | s.on('stream', function (bundle) { 99 | bundle.pipe(pipelines[bundle.file]); 100 | }); 101 | 102 | b.pipeline.get('pack').unshift(s); 103 | 104 | if (needRecords) files = []; 105 | 106 | next(); 107 | })); 108 | 109 | b.pipeline.get('label').push(through.obj(function(row, enc, next) { 110 | opts.rmap[row.id] = path.resolve(cwd, row.file); 111 | next(null, row); 112 | })); 113 | } 114 | 115 | return b; 116 | 117 | }; 118 | 119 | function createStream (files, opts) { 120 | if (!opts) opts = {}; 121 | 122 | var fr = new Factor(files, opts); 123 | var parse, dup; 124 | 125 | if (opts.objectMode) { 126 | dup = combine(depsTopoSort(), reverse(), fr); 127 | } 128 | else { 129 | parse = JSONStream.parse([true]); 130 | dup = opts.raw 131 | ? combine(parse, depsTopoSort(), reverse(), fr) 132 | : combine( 133 | parse, depsTopoSort(), reverse(), fr, JSONStream.stringify() 134 | ) 135 | ; 136 | parse.on('error', function (err) { dup.emit('error', err) }); 137 | } 138 | 139 | fr.on('error', function (err) { dup.emit('error', err) }); 140 | fr.on('stream', function (s) { 141 | if (opts.raw) dup.emit('stream', s) 142 | else dup.emit('stream', s.pipe(JSONStream.stringify())) 143 | }); 144 | return dup; 145 | } 146 | 147 | inherits(Factor, Transform); 148 | 149 | function Factor (files, opts) { 150 | var self = this; 151 | if (!(this instanceof Factor)) return new Factor(files, opts); 152 | Transform.call(this, { objectMode: true }); 153 | 154 | if (!opts) opts = {}; 155 | this.basedir = defined(opts.basedir, process.cwd()); 156 | 157 | this._streams = {}; 158 | this._groups = {}; 159 | this._buffered = {}; 160 | 161 | this._ensureCommon = {}; 162 | this._files = files.reduce(function (acc, file) { 163 | acc[path.resolve(self.basedir, file)] = true; 164 | return acc; 165 | }, {}); 166 | this._rmap = opts.rmap || {}; 167 | 168 | this._thresholdVal = typeof opts.threshold === "number" 169 | ? opts.threshold : 1 170 | ; 171 | this._defaultThreshold = function(row, group) { 172 | return group.length > this._thresholdVal || group.length === 0; 173 | }; 174 | this._threshold = typeof opts.threshold === "function" 175 | ? opts.threshold 176 | : this._defaultThreshold 177 | ; 178 | } 179 | 180 | Factor.prototype._transform = function (row, enc, next) { 181 | var self = this; 182 | var groups = nub(self._groups[row.id] || []); 183 | var id = this._resolveMap(row.id); 184 | 185 | if (self._files[id]) { 186 | var s = self._streams[id]; 187 | if (!s) s = self._makeStream(row); 188 | groups.push(id); 189 | } 190 | groups.forEach(addGroups); 191 | 192 | if (self._ensureCommon[row.id] || self._threshold(row, groups)) { 193 | Object.keys(row.deps).forEach(function(k) { 194 | self._ensureCommon[row.deps[k]] = true; 195 | }); 196 | self.push(row); 197 | } 198 | else { 199 | groups.forEach(function (id) { 200 | self._streams[id].push(row); 201 | }); 202 | } 203 | 204 | next(); 205 | 206 | function addGroups (gid) { 207 | Object.keys(row.deps || {}).forEach(function (key) { 208 | var file = row.deps[key]; 209 | var g = self._groups[file]; 210 | if (!g) g = self._groups[file] = []; 211 | g.push(gid); 212 | }); 213 | } 214 | }; 215 | 216 | Factor.prototype._flush = function () { 217 | var self = this; 218 | 219 | Object.keys(self._streams).forEach(function (key) { 220 | self._streams[key].push(null); 221 | }); 222 | self.push(null); 223 | }; 224 | 225 | Factor.prototype._makeStream = function (row) { 226 | var s = new Readable({ objectMode: true }); 227 | var id = this._resolveMap(row.id); 228 | s.file = id; 229 | s._read = function () {}; 230 | this._streams[id] = s; 231 | this.emit('stream', s); 232 | return s; 233 | }; 234 | 235 | Factor.prototype._resolveMap = function(id) { 236 | return this._rmap[id] || id; 237 | } 238 | 239 | function isStream (s) { return s && typeof s.pipe === 'function' } 240 | -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | # factor-bundle 2 | 3 | factor [browser-pack](https://npmjs.org/package/browser-pack) bundles into a 4 | common bundle and entry-specific bundles 5 | 6 | [![build status](https://secure.travis-ci.org/substack/factor-bundle.png)](http://travis-ci.org/substack/factor-bundle) 7 | 8 | # example 9 | 10 | x.js: 11 | 12 | ``` js 13 | var z = require('./z.js'); 14 | var w = require('./w.js'); 15 | console.log(z(5) * w(2)); 16 | ``` 17 | 18 | y.js: 19 | 20 | ``` js 21 | var z = require('./z.js'); 22 | console.log(z(2) + 111); 23 | ``` 24 | 25 | z.js: 26 | 27 | ``` js 28 | module.exports = function (n) { return n * 111 } 29 | ``` 30 | 31 | w.js: 32 | 33 | ``` js 34 | module.exports = function (n) { return n * 50 } 35 | ``` 36 | 37 | Now run factor-bundle as a plugin (new in browserify 3.28.0): 38 | 39 | ``` sh 40 | browserify x.js y.js -p [ factor-bundle -o bundle/x.js -o bundle/y.js ] \ 41 | -o bundle/common.js 42 | ``` 43 | 44 | or you can pipe [module-deps](https://npmjs.org/package/module-deps) json 45 | directly into the `factor-bundle` command: 46 | 47 | ``` sh 48 | $ module-deps x.js y.js | factor-bundle \ 49 | x.js -o bundle/x.js \ 50 | y.js -o bundle/y.js \ 51 | > bundle/common.js 52 | ``` 53 | 54 | or factor out an existing bundle already compiled by browserify: 55 | 56 | ``` sh 57 | $ browserify x.js y.js > bundle.js 58 | $ browser-unpack < bundle.js | factor-bundle \ 59 | x.js -o bundle/x.js \ 60 | y.js -o bundle/y.js \ 61 | > bundle/common.js 62 | ``` 63 | 64 | Whichever one of these 3 options, you take, you can now have 2 pages, each with 65 | a different combination of script tags but with all the common modules factored 66 | out into a `common.js` to avoid transferring the same code multiple times: 67 | 68 | ``` html 69 | 70 | 71 | ``` 72 | 73 | ``` html 74 | 75 | 76 | ``` 77 | 78 | to verify this works from node you can do: 79 | 80 | ``` 81 | $ cat bundle/common.js bundle/x.js | node 82 | 55500 83 | $ cat bundle/common.js bundle/y.js | node 84 | 333 85 | ``` 86 | 87 | ## command-line outpipe example 88 | 89 | We can pipe each output file through some other processes. Here we'll do 90 | minification with uglify compression with gzip: 91 | 92 | ``` sh 93 | browserify files/*.js \ 94 | -p [ factor-bundle -o 'uglifyjs -cm | gzip > bundle/`basename $FILE`.gz' ] \ 95 | | uglifyjs -cm | gzip > bundle/common.js.gz 96 | ``` 97 | 98 | ## api plugin example 99 | 100 | If you prefer you can use the factor-bundle plugin api directly in code: 101 | 102 | ``` js 103 | var browserify = require('browserify'); 104 | var fs = require('fs'); 105 | 106 | var files = [ './files/x.js', './files/y.js' ]; 107 | var b = browserify(files); 108 | b.plugin('factor-bundle', { outputs: [ 'bundle/x.js', 'bundle/y.js' ] }); 109 | b.bundle().pipe(fs.createWriteStream('bundle/common.js')); 110 | ``` 111 | 112 | or instead of writing to files you can output to writable streams: 113 | 114 | ``` js 115 | var browserify = require('browserify'); 116 | var concat = require('concat-stream'); 117 | 118 | var files = [ './files/x.js', './files/y.js' ]; 119 | var b = browserify(files); 120 | 121 | b.plugin('factor-bundle', { outputs: [ write('x'), write('y') ] }); 122 | b.bundle().pipe(write('common')); 123 | 124 | function write (name) { 125 | return concat(function (body) { 126 | console.log('// ----- ' + name + ' -----'); 127 | console.log(body.toString('utf8')); 128 | }); 129 | } 130 | ``` 131 | 132 | # usage 133 | 134 | You can use factor-bundle as a browserify plugin: 135 | 136 | ``` 137 | browserify -p [ factor-bundle OPTIONS ] 138 | 139 | where OPTIONS are: 140 | 141 | -o Output FILE or CMD that maps to a corresponding entry file at the same 142 | index. CMDs are executed with $FILE set to the corresponding input file. 143 | Optionally specify a function that returns a valid value for this argument. 144 | 145 | -e Entry file to use, overriding the entry files listed in the original 146 | bundle. 147 | 148 | ``` 149 | 150 | or you can use the command: 151 | 152 | ``` 153 | usage: factor-bundle [ x.js -o bundle/x.js ... ] > bundle/common.js 154 | 155 | Read `module-deps` json output from stdin, factoring each entry file out into 156 | the corresponding output file (-o). 157 | 158 | -o FILE, -o CMD 159 | 160 | Write output to FILE or CMD. CMD is distinguished from FILE by the presence 161 | of one or more `>` or `|` characters. For example, use: 162 | 163 | factor-bundle browser/a.js browser/b.js \ 164 | -o bundle/a.js -o bundle/b.js 165 | 166 | to write to a FILE. And do something like: 167 | 168 | factor-bundle browser/*.js \ 169 | -o 'uglifyjs -cm | gzip > bundle/`basename $FILE`.gz' 170 | 171 | to use a CMD. In the CMD, $FILE is set to the corresponding input file path. 172 | 173 | If there is a trailing unpaired `-o`, that file will be used for the common 174 | bundle output. Otherwise, the final bundle is written to stdout. 175 | 176 | ``` 177 | 178 | # methods 179 | 180 | ``` js 181 | var factor = require('factor-bundle') 182 | ``` 183 | 184 | ## var fr = factor(files, opts={}) 185 | 186 | Return a transform stream `tr` that factors the array of entry path strings 187 | `files` out into bundle files. The input format that `fr` expects is described 188 | in the [module-deps package](https://npmjs.org/package/module-deps). 189 | 190 | The output format for `fr` and each of the `fr` sub-streams given by each 191 | `'stream'` event is also in the 192 | [module-deps](https://npmjs.org/package/module-deps) format. 193 | 194 | `opts.o` or `opts.outputs` should be an array that pairs up with the `files` array to specify 195 | where each bundle output for each entry file should be written. The elements in 196 | `opts.o` can be string filenames, writable streams, or functions that return a 197 | string filename or writable stream. 198 | 199 | `opts.entries` or `opts.e` should be the array of entry files to create 200 | a page-specific bundle for each file. If you don't pass in an `opts.entries`, 201 | this information is gathered from browserify itself. 202 | 203 | The files held in common among `> opts.threshold` (default: 1) bundles will be 204 | output on the `fr` stream itself. The entry-specific bundles are diverted into 205 | each `'stream'` event's output. `opts.threshold` can be a number or a function 206 | `opts.threshold(row, groups)` where `row` is a 207 | [module-deps](https://github.com/substack/module-deps) object and `groups` is 208 | an array of bundles which depend on the row. If the threshold function returns 209 | `true`, that row and all its dependencies will go to the `common` bundle. If 210 | false, the row (but not its dependencies) will go to each bundle in `groups`. 211 | For example: 212 | 213 | ``` js 214 | factor(files, {threshold: function(row, groups) { 215 | if (/.*a\.js$/.test(row.id)) return false; 216 | if (/.*[z]\.js$/.test(row.id)) return true; 217 | return this._defaultThreshold(row, groups); 218 | }}); 219 | ``` 220 | 221 | # events 222 | 223 | ## fr.on('stream', function (stream) {}) 224 | 225 | Each entry file emits a `'stream'` event containing all of the entry-specific 226 | rows that are only used by that entry file (when `opts.threshold === 1`, the 227 | default). 228 | 229 | The entry file name is available as `stream.file`. 230 | 231 | ## b.on('factor.pipeline', function (file, pipeline) {}) 232 | 233 | Emits the full path to the entry file (`file`) and a [labeled-stream-splicer](https://npmjs.org/package/labeled-stream-splicer) (`pipeline`) for each entry file with these labels: 234 | 235 | * `'pack'` - [browser-pack](https://npmjs.org/package/browser-pack) 236 | * `'wrap'` - apply final wrapping 237 | 238 | You can call `pipeline.get` with a label name to get a handle on a stream pipeline that you can `push()`, `unshift()`, or `splice()` to insert your own transform streams. 239 | 240 | Event handlers must be attached *before* calling `b.plugin`. 241 | 242 | # install 243 | 244 | With [npm](https://npmjs.org) do: 245 | 246 | ``` 247 | npm install factor-bundle 248 | ``` 249 | 250 | # license 251 | 252 | MIT 253 | -------------------------------------------------------------------------------- /test/files.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var factor = require('../'); 3 | var mdeps = require('module-deps'); 4 | var through = require('through'); 5 | var path = require('path'); 6 | var fs = require('fs'); 7 | var pack = require('browser-pack'); 8 | var concat = require('concat-stream'); 9 | var vm = require('vm'); 10 | 11 | test('more complicated dependencies', function (t) { 12 | t.plan(5); 13 | var files = [ 'x.js', 'y.js' ].map(function (file) { 14 | return path.join(__dirname, 'files', file); 15 | }); 16 | 17 | var expected = { 18 | common: [ read('z.js'), read('a.js') ], 19 | 'x.js': [ 20 | read('x.js', { 21 | entry: true, 22 | deps: { './z.js': norm('z.js'), './w.js': norm('w.js') } 23 | }), 24 | read('w.js', { 25 | deps: { './a.js': norm('a.js') } 26 | }) 27 | ], 28 | 'y.js': [ 29 | read('y.js', { 30 | entry: true, 31 | deps: { './z.js': norm('z.js'), './a.js': norm('a.js') } 32 | }) 33 | ] 34 | }; 35 | 36 | var packs = { 37 | common: pack({ raw: true }), 38 | 'x.js': pack({ raw: true }), 39 | 'y.js': pack({ raw: true }) 40 | }; 41 | 42 | var pending = 3; 43 | 44 | var sources = {}; 45 | packs.common.pipe(concat(function (src) { 46 | sources.common = src; 47 | done(); 48 | })); 49 | packs['x.js'].pipe(concat(function (src) { 50 | sources['x.js'] = src; 51 | done(); 52 | })); 53 | packs['y.js'].pipe(concat(function (src) { 54 | sources['y.js'] = src; 55 | done(); 56 | })); 57 | 58 | function done () { 59 | if (--pending !== 0) return; 60 | var srcx = 'require=' + sources.common 61 | + ';require=' + sources['x.js'] 62 | ; 63 | function logx (msg) { t.equal(msg, 55500) } 64 | vm.runInNewContext(srcx, { console: { log: logx } }); 65 | 66 | var srcy = 'require=' + sources.common 67 | + ';require=' + sources['y.js'] 68 | ; 69 | function logy (msg) { t.equal(msg, 333) } 70 | vm.runInNewContext(srcy, { console: { log: logy } }); 71 | } 72 | 73 | var rows = []; 74 | var fr = factor(files, { objectMode: true, raw: true }); 75 | fr.on('stream', function (bundle) { 76 | var name = path.basename(bundle.file); 77 | bundle.pipe(rowsOf(function (rows) { 78 | t.deepEqual(rows, expected[name]); 79 | })); 80 | bundle.pipe(packs[name]); 81 | }); 82 | var md = mdeps(); 83 | md.pipe(fr) 84 | fr.pipe(rowsOf(function (rows) { 85 | t.deepEqual(rows, expected.common); 86 | })); 87 | fr.pipe(packs.common); 88 | files.forEach(function (file) { md.write({ file: file }) }); 89 | md.end(); 90 | }); 91 | 92 | test('same module included twice', function (t) { 93 | //t.plan(5); 94 | t.plan(3); 95 | 96 | var files = [ 't.js' ].map(function (file) { 97 | return path.join(__dirname, 'files', file); 98 | }); 99 | 100 | var expected = { 101 | common: [], 102 | 't.js': sortRows([ 103 | read('t.js', { 104 | entry: true, 105 | deps: { './a.js': norm('a.js'), './w.js': norm('w.js') } 106 | }), 107 | read('w.js', { 108 | deps: { './a.js': norm('a.js') } 109 | }), 110 | read('a.js') 111 | ]) 112 | }; 113 | 114 | var packs = { 115 | common: pack({ raw: true }), 116 | 't.js': pack({ raw: true }) 117 | }; 118 | 119 | var pending = 2; 120 | 121 | var sources = {}; 122 | packs.common.pipe(concat(function (src) { 123 | sources.common = src; 124 | done(); 125 | })); 126 | packs['t.js'].pipe(concat(function (src) { 127 | sources['t.js'] = src; 128 | done(); 129 | })); 130 | 131 | function done () { 132 | if (--pending !== 0) return; 133 | var srct = 'require=' + sources.common 134 | + ';require=' + sources['t.js'] 135 | ; 136 | function logx (msg) { t.equal(msg, 300) } 137 | vm.runInNewContext(srct, { console: { log: logx } }); 138 | } 139 | 140 | var rows = []; 141 | var fr = factor(files, { objectMode: true, raw: true }); 142 | fr.on('stream', function (bundle) { 143 | var name = path.basename(bundle.file); 144 | bundle.pipe(rowsOf(function (rows) { 145 | t.deepEqual(rows, expected[name]); 146 | })); 147 | bundle.pipe(packs[name]); 148 | }); 149 | var md = mdeps(); 150 | md.pipe(fr) 151 | fr.pipe(rowsOf(function (rows) { 152 | t.deepEqual(rows, expected.common); 153 | })); 154 | fr.pipe(packs.common); 155 | files.forEach(function (file) { md.write({ file: file }) }); 156 | md.end(); 157 | }); 158 | 159 | test('threshold function reorganizes bundles', function (t) { 160 | t.plan(5); 161 | var files = [ 'y.js', 't.js' ].map(function (file) { 162 | return path.join(__dirname, 'files', file); 163 | }); 164 | 165 | var expected = { 166 | common: [ 167 | read('z.js') 168 | ], 169 | 't.js': sortRows([ 170 | read('t.js', { 171 | entry: true, 172 | deps: { './a.js': norm('a.js'), './w.js': norm('w.js') } 173 | }), 174 | read('w.js', { 175 | deps: { './a.js': norm('a.js') } 176 | }), 177 | read('a.js') 178 | ]), 179 | 'y.js': sortRows([ 180 | read('y.js', { 181 | entry: true, 182 | deps: { './z.js': norm('z.js'), './a.js': norm('a.js') } 183 | }), 184 | read('a.js') 185 | ]) 186 | }; 187 | 188 | var packs = { 189 | common: pack({ raw: true }), 190 | 't.js': pack({ raw: true }), 191 | 'y.js': pack({ raw: true }) 192 | }; 193 | 194 | var pending = 3; 195 | 196 | var sources = {}; 197 | packs.common.pipe(concat(function (src) { 198 | sources.common = src; 199 | done(); 200 | })); 201 | packs['t.js'].pipe(concat(function (src) { 202 | sources['t.js'] = src; 203 | done(); 204 | })); 205 | packs['y.js'].pipe(concat(function (src) { 206 | sources['y.js'] = src; 207 | done(); 208 | })); 209 | 210 | function done () { 211 | if (--pending !== 0) return; 212 | var srct = 'require=' + sources.common 213 | + ';require=' + sources['t.js'] 214 | ; 215 | function logt (msg) { t.equal(msg, 300) } 216 | vm.runInNewContext(srct, { console: { log: logt } }); 217 | 218 | var srcy = 'require=' + sources.common 219 | + ';require=' + sources['y.js'] 220 | ; 221 | function logy (msg) { t.equal(msg, 333) } 222 | vm.runInNewContext(srcy, { console: { log: logy } }); 223 | } 224 | 225 | var rows = []; 226 | var fr = factor(files, { objectMode: true, raw: true, threshold: function(row, groups) { 227 | if (/.*a\.js$/.test(row.id)) { 228 | return false; 229 | }; 230 | if (/.*[z]\.js$/.test(row.id)) { 231 | return true; 232 | }; 233 | return this._defaultThreshold(row, groups); 234 | }}); 235 | fr.on('stream', function (bundle) { 236 | var name = path.basename(bundle.file); 237 | bundle.pipe(rowsOf(function (rows) { 238 | t.deepEqual(rows, expected[name]); 239 | })); 240 | bundle.pipe(packs[name]); 241 | }); 242 | var md = mdeps(); 243 | md.pipe(fr) 244 | fr.pipe(rowsOf(function (rows) { 245 | t.deepEqual(rows, expected.common); 246 | })); 247 | fr.pipe(packs.common); 248 | files.forEach(function (file) { md.write({ file: file }) }); 249 | md.end(); 250 | }); 251 | 252 | test('if dependent is in common, so is dependee', function (t) { 253 | t.plan(3); 254 | var files = [ 't.js' ].map(function (file) { 255 | return path.join(__dirname, 'files', file); 256 | }); 257 | 258 | var expected = { 259 | common: [ 260 | read('w.js', { 261 | deps: { './a.js': norm('a.js') } 262 | }), 263 | read('a.js') 264 | ], 265 | 't.js': [ 266 | read('t.js', { 267 | entry: true, 268 | deps: { './a.js': norm('a.js'), './w.js': norm('w.js') } 269 | }) 270 | ] 271 | }; 272 | 273 | var packs = { 274 | common: pack({ raw: true }), 275 | 't.js': pack({ raw: true }), 276 | }; 277 | 278 | var pending = 2; 279 | 280 | var sources = {}; 281 | packs.common.pipe(concat(function (src) { 282 | sources.common = src; 283 | done(); 284 | })); 285 | packs['t.js'].pipe(concat(function (src) { 286 | sources['t.js'] = src; 287 | done(); 288 | })); 289 | 290 | function done () { 291 | if (--pending !== 0) return; 292 | var srct = 'require=' + sources.common 293 | + ';require=' + sources['t.js'] 294 | ; 295 | function logt (msg) { t.equal(msg, 300) } 296 | vm.runInNewContext(srct, { console: { log: logt } }); 297 | } 298 | 299 | var rows = []; 300 | var fr = factor(files, { objectMode: true, raw: true, threshold: function(row, groups) { 301 | if (/.*[w]\.js$/.test(row.id)) { 302 | return true; 303 | }; 304 | return this._defaultThreshold(row, groups); 305 | }}); 306 | fr.on('stream', function (bundle) { 307 | var name = path.basename(bundle.file); 308 | bundle.pipe(rowsOf(function (rows) { 309 | t.deepEqual(rows, expected[name]); 310 | })); 311 | bundle.pipe(packs[name]); 312 | }); 313 | var md = mdeps(); 314 | md.pipe(fr) 315 | fr.pipe(rowsOf(function (rows) { 316 | t.deepEqual(rows, expected.common); 317 | })); 318 | fr.pipe(packs.common); 319 | files.forEach(function (file) { md.write({ file: file }) }); 320 | md.end(); 321 | }); 322 | 323 | function rowsOf (cb) { 324 | var rows = []; 325 | return through(write, end); 326 | 327 | function write (row) { rows.push(row) } 328 | function end () { cb(sortRows(rows)) } 329 | } 330 | 331 | function sortRows (rows) { 332 | return rows.sort(function (a, b) { 333 | return a.id < b.id ? 1 : -1; 334 | }); 335 | } 336 | 337 | function read (name, ref) { 338 | if (!ref) ref = {}; 339 | var file = norm(name); 340 | ref.id = file; 341 | ref.file = file; 342 | ref.source = fs.readFileSync(file, 'utf8'); 343 | if (!ref.deps) ref.deps = {}; 344 | return ref; 345 | } 346 | 347 | function norm (file) { 348 | return path.normalize(__dirname + '/files/' + file); 349 | } 350 | --------------------------------------------------------------------------------