├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── index.js ├── package.json └── test ├── .jshintrc ├── app-a-test.js ├── app-a ├── index.js ├── node_modules │ ├── lib-a │ │ ├── index.js │ │ └── package.json │ ├── lib-ab │ │ ├── index.js │ │ └── package.json │ ├── lib-b │ │ ├── index.js │ │ ├── node_modules │ │ │ ├── lib-a │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ └── lib-ab │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ └── package.json │ └── lib-c │ │ ├── index.js │ │ ├── node_modules │ │ └── lib-b │ │ │ ├── index.js │ │ │ └── package.json │ │ └── package.json └── package.json ├── app-b-test.js ├── app-b ├── index.js ├── node_modules │ ├── lib-a │ │ ├── index.js │ │ ├── package.json │ │ └── source.js │ └── lib-b │ │ ├── index.js │ │ ├── node_modules │ │ └── lib-a │ │ │ ├── index.js │ │ │ ├── package.json │ │ │ └── source.js │ │ └── package.json └── package.json ├── non-main-require-test.js ├── non-main-require ├── index.js ├── node_modules │ ├── lib-a │ │ ├── index.js │ │ ├── node_modules │ │ │ └── lib-common │ │ │ │ ├── index.js │ │ │ │ ├── non-main.js │ │ │ │ └── package.json │ │ └── package.json │ └── lib-common │ │ ├── index.js │ │ ├── non-main.js │ │ └── package.json └── package.json ├── package-submodules-test.js ├── package-submodules ├── index.js ├── node_modules │ ├── lib-a │ │ ├── index.js │ │ ├── node_modules │ │ │ └── lib-submodules │ │ │ │ ├── index.js │ │ │ │ ├── module-a │ │ │ │ └── module-a.js │ │ │ │ ├── module-b │ │ │ │ └── module-b.js │ │ │ │ └── package.json │ │ └── package.json │ └── lib-submodules │ │ ├── index.js │ │ ├── module-a │ │ └── module-a.js │ │ ├── module-b │ │ └── module-b.js │ │ └── package.json └── package.json └── utils.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | !test/**/*/node_modules 3 | *.sublime* 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "quotmark": "single", 3 | "undef": true, 4 | "unused": true, 5 | "devel": true, 6 | "maxlen": 120, 7 | "node": true 8 | } 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.12" 5 | - "iojs" 6 | notifications: 7 | email: false 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.1.0 4 | ##### Features 5 | * Allow usage via Browserify CLI (thanks [@yaycmyk](https://github.com/yaycmyk)). 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #browserify-resolutions [![build status](https://travis-ci.org/Updater/browserify-resolutions.svg?branch=master)](https://travis-ci.org/Updater/browserify-resolutions) 2 | [Bower resolutions](http://jaketrent.com/post/bower-resolutions/) for npm + Browserify... sort of. 3 | 4 | A Browserify plugin that allows more explicit control of module deduping. It purges duplicate modules from the output bundle and prevents modules from loading several times. 5 | 6 | ## Why? 7 | A large dependency tree may include multiple versions of the same module, which may result in it being bundled multiple times, greatly increasing the bundle's size. 8 | 9 | #### What about `npm dedupe`? 10 | It can be sufficient, but is sometimes hamstrung as third party modules may be asking for incompatible versions of the same library. 11 | 12 | #### What about `peerDependencies`? 13 | Hopefully solves this problem in the future, but currently difficult to work with: https://github.com/npm/npm/issues/6565 14 | 15 | #### What about Browserify's own dedupe? 16 | It currently only dedupes identical source files. Even if deduped, a library may be instantiated several times. 17 | 18 | E.g., even if Angular is deduped and only bundled once, you may still see: 19 | ``` 20 | WARNING: Tried to load angular more than once. 21 | ``` 22 | 23 | ## How to use 24 | Pass either an array of package names to dedupe or "*" to dedupe everything possible. 25 | 26 | ```javascript 27 | var resolutions = require('browserify-resolutions'); 28 | ``` 29 | 30 | ```javascript 31 | // Dedupe Angular 32 | browserify(options) 33 | .plugin(resolutions, ['angular']) 34 | .bundle(); 35 | ``` 36 | 37 | ```javascript 38 | // Dedupe everything possible 39 | browserify(options) 40 | .plugin(resolutions, '*') 41 | .bundle(); 42 | ``` 43 | 44 | via Browserify CLI 45 | 46 | ```bash 47 | browserify path-to-your-entry-file.js -p [ browserify-resolutions '*' ] -o path-to-your-destination.js 48 | ``` 49 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var through = require('through2'); 3 | var join = require('path').join; 4 | var toString = Object.prototype.toString; 5 | 6 | // Exports 7 | // ________________________ 8 | 9 | exports = module.exports = 10 | 11 | /** 12 | * Entry point for the two plugins that comprise browserify-resolutions. 13 | * See below for detailed write-ups on each. 14 | * 15 | * @param {Object} bundler Browserify instance. 16 | * @param {Object|Array|String} packageMatcher List of modules to resolve a version for or 17 | * "*" to attempt to resolve all. 18 | * @return {Object} Browserify instance. 19 | */ 20 | function(bundler, packageMatcher) { 21 | packageMatcher = parseOptions( 22 | toString.call(packageMatcher) === '[object Object]' ? packageMatcher._ : packageMatcher 23 | ); 24 | 25 | function applyPlugins() { 26 | var options = { 27 | deduped: {}, 28 | resolved: {}, 29 | packageMatcher: packageMatcher 30 | }; 31 | 32 | return bundler 33 | .plugin(dedupeCache, options) 34 | .plugin(dedupeResolutions, options); 35 | } 36 | 37 | // Must re-attach plugins every time Browserify `reset`s as a new pipeline is created. 38 | bundler.on('reset', applyPlugins); 39 | 40 | return applyPlugins(); 41 | }; 42 | 43 | function parseOptions(options) { 44 | if(options === '*') { 45 | return options; 46 | } else if (_.isArray(options) && options.length === 1 && options[0] === '*') { 47 | return options[0]; 48 | } else if(_.isString(options)) { 49 | return [options]; 50 | } else if(!_.isArray(options)) { 51 | return []; 52 | } else { 53 | return options; 54 | } 55 | } 56 | 57 | // Plugin implementations 58 | // ________________________ 59 | 60 | /** 61 | * Custom Browserify deduper that exports the already instantiated module instead of 62 | * reinstantiating it like the regular Browserify deduper does. This is more in line 63 | * with how we'd expect `require` to work, caching the first result for subsequent calls. 64 | * 65 | * Solves: Libraries like Angular executing multiple times even if there's only a single 66 | * copy included in the bundle. Fortunately Angular warns us when this happens. 67 | * 68 | * Note: For safety, we only apply caching if the module dependencies are either all dupes too 69 | * (or has no dependencies). Even if two source file are identical, they could be requiring 70 | * different versions of the same dependencies. Case in point, Angular's CJS entry point index.js, 71 | * basically a shim that `require`s the main Angular source. This shim module will likely stay 72 | * identical for many Angular versions, while the actual source is different for all versions. 73 | * 74 | * E.g.: If we have two or more Angular versions bundled, their `index.js`s are likely indentical 75 | * and will be deduped. If we cached `index.js`, all `require('angular')`s will then return the same 76 | * Angular version; the one associated with the first `index.js` that the deduper comes across. 77 | * The bundle would still include multiple Angular versions since `angular.js`, the main source, 78 | * is different between all versions and not deduped. 79 | * 80 | * Note II: This method otherwise mimics Browserify's own to make sure nothing breaks. 81 | */ 82 | function dedupeCache(bundler, options) { 83 | var resolved = options.resolved; 84 | var deduped = options.deduped; 85 | 86 | bundler.pipeline.get('dedupe') 87 | .splice(0, 1, through.obj(function(row, enc, next) { 88 | var id = row.dedupe && !row.dedupeIndex ? row.dedupe : row.dedupeIndex; 89 | var stringId; 90 | 91 | if (id) { 92 | stringId = JSON.stringify(id); 93 | 94 | // For safety, only cache modules which dependencies are also all duped (or it has none). 95 | if (resolved[row.dedupe] && _.values(row.deps).every(isDuped)) { 96 | row.source = 'module.exports = require(' + stringId + ');'; 97 | } else { 98 | // Default browserify dedupe. 99 | row.source = 'arguments[4][' + stringId + '][0].apply(exports,arguments)'; 100 | } 101 | 102 | row.nomap = true; 103 | 104 | if (row.dedupeIndex && row.indexDeps) { 105 | row.indexDeps.dup = row.dedupeIndex; 106 | } 107 | } 108 | 109 | this.push(row); 110 | next(); 111 | })); 112 | 113 | function isDuped(id) { 114 | return !!deduped[id]; 115 | } 116 | 117 | return bundler; 118 | } 119 | 120 | /** 121 | * "Bower resolutions" for Browserify... sort of. Browserify/`npm dedupe` currently only dedupes 122 | * modules that are exactly identical, i.e. by checksum or version number. That's fine for Node, 123 | * but not great for the browser where we try to create the slimmest possible packages. If a bundle's 124 | * dependencies include multiple versions of the same module, Browserify will bundle them all anyway. 125 | * If the duplicated modules include large libraries like Angular, it could balloon the bundle's size. 126 | * 127 | * This plugin dedupes modules passed in options, ensuring there is only one version bundled. 128 | * Currently, the given version is the first one that Browserify parses. Typically, that is the desired one. 129 | * TODO: Allow choosing a specific module version to bundle. 130 | * 131 | */ 132 | function dedupeResolutions(bundler, options) { 133 | var modules = {}; 134 | var deps = {}; 135 | var index = {}; 136 | var rows = []; 137 | 138 | var resolved = options.resolved; 139 | var deduped = options.deduped; 140 | var packageMatcher = options.packageMatcher; 141 | var packageCache = bundler._options.packageCache; 142 | 143 | bundler.pipeline.on('package', packageListener); 144 | 145 | function packageListener(package) { 146 | if (isResolvablePackage(package)) { 147 | if (isCachedPackage(package)) { 148 | groupByPackage(package, getMain(package)); 149 | } else { 150 | bundler.pipeline.once('file', function(file) { 151 | if(isMain(package, file)) { 152 | groupByPackage(package, file); 153 | } 154 | }); 155 | } 156 | } 157 | 158 | function isResolvablePackage(package) { 159 | return package.main && package.name && 160 | (packageMatcher.indexOf(package.name) !== -1 || packageMatcher === '*'); 161 | } 162 | 163 | function isCachedPackage(package) { 164 | var packagePath = join(package.__dirname, 'package.json'); 165 | return packageCache && packageCache.hasOwnProperty(packagePath); 166 | } 167 | 168 | // Group all `main`s by their package name. 169 | function groupByPackage(package, file) { 170 | modules[package.name] = modules[package.name] || []; 171 | modules[package.name].push(file); 172 | 173 | // Flag to grab these dependencies when available in our 'deps' stream handler. 174 | deps[file] = true; 175 | } 176 | 177 | function isMain(package, file) { 178 | return getMain(package) === file; 179 | } 180 | 181 | function getMain(package) { 182 | return join(package.__dirname, package.main); 183 | } 184 | } 185 | 186 | bundler.pipeline.get('deps') 187 | .push(through.obj( 188 | function write(row, enc, next) { 189 | var file = row.file; 190 | 191 | // Store a reference to these dependencies as we want to try to dedupe them later. 192 | if (deps[file]) { 193 | deps[file] = row.deps; 194 | } 195 | 196 | this.push(row); 197 | next(); 198 | }, 199 | function end(cb) { 200 | _.each(modules, function(sources) { 201 | var resolvedSource; 202 | 203 | if (sources.length < 2) { 204 | // Found no dupes, bail. 205 | return; 206 | } 207 | 208 | // Resolve the most shallow package (in terms of path length) as the "original". 209 | // Otherwise, the bundle may be non-deterministic as the order of module-deps's 210 | // package traversal currently isn't dependable. 211 | // TODO?: Allow choosing a specific package version, but that's more difficult. 212 | sources = _.sortBy(sources, 'length'); 213 | resolvedSource = sources.shift(); 214 | resolved[resolvedSource] = true; 215 | 216 | // Dedupe the remaining sources 217 | sources.forEach(function(source) { 218 | deduped[source] = resolvedSource; 219 | }); 220 | }); 221 | 222 | // Dedupe the dependencies of any deduped modules. 223 | // If we don't do this, we may end up with orphaned code in our bundle. 224 | // TODO: Deduped modules with dependency differences could be problematic. 225 | _.each(deduped, function(resolved, dupe) { 226 | if (resolved) { 227 | _.each(deps[dupe], function(file, id) { 228 | var resolvedDependency = deps[resolved][id]; 229 | // We might have already picked up this dependency as a dupe. 230 | // Deduping to a dependency of the original could cause a circular reference. 231 | // A resolved dependency is "false" if that module was externalized via browserify.external. 232 | if (!deduped[file] && resolvedDependency !== false) { 233 | deduped[file] = resolvedDependency; 234 | } 235 | }); 236 | } 237 | }); 238 | 239 | cb(); 240 | })); 241 | 242 | // Browserify dedupes within the "sort" label. Here we mimic it with the results of our own deduping. 243 | bundler.pipeline.get('sort') 244 | .push(through.obj( 245 | function write(row, enc, next) { 246 | // Collect all row indexes added by Browserify's in the "sort" label to correctly populate dedupeIndex. 247 | index[row.id] = row.index; 248 | rows.push(row); 249 | next(); 250 | }, 251 | function end(cb) { 252 | // Array of ids for files that we're treating as originals. 253 | var originals = _(deduped) 254 | .values() 255 | .concat(_.keys(resolved)) 256 | .unique() 257 | .value(); 258 | 259 | _.each(rows, function(row) { 260 | var file = row.file; 261 | 262 | if (deduped[file]) { 263 | row.dedupe = deduped[file]; 264 | row.dedupeIndex = index[deduped[file]]; 265 | } else if (originals.indexOf(file) !== -1) { 266 | // Prevent Browserify's default dedupe algorithm from creating circular dependencies 267 | // by not allowing it to dedupe files which we're considering "originals". 268 | delete row.dedupe; 269 | delete row.dedupeIndex; 270 | } 271 | 272 | this.push(row); 273 | }, this); 274 | 275 | cb(); 276 | })); 277 | 278 | return bundler; 279 | } 280 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browserify-resolutions", 3 | "version": "1.1.0", 4 | "description": "A Browserify plugin that allows more explicit control of module deduping. It purges duplicate modules from the output bundle and prevents modules from loading several times.", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/Updater/browserify-resolutions.git" 10 | }, 11 | "keywords": [ 12 | "browserify", 13 | "browserify-plugin", 14 | "resolutions", 15 | "dedupe", 16 | "duplication" 17 | ], 18 | "scripts": { 19 | "test": "mocha" 20 | }, 21 | "dependencies": { 22 | "lodash": "^3.7.0", 23 | "through2": "^0.6.5" 24 | }, 25 | "devDependencies": { 26 | "browserify": "^10.0.0", 27 | "chai": "^2.2.0", 28 | "mocha": "^2.2.4", 29 | "watchify": "^3.2.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.jshintrc", 3 | "evil": true, 4 | "mocha": true, 5 | "globals": { 6 | "libs": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/app-a-test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var browserify = require('browserify'); 3 | var watchify = require('watchify'); 4 | var resolutions = require('../index'); 5 | var bundleCallback = require('./utils').bundleCallback; 6 | 7 | describe('when bundling app-a', function() { 8 | 9 | // Bundle expectations 10 | // -------------------------- 11 | var expectedBundledLibs = {}; 12 | 13 | expectedBundledLibs.vanilla = [ 14 | 'lib-a-1.0.0', 15 | 'lib-ab-1.0.0', 16 | 'lib-ab-2.0.0', 17 | 'lib-b-1.0.0', 18 | 'lib-b-2.0.0', 19 | 'lib-c-1.0.0' 20 | ].sort(); 21 | 22 | expectedBundledLibs['lib-a'] = expectedBundledLibs.vanilla; 23 | 24 | expectedBundledLibs['lib-ab'] = [ 25 | 'lib-a-1.0.0', 26 | 'lib-ab-1.0.0', 27 | 'lib-b-1.0.0', 28 | 'lib-b-2.0.0', 29 | 'lib-c-1.0.0' 30 | ].sort(); 31 | 32 | expectedBundledLibs['lib-ab,lib-b'] = [ 33 | 'lib-a-1.0.0', 34 | 'lib-ab-1.0.0', 35 | 'lib-b-1.0.0', 36 | 'lib-c-1.0.0' 37 | ].sort(); 38 | 39 | expectedBundledLibs['*'] = expectedBundledLibs['lib-ab,lib-b']; 40 | 41 | // Execution expectations 42 | // -------------------------- 43 | var expectedExecutedLibs = {}; 44 | 45 | expectedExecutedLibs.vanilla = expectedBundledLibs.vanilla.concat([ 46 | 'lib-a-1.0.0' 47 | ]).sort(); 48 | 49 | expectedExecutedLibs['lib-a'] = expectedBundledLibs['lib-a']; 50 | 51 | expectedExecutedLibs['lib-ab'] = [ 52 | 'lib-a-1.0.0', 53 | 'lib-a-1.0.0', 54 | 'lib-ab-1.0.0', 55 | 'lib-b-1.0.0', 56 | 'lib-b-2.0.0', 57 | 'lib-c-1.0.0' 58 | ].sort(); 59 | 60 | expectedExecutedLibs['lib-ab,lib-b'] = [ 61 | 'lib-a-1.0.0', 62 | 'lib-a-1.0.0', 63 | 'lib-ab-1.0.0', 64 | 'lib-b-1.0.0', 65 | 'lib-c-1.0.0' 66 | ].sort(); 67 | 68 | expectedExecutedLibs['*'] = [ 69 | 'lib-a-1.0.0', 70 | 'lib-ab-1.0.0', 71 | 'lib-b-1.0.0', 72 | 'lib-c-1.0.0' 73 | ].sort(); 74 | 75 | // Tests 76 | // -------------------------- 77 | var bundler; 78 | 79 | beforeEach(function() { 80 | libs = []; 81 | bundler = browserify({ 82 | entries: ['./test/app-a'] 83 | }); 84 | }); 85 | 86 | describe('using vanilla browserify', function() { 87 | it('dedupes identical sources', function(done) { 88 | bundler 89 | .bundle(bundleCallback(function(bundledLibs) { 90 | expect(bundledLibs.sort()).to.eql(expectedBundledLibs.vanilla); 91 | done(); 92 | })); 93 | }); 94 | 95 | it('executes deduped sources', function(done) { 96 | bundler 97 | .bundle(bundleCallback(function() { 98 | expect(libs.sort()).to.eql(expectedExecutedLibs.vanilla); 99 | done(); 100 | })); 101 | }); 102 | }); 103 | 104 | describe('using browserify-resolutions', function() { 105 | describe('and not passing options', function() { 106 | it('is vanilla dedupe', function(done) { 107 | bundler 108 | .plugin(resolutions) 109 | .bundle(bundleCallback(function(bundledLibs) { 110 | expect(bundledLibs.sort()).to.eql(expectedBundledLibs.vanilla); 111 | expect(libs.sort()).to.eql(expectedExecutedLibs.vanilla); 112 | done(); 113 | })); 114 | }); 115 | }); 116 | 117 | describe('and passing empty array', function() { 118 | it('is vanilla dedupe', function(done) { 119 | bundler 120 | .plugin(resolutions, []) 121 | .bundle(bundleCallback(function(bundledLibs) { 122 | expect(bundledLibs.sort()).to.eql(expectedBundledLibs.vanilla); 123 | expect(libs.sort()).to.eql(expectedExecutedLibs.vanilla); 124 | done(); 125 | })); 126 | }); 127 | }); 128 | 129 | describe('and passing a matching package name', function() { 130 | it('bundles and executes the matching package once', function(done) { 131 | var options = ['lib-ab']; 132 | 133 | bundler 134 | .plugin(resolutions, options) 135 | .bundle(bundleCallback(function(bundledLibs) { 136 | expect(bundledLibs.sort()).to.eql(expectedBundledLibs[options.toString()]); 137 | expect(libs.sort()).to.eql(expectedExecutedLibs[options.toString()]); 138 | done(); 139 | })); 140 | }); 141 | }); 142 | 143 | describe('and passing multiple matching package names', function() { 144 | it('bundles and executes the matching packages once', function(done) { 145 | var options = ['lib-ab', 'lib-b']; 146 | 147 | bundler 148 | .plugin(resolutions, options) 149 | .bundle(bundleCallback(function(bundledLibs) { 150 | expect(bundledLibs.sort()).to.eql(expectedBundledLibs[options.join(',')]); 151 | expect(libs.sort()).to.eql(expectedExecutedLibs[options.join(',')]); 152 | done(); 153 | })); 154 | }); 155 | }); 156 | 157 | describe('and passing *', function() { 158 | var options = '*'; 159 | 160 | it('bundles and executes all packages once', function(done) { 161 | bundler 162 | .plugin(resolutions, options) 163 | .bundle(bundleCallback(function(bundledLibs) { 164 | expect(bundledLibs.sort()).to.eql(expectedBundledLibs[options]); 165 | expect(libs.sort()).to.eql(expectedExecutedLibs[options]); 166 | done(); 167 | })); 168 | }); 169 | 170 | // Integration test to verify that the plugin is watchify-compatible. 171 | // Piggy-backing off of the '*'-option test b/c its more likely to expose flaws. 172 | // 173 | // TODO: Fails randomly due to the non-deterministic order in which `moduleDeps`'s' `package` event 174 | // is dispatching cached packages. As browserify-resolutions currently uses the first package it 175 | // comes across as the "original" and marks all other dupes, it makes the result non-deterministic as well. 176 | describe('and is rebundled with watchify', function() { 177 | it('produces the same bundle as the first time', function(done) { 178 | bundler._options.cache = {}; 179 | bundler._options.packageCache = {}; 180 | bundler = watchify(bundler); 181 | 182 | bundler 183 | .plugin(resolutions, options) 184 | .bundle(function() { 185 | bundler 186 | .bundle(bundleCallback(function(bundledLibs) { 187 | expect(bundledLibs.sort()).to.eql(expectedBundledLibs[options]); 188 | expect(libs.sort()).to.eql(expectedExecutedLibs[options]); 189 | done(); 190 | })); 191 | }); 192 | }); 193 | }); 194 | }); 195 | 196 | describe('and passing a matching package name that is a subset of another', function() { 197 | var options = ['lib-a']; 198 | 199 | it('dedupes only the matching package name, not the superset', function(done) { 200 | bundler 201 | .plugin(resolutions, options) 202 | .bundle(bundleCallback(function(bundledLibs) { 203 | expect(bundledLibs.sort()).to.eql(expectedBundledLibs[options]); 204 | expect(libs.sort()).to.eql(expectedExecutedLibs[options]); 205 | done(); 206 | })); 207 | }); 208 | 209 | // Test to verify that calling bundle() twice consecutively works, in general. 210 | // No specific reason its mirroring the test above, just piggy-backing off a verified result. 211 | describe('and calling bundle a second time', function() { 212 | it('produces the same bundle as the first time', function(done) { 213 | bundler 214 | .plugin(resolutions, options) 215 | .bundle(); 216 | 217 | bundler 218 | .bundle(bundleCallback(function(bundledLibs) { 219 | expect(bundledLibs.sort()).to.eql(expectedBundledLibs[options]); 220 | expect(libs.sort()).to.eql(expectedExecutedLibs[options]); 221 | done(); 222 | })); 223 | }); 224 | }); 225 | }); 226 | }); 227 | }); 228 | -------------------------------------------------------------------------------- /test/app-a/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | require('lib-a'); 3 | require('lib-ab'); 4 | require('lib-b'); 5 | require('lib-c'); 6 | })(); 7 | -------------------------------------------------------------------------------- /test/app-a/node_modules/lib-a/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | libs.push('lib-a-1.0.0'); 3 | })(); 4 | -------------------------------------------------------------------------------- /test/app-a/node_modules/lib-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib-a", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/app-a/node_modules/lib-ab/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | libs.push('lib-ab-1.0.0'); 3 | })(); 4 | -------------------------------------------------------------------------------- /test/app-a/node_modules/lib-ab/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib-ab", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/app-a/node_modules/lib-b/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | require('lib-a'); 3 | require('lib-ab'); 4 | libs.push('lib-b-1.0.0'); 5 | })(); 6 | -------------------------------------------------------------------------------- /test/app-a/node_modules/lib-b/node_modules/lib-a/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | libs.push('lib-a-1.0.0'); 3 | })(); 4 | -------------------------------------------------------------------------------- /test/app-a/node_modules/lib-b/node_modules/lib-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib-a", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/app-a/node_modules/lib-b/node_modules/lib-ab/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | libs.push('lib-ab-2.0.0'); 3 | })(); 4 | -------------------------------------------------------------------------------- /test/app-a/node_modules/lib-b/node_modules/lib-ab/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib-ab", 3 | "version": "2.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/app-a/node_modules/lib-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib-b", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/app-a/node_modules/lib-c/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | require('lib-b'); 3 | libs.push('lib-c-1.0.0'); 4 | })(); 5 | -------------------------------------------------------------------------------- /test/app-a/node_modules/lib-c/node_modules/lib-b/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | libs.push('lib-b-2.0.0'); 3 | })(); 4 | -------------------------------------------------------------------------------- /test/app-a/node_modules/lib-c/node_modules/lib-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib-b", 3 | "version": "2.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/app-a/node_modules/lib-c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib-c", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/app-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-a", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/app-b-test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var browserify = require('browserify'); 3 | var resolutions = require('../index'); 4 | var bundleCallback = require('./utils').bundleCallback; 5 | 6 | describe('when bundling app-b', function() { 7 | 8 | // Bundle expectations 9 | // -------------------------- 10 | var expectedBundledLibs = {}; 11 | 12 | expectedBundledLibs.vanilla = [ 13 | 'lib-a-1.0.0', 14 | 'lib-a-2.0.0', 15 | 'lib-b-1.0.0' 16 | ].sort(); 17 | 18 | expectedBundledLibs['lib-a'] = [ 19 | 'lib-a-1.0.0', 20 | 'lib-b-1.0.0' 21 | ].sort(); 22 | 23 | // Execution expectations 24 | // -------------------------- 25 | var expectedExecutedLibs = {}; 26 | 27 | expectedExecutedLibs.vanilla = expectedBundledLibs.vanilla; 28 | expectedExecutedLibs['lib-a'] = expectedBundledLibs['lib-a']; 29 | 30 | // Tests 31 | // -------------------------- 32 | var bundler; 33 | 34 | beforeEach(function() { 35 | libs = []; 36 | bundler = browserify({ 37 | entries: ['./test/app-b'] 38 | }); 39 | }); 40 | 41 | describe('using vanilla browserify', function() { 42 | it('dedupes identical sources', function(done) { 43 | bundler 44 | .bundle(bundleCallback(function(bundledLibs) { 45 | expect(bundledLibs.sort()).to.eql(expectedBundledLibs.vanilla); 46 | done(); 47 | })); 48 | }); 49 | 50 | it('executes deduped sources', function(done) { 51 | bundler 52 | .bundle(bundleCallback(function() { 53 | expect(libs.sort()).to.eql(expectedExecutedLibs.vanilla); 54 | done(); 55 | })); 56 | }); 57 | }); 58 | 59 | describe('using browserify-resolutions', function() { 60 | describe('and passing matching package which main is a CJS "wrapper" (ala Angular)', function() { 61 | it('dedupes both the wrapper and the source', function(done) { 62 | var options = ['lib-a']; 63 | 64 | bundler 65 | .plugin(resolutions, options) 66 | .bundle(bundleCallback(function(bundledLibs) { 67 | expect(bundledLibs.sort()).to.eql(expectedBundledLibs[options]); 68 | expect(libs.sort()).to.eql(expectedExecutedLibs[options]); 69 | done(); 70 | })); 71 | }); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/app-b/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | require('lib-a'); 3 | require('lib-b'); 4 | })(); 5 | -------------------------------------------------------------------------------- /test/app-b/node_modules/lib-a/index.js: -------------------------------------------------------------------------------- 1 | require('./source'); 2 | -------------------------------------------------------------------------------- /test/app-b/node_modules/lib-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib-a", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/app-b/node_modules/lib-a/source.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | libs.push('lib-a-1.0.0'); 3 | })(); 4 | -------------------------------------------------------------------------------- /test/app-b/node_modules/lib-b/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | require('lib-a'); 3 | libs.push('lib-b-1.0.0'); 4 | })(); 5 | -------------------------------------------------------------------------------- /test/app-b/node_modules/lib-b/node_modules/lib-a/index.js: -------------------------------------------------------------------------------- 1 | require('./source'); 2 | -------------------------------------------------------------------------------- /test/app-b/node_modules/lib-b/node_modules/lib-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib-a", 3 | "version": "2.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/app-b/node_modules/lib-b/node_modules/lib-a/source.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | libs.push('lib-a-2.0.0'); 3 | })(); 4 | -------------------------------------------------------------------------------- /test/app-b/node_modules/lib-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib-b", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/app-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-a", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/non-main-require-test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var browserify = require('browserify'); 3 | var resolution = require('../index'); 4 | var bundleCallback = require('./utils').bundleCallback; 5 | 6 | describe('when bundling app that does non-main requires', function() { 7 | var bundler; 8 | 9 | // Bundle expectations 10 | // -------------------------- 11 | var expectedBundledLibs = {}; 12 | 13 | expectedBundledLibs.vanilla = [ 14 | 'lib-a-1.0.0', 15 | 'lib-common-1.0.0' 16 | ].sort(); 17 | 18 | // Execution expectations 19 | // -------------------------- 20 | var expectedExecutedLibs = {}; 21 | 22 | expectedExecutedLibs.vanilla = [ 23 | 'lib-a-1.0.0', 24 | 'lib-common-1.0.0', 25 | 'lib-common-1.0.0' 26 | ]; 27 | 28 | beforeEach(function() { 29 | libs = []; 30 | bundler = browserify({ 31 | entries: ['./test/non-main-require'] 32 | }); 33 | }); 34 | 35 | describe('using vanilla browserify', function() { 36 | it('only dedupes identical sources', function(done) { 37 | bundler 38 | .bundle(bundleCallback(function (bundledLibs) { 39 | expect(bundledLibs.sort()).to.eql(expectedBundledLibs.vanilla); 40 | expect(libs.sort()).to.eql(expectedExecutedLibs.vanilla); 41 | done(); 42 | })); 43 | }); 44 | }); 45 | 46 | describe('using browserify-resolutions', function() { 47 | // TODO: Allow deduping non-main requires. 48 | it('is not yet able to dedupe non-main requires', function(done) { 49 | bundler 50 | .plugin(resolution, ['lib-common']) 51 | .bundle(bundleCallback(function (bundledLibs) { 52 | expect(bundledLibs.sort()).to.eql(expectedBundledLibs.vanilla); 53 | expect(libs.sort()).to.eql(expectedExecutedLibs.vanilla); 54 | done(); 55 | })); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/non-main-require/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | require('lib-a'); 3 | require('lib-common/non-main'); 4 | })(); 5 | -------------------------------------------------------------------------------- /test/non-main-require/node_modules/lib-a/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | require('lib-common/non-main'); 3 | libs.push('lib-a-1.0.0'); 4 | })(); 5 | -------------------------------------------------------------------------------- /test/non-main-require/node_modules/lib-a/node_modules/lib-common/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | libs.push('lib-common-1.0.0'); 3 | })(); 4 | -------------------------------------------------------------------------------- /test/non-main-require/node_modules/lib-a/node_modules/lib-common/non-main.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./index'); 2 | -------------------------------------------------------------------------------- /test/non-main-require/node_modules/lib-a/node_modules/lib-common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib-common", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/non-main-require/node_modules/lib-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib-a", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/non-main-require/node_modules/lib-common/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | libs.push('lib-common-1.0.0'); 3 | })(); 4 | -------------------------------------------------------------------------------- /test/non-main-require/node_modules/lib-common/non-main.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./index'); 2 | -------------------------------------------------------------------------------- /test/non-main-require/node_modules/lib-common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib-common", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/non-main-require/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "non-main-require", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/package-submodules-test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var browserify = require('browserify'); 3 | var resolutions = require('../index'); 4 | var bundleCallback = require('./utils').bundleCallback; 5 | 6 | describe('when bundling app that requires package.json-less submodules of a module (ala lodash)', function() { 7 | 8 | // Bundle expectations 9 | // -------------------------- 10 | var expectedBundledLibs = {}; 11 | 12 | expectedBundledLibs.vanilla = [ 13 | 'lib-a-1.0.0', 14 | 'lib-submodules/module-a-1.0.0', 15 | 'lib-submodules/module-a-2.0.0', 16 | 'lib-submodules/module-b-1.0.0', 17 | 'lib-submodules/module-b-2.0.0' 18 | ].sort(); 19 | 20 | // TODO: This is the result we want if/when we implement deduping submodules. 21 | expectedBundledLibs.resolutions = [ 22 | 'lib-a-1.0.0', 23 | 'lib-submodules/module-a-1.0.0', 24 | 'lib-submodules/module-b-1.0.0', 25 | ].sort(); 26 | 27 | // Execution expectations 28 | // -------------------------- 29 | var expectedExecutedLibs = {}; 30 | 31 | expectedExecutedLibs.vanilla = expectedBundledLibs.vanilla; 32 | expectedExecutedLibs.resolutions = expectedBundledLibs.resolutions; 33 | 34 | var bundler; 35 | 36 | beforeEach(function() { 37 | libs = []; 38 | bundler = browserify({ 39 | entries: ['./test/package-submodules'] 40 | }); 41 | }); 42 | 43 | describe('using vanilla browserify', function() { 44 | it('only dedupes identical sources', function(done) { 45 | bundler 46 | .bundle(bundleCallback(function (bundledLibs) { 47 | expect(bundledLibs.sort()).to.eql(expectedBundledLibs.vanilla); 48 | expect(libs.sort()).to.eql(expectedExecutedLibs.vanilla); 49 | done(); 50 | })); 51 | }); 52 | }); 53 | 54 | describe('using browserify-resolutions', function() { 55 | // TODO: Allow deduping submodules of a duplicate module. 56 | it('is not able to dedupe package.json-less submodules of a duplicate module', function(done) { 57 | bundler 58 | .plugin(resolutions, '*') 59 | .bundle(bundleCallback(function (bundledLibs) { 60 | expect(bundledLibs.sort()).to.eql(expectedBundledLibs.vanilla); 61 | expect(libs.sort()).to.eql(expectedExecutedLibs.vanilla); 62 | done(); 63 | })); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/package-submodules/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | require('lib-a'); 3 | require('lib-submodules/module-a/module-a'); 4 | require('lib-submodules/module-b/module-b'); 5 | })(); 6 | -------------------------------------------------------------------------------- /test/package-submodules/node_modules/lib-a/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | libs.push('lib-a-1.0.0'); 3 | require('lib-submodules/module-a/module-a'); 4 | require('lib-submodules/module-b/module-b'); 5 | })(); 6 | -------------------------------------------------------------------------------- /test/package-submodules/node_modules/lib-a/node_modules/lib-submodules/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | libs.push('lib-submodules-2.0.0'); 3 | })(); 4 | -------------------------------------------------------------------------------- /test/package-submodules/node_modules/lib-a/node_modules/lib-submodules/module-a/module-a.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | libs.push('lib-submodules/module-a-2.0.0'); 3 | })(); 4 | -------------------------------------------------------------------------------- /test/package-submodules/node_modules/lib-a/node_modules/lib-submodules/module-b/module-b.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | libs.push('lib-submodules/module-b-2.0.0'); 3 | })(); 4 | -------------------------------------------------------------------------------- /test/package-submodules/node_modules/lib-a/node_modules/lib-submodules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib-submodules", 3 | "version": "2.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/package-submodules/node_modules/lib-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib-a", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/package-submodules/node_modules/lib-submodules/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | libs.push('lib-submodules-1.0.0'); 3 | })(); 4 | -------------------------------------------------------------------------------- /test/package-submodules/node_modules/lib-submodules/module-a/module-a.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | libs.push('lib-submodules/module-a-1.0.0'); 3 | })(); 4 | -------------------------------------------------------------------------------- /test/package-submodules/node_modules/lib-submodules/module-b/module-b.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | libs.push('lib-submodules/module-b-1.0.0'); 3 | })(); 4 | -------------------------------------------------------------------------------- /test/package-submodules/node_modules/lib-submodules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib-submodules", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/package-submodules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-submodules", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bundleCallback: bundleCallback 3 | }; 4 | 5 | function getBundledLibs(bundleString) { 6 | var bundled = []; 7 | var regex = /libs\.push\('(lib-.+)'\)/g; 8 | var matches; 9 | 10 | /* jshint -W084 */ 11 | while (matches = regex.exec(bundleString)) { 12 | bundled.push(matches[1]); 13 | } 14 | 15 | return bundled; 16 | /* jshint +W084 */ 17 | } 18 | 19 | function bundleCallback(testFunc) { 20 | return function(err, buf) { 21 | var bufferString = buf.toString(); 22 | var bundledLibs = getBundledLibs(bufferString); 23 | eval(bufferString); 24 | 25 | return testFunc(bundledLibs); 26 | }; 27 | } 28 | 29 | --------------------------------------------------------------------------------