├── .gitignore ├── .travis.yml ├── test ├── tree-walker.spec.js ├── fixtures │ ├── app │ │ ├── app.js │ │ ├── common │ │ │ ├── util.js │ │ │ └── common.js │ │ ├── route2 │ │ │ └── route2.js │ │ └── route1 │ │ │ └── route1.js │ └── system.config.js ├── run.sh ├── builder.spec.js └── nearest-common-ancestor.spec.js ├── assets ├── tree.png └── result.png ├── .editorconfig ├── lib ├── clone.js ├── nearest-common-ancestor.js ├── tree-walker.js └── builder.js ├── LICENSE ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | test/output 4 | test/jspm_packages 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | - '0.11' 5 | - '0.12' 6 | -------------------------------------------------------------------------------- /test/tree-walker.spec.js: -------------------------------------------------------------------------------- 1 | var treewalker = require('../lib/tree-walker'); 2 | 3 | // TODO 4 | -------------------------------------------------------------------------------- /assets/tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swimlane/systemjs-route-bundler/HEAD/assets/tree.png -------------------------------------------------------------------------------- /assets/result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swimlane/systemjs-route-bundler/HEAD/assets/result.png -------------------------------------------------------------------------------- /test/fixtures/app/app.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | 3 | export default class App { 4 | 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/app/common/util.js: -------------------------------------------------------------------------------- 1 | export default class Util { 2 | print() { 3 | console.log('in util'); 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/app/common/common.js: -------------------------------------------------------------------------------- 1 | import Util from 'app/common/util'; 2 | 3 | export default class Common { 4 | constructor() { 5 | let util = new Util(); 6 | util.print(); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/app/route2/route2.js: -------------------------------------------------------------------------------- 1 | import App from '../app'; 2 | import Util from '../common/util'; 3 | 4 | export default class Route2 { 5 | constructor() { 6 | console.log('route2'); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /lib/clone.js: -------------------------------------------------------------------------------- 1 | var shallowClone = function(src){ 2 | var dest = {}; 3 | for (var prop in src) { 4 | dest[prop] = src[prop]; 5 | } 6 | return dest; 7 | }; 8 | 9 | module.exports = { 10 | shallowClone: shallowClone 11 | }; 12 | -------------------------------------------------------------------------------- /test/fixtures/app/route1/route1.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | 3 | import App from '../app'; 4 | import Util from '../common/util'; 5 | 6 | export default class Route1 { 7 | constructor() { 8 | console.log('route1'); 9 | angular.module('myModule', []); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /test/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | if [ -d "test/output" ]; then 6 | rm -rf test/output 7 | fi 8 | 9 | if [ ! -d "test/jspm_packages" ]; then 10 | jspm install 11 | fi 12 | 13 | babel test/fixtures/app --out-dir test/output/app --modules system 14 | 15 | mocha --timeout 5000 --compilers js:babel/register test/**/*.spec.js 16 | -------------------------------------------------------------------------------- /test/fixtures/system.config.js: -------------------------------------------------------------------------------- 1 | System.config({ 2 | "baseURL": ".", 3 | "transpiler": "babel", 4 | "babelOptions": { 5 | "optional": [ 6 | "runtime" 7 | ] 8 | }, 9 | "paths": { 10 | "*": "*.js", 11 | "github:*": "../jspm_packages/github/*.js", 12 | "npm:*": "../jspm_packages/npm/*.js" 13 | }, 14 | "buildCSS": true, 15 | "separateCSS": false 16 | }); 17 | 18 | System.config({ 19 | "map": { 20 | "angular": "github:angular/bower-angular@1.3.15", 21 | "babel": "npm:babel-core@5.1.13", 22 | "babel-runtime": "npm:babel-runtime@5.1.13", 23 | "core-js": "npm:core-js@0.9.4", 24 | "github:jspm/nodelibs-process@0.1.1": { 25 | "process": "npm:process@0.10.1" 26 | }, 27 | "npm:core-js@0.9.4": { 28 | "process": "github:jspm/nodelibs-process@0.1.1" 29 | } 30 | } 31 | }); 32 | 33 | -------------------------------------------------------------------------------- /test/builder.spec.js: -------------------------------------------------------------------------------- 1 | import routeBundler from '../lib/builder'; 2 | import assert from 'assert'; 3 | 4 | let routesSrc = [ 5 | { 6 | "stateName": "route1", 7 | "urlPrefix": "/route1", 8 | "type": "load", 9 | "src": "app/route1/route1" 10 | }, 11 | { 12 | "stateName": "route2", 13 | "urlPrefix": "/route2", 14 | "type": "load", 15 | "src": "app/route2/route2" 16 | } 17 | ].map(function(r) { return r.src; }); 18 | 19 | let config = { 20 | baseURL: 'test/output/', 21 | main: 'app/app', 22 | routes: routesSrc, 23 | bundleThreshold: 2.0, 24 | config: 'test/fixtures/system.config.js', 25 | sourceMaps: true, 26 | minify: false, 27 | dest: 'test/output', 28 | destJs: 'test/output/app.js' 29 | }; 30 | 31 | describe('builder', () => { 32 | it('can bundle a route', () => { 33 | return routeBundler.build(config).then(function(result) { 34 | 35 | // TODO: add some tests to ensure that files on disk are accurate. 36 | 37 | assert.equal(result, true); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2016 Swimlane LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/nearest-common-ancestor.js: -------------------------------------------------------------------------------- 1 | // derived from: http://stackoverflow.com/a/5350888/253773 2 | 3 | // finds nearest common ancestor for array of nodes; 4 | var nca = function(nodes){ 5 | var result = nodes[0]; 6 | 7 | nodes.forEach(function(n, i){ 8 | if (i === 0) return; 9 | result = commonAncestor(result, n); 10 | }) 11 | 12 | return result; 13 | } 14 | 15 | // returns array of parents of a node. root is first 16 | function parents(node) { 17 | var nodes = []; 18 | for (; node; node = node.parent) { 19 | nodes.unshift(node) 20 | } 21 | return nodes; 22 | } 23 | 24 | // returns nearest common ancestor for two nodes 25 | function commonAncestor(node1, node2) { 26 | var parents1 = parents(node1); 27 | var parents2 = parents(node2); 28 | 29 | if (parents1[0].moduleName !== parents2[0].moduleName) { 30 | throw "No common ancestor!"; 31 | } 32 | 33 | for (var i = 0; i < parents1.length; i++) { 34 | if (parents2[i] === undefined || parents1[i].moduleName !== parents2[i].moduleName) { 35 | return parents1[i - 1]; 36 | } 37 | } 38 | 39 | return parents1[i - 1]; 40 | } 41 | 42 | module.exports = { 43 | nca: nca 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "systemjs-route-bundler", 3 | "description": "SystemJS Route-Driven Bundler", 4 | "version": "1.6.0", 5 | "main": "lib/builder.js", 6 | "homepage": "http://swimlane.com/", 7 | "author": { 8 | "name": "Swimlane", 9 | "email": "marjan.georgiev@swimlane.com", 10 | "web": "http://swimlane.com/" 11 | }, 12 | "licenses": [ 13 | { 14 | "type": "MIT", 15 | "url": "http://opensource.org/licenses/mit-license.php" 16 | } 17 | ], 18 | "dependencies": { 19 | "gulp": "^3.8.11", 20 | "gulp-insert": "^0.4.0", 21 | "systemjs-builder": "0.15.13", 22 | "rsvp": "^3.0.18" 23 | }, 24 | "devDependencies": { 25 | "babel": "^5.1.13", 26 | "babel-runtime": "^5.1.13", 27 | "jspm": "^0.17.0-beta.11", 28 | "mocha": "^2.2.4" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/Swimlane/systemjs-route-bundler.git" 33 | }, 34 | "keywords": [ 35 | "systemjs", 36 | "route", 37 | "bundler" 38 | ], 39 | "scripts": { 40 | "test": "test/run.sh" 41 | }, 42 | "jspm": { 43 | "directories": { 44 | "baseURL": "test/output", 45 | "lib": "test/fixtures", 46 | "packages": "test/jspm_packages" 47 | }, 48 | "configFile": "test/fixtures/system.config.js", 49 | "dependencies": { 50 | "angular": "github:angular/bower-angular@^1.3.15" 51 | }, 52 | "devDependencies": { 53 | "babel": "npm:babel-core@^5.1.13", 54 | "babel-runtime": "npm:babel-runtime@^5.1.13", 55 | "core-js": "npm:core-js@^0.9.4" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/nearest-common-ancestor.spec.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var nearestCommonAncestor = require('../lib/nearest-common-ancestor'); 3 | 4 | // tests derived from: https://gist.github.com/benpickles/4059636 5 | 6 | function createElement(name, parent) { 7 | var elem = { 8 | "moduleName": name 9 | } 10 | if (parent) { 11 | if (parent.children) { 12 | parent.children.push(elem); 13 | } else { 14 | parent.children = [elem]; 15 | } 16 | elem.parent = parent; 17 | } 18 | return elem; 19 | } 20 | 21 | var root = createElement('root') // . 22 | var elem1 = createElement('elem1', root) // ├── elem1 23 | var elem2 = createElement('elem2', elem1) // | ├── elem2 24 | var elem3 = createElement('elem3', elem1) // | └── elem3 25 | var elem4 = createElement('elem4', elem3) // | └── elem4 26 | var elem5 = createElement('elem5', root) // └── elem5 27 | 28 | describe('nearest-common-ancestor', function() { 29 | it('has the same parent', function() { 30 | assert(nearestCommonAncestor.nca([elem1, elem5]) === root); 31 | }) 32 | 33 | it('has same parent but deeper', function() { 34 | assert(nearestCommonAncestor.nca([elem4, elem5]) === root); 35 | }) 36 | 37 | it('has direct child of the other', function() { 38 | assert(nearestCommonAncestor.nca([elem1, elem2]) === elem1); 39 | }) 40 | 41 | it('has grandchild of the other', function () { 42 | assert(nearestCommonAncestor.nca([elem1, elem4]) === elem1); 43 | }) 44 | 45 | it('has no common ancestor', function () { 46 | var root2 = createElement(); 47 | var thrown = false; 48 | try { 49 | nearestCommonAncestor.nca([elem3, root2]); 50 | } catch (e) { 51 | thrown = true; 52 | } 53 | assert(thrown); 54 | }) 55 | }); 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SystemJS Route Bundle Builder 2 | 3 | A build tool for [SystemJS Builder](https://github.com/systemjs/builder) that will identify your routes and build separate JS files for each. 4 | 5 | ### Concept 6 | 7 | Bundling isn't a new concept but with a large applications you have quite a bit of overlap of components between your routes. Typically those bundles would just be looped up into the main momdule. Our bundler is unique in the fact that it identifies potential optimizations and creates micro-bundles. So lets take a look at: 8 | 9 | ![example](https://raw.githubusercontent.com/swimlane/systemjs-route-bundler/master/assets/tree.png) 10 | 11 | We can see that the `Modal` component is used by `Login` and `Profile` but not by `Admin`. We can also see that `Select` is used by all the modules. The most optimal way to download this module graph would be to only download `Modal` when `Login` or `Profile` is requested. But you don't want to include it in the main download nor do you want to include it twice in each module. Our bundler identifies the overlap and creates a new module that is shared between those. So the above example results in something like: 12 | 13 | ![result](https://raw.githubusercontent.com/swimlane/systemjs-route-bundler/master/assets/result.png) 14 | 15 | This bundler can work with ANY platform, all you need is a route definition and to use SystemJS. At Swimlane we use Angular 1.x in our production application, so we wanted to make it work nicely with Angular. So we created a demo [AngularJS + SystemJS seed project](https://github.com/swimlane/angular1-systemjs-seed) that demonstrates this! 16 | 17 | In short, the bundler can cut your initial load time to tenths of what it is now without having to manage your bundle definitions! 18 | 19 | ### Configuration 20 | 21 | Option | Description 22 | ------------- | ------------- 23 | baseURL | Base URL of the project 24 | dest | Destination folder for the output 25 | main | The main file of your application 26 | destMain | The destination folder of your main file 27 | routes | An array of the file names of the main routes of your project. Each of the routes will have its own bundle 28 | bundleThreshold | The ratio of routes including a module over which the module will be bundled in the main bundle. Value must been between 0 and 1. 0.6 means that if 60% of the routes contain a single module, that module will be bundled in the main bundle 29 | jspmConfigPath | Path to the systemjs config file 30 | jspmBrowserPath | Path to the systemjs browser config file 31 | sourceMaps | Build sourceMaps for the bundles 32 | minify | Minify the javascript 33 | mangle | Mangle javascript variables 34 | verboseOutput | Output debug information while tracing and bundling 35 | ignoredPaths | Paths that will not be bundled. Put all paths that contain external libraries here 36 | 37 | Check the [AngularJS + SystemJS seed](https://github.com/swimlane/angular1-systemjs-seed/blob/master/gulpfile.js#L230) project for an example configuration. 38 | 39 | ### Credits 40 | 41 | `systemjs-route-bundler` is a [Swimlane](http://swimlane.com) open-source project; we believe in giving back to the open-source community by sharing some of the projects we build for our application. Swimlane is an automated cyber security operations and incident response platform that enables cyber security teams to leverage threat intelligence, speed up incident response and automate security operations. 42 | -------------------------------------------------------------------------------- /lib/tree-walker.js: -------------------------------------------------------------------------------- 1 | var RSVP = require('rsvp'); 2 | var clone = require('./clone'); 3 | var nearestCommonAncestor = require('./nearest-common-ancestor'); 4 | 5 | // returns all the trees of the file's dependencies, optimized 6 | var getTrees = function(main, config, treeCache, builder){ 7 | var inverseIndex = {}; 8 | var treeIndex = {}; 9 | var trees = []; 10 | 11 | // adds tree to caches 12 | var addToCache = function(tree){ 13 | var found = trees.filter(function(el){ 14 | return el.moduleName === tree.moduleName; 15 | }); 16 | 17 | if (found.length === 0){ 18 | treeCache[tree.moduleName] = tree; 19 | trees.push(tree); 20 | treeIndex[tree.moduleName] = tree; 21 | } 22 | } 23 | 24 | var buildDeps = function(src, level){ 25 | if (config.verboseOutput){ 26 | // console.log('\t\t tracing ' + src); 27 | } 28 | 29 | // trace source to get dependency tree 30 | return builder.trace(src, {browser: true, production: true}).then(function(tt){ 31 | var traceTree = { 32 | moduleName: src, 33 | tree: tt 34 | } 35 | addToCache(traceTree, src); 36 | 37 | // extract dependency source paths 38 | var sources = Object.keys(traceTree.tree); 39 | 40 | // process each dependency individually, and collect their trees 41 | var subTrees = []; 42 | var promises = []; 43 | 44 | sources.forEach(function(source){ 45 | if (source === src){ 46 | return; 47 | } 48 | 49 | if (inverseIndex[source]){ 50 | if (inverseIndex[source].indexOf(traceTree) === -1) { 51 | inverseIndex[source].push(traceTree); 52 | } 53 | } else { 54 | inverseIndex[source] = [traceTree]; 55 | } 56 | 57 | promises.push(new RSVP.Promise(function(resolve, reject) { 58 | if (treeCache[source]){ 59 | var subTree = clone.shallowClone(treeCache[source]); 60 | addToCache(subTree, source); 61 | subTrees.push(subTree); 62 | resolve(); 63 | } else { 64 | buildDeps(source, level + 1).then(function (subTree) { 65 | subTrees.push(subTree); 66 | resolve(); 67 | }); 68 | } 69 | })); 70 | }) 71 | 72 | return RSVP.all(promises).then(function(){ 73 | traceTree.children = subTrees; 74 | 75 | subTrees.forEach(function(subTree){ 76 | subTree.parent = traceTree; 77 | }) 78 | return traceTree; 79 | }); 80 | 81 | }, function(error) { 82 | console.log(error); 83 | console.log(error.stack); 84 | }); 85 | } 86 | 87 | return buildDeps(main, 1).then(function(tree){ 88 | Object.keys(inverseIndex).forEach(function(depName){ 89 | var depTree = treeIndex[depName]; 90 | 91 | var commonAncestor = nearestCommonAncestor.nca(inverseIndex[depName]); 92 | commonAncestor.tree = builder.addTrees(commonAncestor.tree, depTree.tree); 93 | 94 | inverseIndex[depName].forEach(function(n){ 95 | if (n.moduleName === depTree.moduleName || n.moduleName === commonAncestor.moduleName){ 96 | return; 97 | } 98 | n.tree = builder.subtractTrees(n.tree, depTree.tree); 99 | }) 100 | 101 | }) 102 | return treeIndex; 103 | }); 104 | } 105 | 106 | module.exports = { 107 | getTrees: getTrees 108 | }; 109 | -------------------------------------------------------------------------------- /lib/builder.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var treeWalker = require('./tree-walker'); 3 | var Builder = require('systemjs-builder'); 4 | var RSVP = require('rsvp'); 5 | var insert = require('gulp-insert'); 6 | 7 | var builder; 8 | var appTree; 9 | var routeTrees = []; 10 | var promises = []; 11 | var treeCache = {}; 12 | 13 | RSVP.on('error', function(reason) { 14 | console.error(false, reason.stack); 15 | }); 16 | 17 | var build = function (config) { 18 | builder = new Builder(config.baseURL); 19 | if (config.verboseOutput){ 20 | console.log('Config:'); 21 | console.log(config); 22 | console.log(''); 23 | } 24 | 25 | console.log('tracing source files...'); 26 | return builder.loadConfig(config.jspmConfigPath, undefined, true).then(function() { 27 | return treeWalker.getTrees(config.main, config, treeCache, builder).then(function (tree) { 28 | appTree = tree; 29 | 30 | config.routes.forEach(function (route) { 31 | promises.push(new RSVP.Promise(function (resolve, reject) { 32 | treeWalker.getTrees(route, config, treeCache, builder).then(function (tree) { 33 | routeTrees.push(tree); 34 | resolve(); 35 | }); 36 | })); 37 | }); 38 | 39 | return RSVP.all(promises).then(function () { 40 | // Remove app tree dependencies from route trees; 41 | removeDepsFromRoutes(); 42 | // generate inverse index of dependencies 43 | var inverseIndex = generateInverseIndex(); 44 | // generate bundles 45 | console.log('generating bundles...'); 46 | var bundles = generateBundles(inverseIndex, config.bundleThreshold, config.main); 47 | // build trees 48 | console.log('building...'); 49 | return buildTrees(bundles, config, builder); 50 | }); 51 | }, function (error) { 52 | console.error(error.stack) 53 | }); 54 | 55 | }); 56 | }; 57 | 58 | var removeDepsFromRoutes = function () { 59 | Object.keys(appTree).forEach(function (moduleName) { 60 | routeTrees.forEach(function (treeIndex) { 61 | if (treeIndex[moduleName]) { 62 | // deleting the dep tree 63 | delete treeIndex[moduleName]; 64 | // removing dep from the other trees 65 | Object.keys(treeIndex).forEach(function (depName) { 66 | treeIndex[depName].tree = builder.subtractTrees(treeIndex[depName].tree, appTree[moduleName].tree); 67 | }); 68 | } 69 | }); 70 | }); 71 | }; 72 | 73 | var generateInverseIndex = function () { 74 | var inverseIndex = {}; 75 | routeTrees.forEach(function (treeIndex, i) { 76 | Object.keys(treeIndex).forEach(function (depName) { 77 | if (inverseIndex[depName] === undefined) { 78 | inverseIndex[depName] = [i]; 79 | } else { 80 | inverseIndex[depName].push(i); 81 | } 82 | }); 83 | }); 84 | return inverseIndex; 85 | }; 86 | 87 | var generateBundles = function (inverseIndex, bundleThreshold, mainName) { 88 | var bundles = {}; 89 | // generating bundles 90 | Object.keys(inverseIndex).forEach(function (moduleName) { 91 | // if it's included in only one route, leave it there 92 | if (inverseIndex[moduleName].length === 1) { 93 | return; 94 | } 95 | 96 | var module = routeTrees[inverseIndex[moduleName][0]][moduleName]; 97 | if (inverseIndex[moduleName].length / routeTrees.length >= bundleThreshold) { 98 | // if it's included in more than the threshold of the routes, put it in app 99 | //console.log('shared by more than ' + (bundleThreshold*100) + '% of routes - including in app'); 100 | appTree[mainName].tree = builder.addTrees(appTree[mainName].tree, module.tree); 101 | appTree[moduleName] = module; 102 | } else { 103 | // otherwise, put it in a bundle 104 | var bundleName = inverseIndex[moduleName].sort().join('-') + ".js"; 105 | if (bundles[bundleName] === undefined) { 106 | bundles[bundleName] = module; 107 | } else { 108 | bundles[bundleName].tree = builder.addTrees(bundles[bundleName].tree, module.tree); 109 | } 110 | } 111 | 112 | // remove from other trees; 113 | inverseIndex[moduleName].forEach(function (index) { 114 | var treeIndex = routeTrees[index]; 115 | delete treeIndex[moduleName]; 116 | 117 | Object.keys(treeIndex).forEach(function (depName) { 118 | treeIndex[depName].tree = builder.subtractTrees(treeIndex[depName].tree, module.tree); 119 | }); 120 | 121 | }) 122 | }); 123 | return bundles; 124 | }; 125 | 126 | var buildTrees = function (bundles, config, builder) { 127 | var builderPromises = []; 128 | 129 | // build bundles 130 | var bundlesConfig = {}; 131 | console.log('building bundles...'); 132 | Object.keys(bundles).forEach(function (bundleName) { 133 | builderPromises.push(new RSVP.Promise(function (resolve, reject) { 134 | buildTree(bundles[bundleName], "bundles/" + bundleName, config, builder).then(function () { 135 | resolve(); 136 | }, function (error) { 137 | console.log(error); 138 | console.log(error.stack); 139 | }); 140 | })); 141 | 142 | var bundledModules = []; 143 | Object.keys(bundles[bundleName].tree).filter(function (item) { 144 | return item.indexOf('.css!') === -1 145 | }).forEach(function(n){ 146 | bundledModules.push(n); 147 | }) 148 | 149 | bundlesConfig["bundles/" + bundleName] = bundledModules; 150 | }); 151 | 152 | // build route trees 153 | console.log('building routes...'); 154 | routeTrees.forEach(function (treeIndex) { 155 | Object.keys(treeIndex).forEach(function (moduleName) { 156 | builderPromises.push(new RSVP.Promise(function (resolve, reject) { 157 | buildTree(treeIndex[moduleName], moduleName, config, builder).then(function () { 158 | resolve(); 159 | }, function (error) { 160 | console.log(error); 161 | console.log(error.stack) 162 | }); 163 | })); 164 | }); 165 | }); 166 | 167 | // build root app 168 | console.log('building app...'); 169 | Object.keys(appTree).forEach(function (moduleName) { 170 | builderPromises.push(new RSVP.Promise(function (resolve, reject) { 171 | var isMainModule = config.main.indexOf(moduleName) === 0; 172 | 173 | buildTree(appTree[moduleName], moduleName, config, builder).then(function () { 174 | if (isMainModule) { 175 | if (config.verboseOutput){ 176 | console.log('embedding bundles config...'); 177 | } 178 | var bundlesString = "System.config({bundles: " + JSON.stringify(bundlesConfig, null, 4) + "});"; 179 | gulp.src(config.dest + '/' + config.main) 180 | .pipe(insert.prepend(bundlesString)) 181 | .pipe(gulp.dest(config.destMain)) 182 | .on('end', function() { 183 | resolve(); 184 | }) 185 | } else { 186 | resolve(); 187 | } 188 | }, function (error) { 189 | console.log(error); 190 | console.log(error.stack) 191 | }) 192 | })); 193 | }); 194 | 195 | return RSVP.all(builderPromises).then(function () { 196 | console.log('build succeeded'); 197 | return true; 198 | }, function (error) { 199 | console.log(error); 200 | console.log(error.stack) 201 | }) 202 | }; 203 | 204 | var buildTree = function (tree, destination, config, builder) { 205 | // Don't build external libraries 206 | for (var i = 0; i < config.ignoredPaths.length; i++) { 207 | if (destination.indexOf(config.ignoredPaths[i]) !== -1){ 208 | if (config.verboseOutput){ 209 | console.log('\t\t skipping ' + destination); 210 | } 211 | return new RSVP.Promise(function (resolve, reject) { 212 | resolve(); 213 | }) 214 | } 215 | }; 216 | 217 | if (config.verboseOutput){ 218 | console.log('\t\t building ' + config.dest + '/' + destination); 219 | } 220 | 221 | return builder.bundle(tree.tree, config.dest + '/' + destination + "", { 222 | sourceMaps: config.sourceMaps, 223 | minify: config.minify, 224 | mangle: config.mangle, 225 | config: config.jspmConfigPath, 226 | rollup: true 227 | }) 228 | }; 229 | 230 | module.exports = { 231 | build: build 232 | }; 233 | --------------------------------------------------------------------------------