Angular 2 Bundling With Rollup

If you’re interested in the motivations for the bundling setup read the Back Story otherwise skip down to My Solution.

Back Story

A couple of weeks ago the Angular team released their first release candidate for Angular 2. I, along side the rest of the Angular community, was awash in anxious excitement. So I busted out my IDE and started the work of upgrading my work from beta17 to RC1. I didn’t mind the little breaking changes, because, of course, that is what I had signed up for by using the beta release. Everything went pretty smoothly until I went to actually run my app… it took more than 10 seconds to render the first pixels. Yikes! It took 633 network requests just to render my app (which is only 2 screens). (It turns out that if you use much of rxjs at all you will end up with this kind of problem.)

So it turns out that in order to be able to accomplish really small package sizes the Angular team needed to change the way that they were packaging the framework code. And as a part of that stopped bundling it all up neatly for my little app to use. So now my app has to load each imported dependency individually. 633 synchronous requests. I’m sure developers who are more experienced and wiser will be exuberant at the idea that the bundles are gone but for my little app, that deployment strategy works just fine. I was still committed to using Angular 2 and so I turned to looking at bundling strategies.

I looked at what some other people were doing and in particular what angular-cli was doing in their build step was to:

  • Development: don’t bundle
  • Production: bundle all application and vendor code together into one self-executing package.

This strategy sucks for a couple reasons:

  • Load time is terrible during development
  • Deployment (loading the code into the browser) is different between development and production. And when there are significant differences between environments it has always come back to bite me at some point.

I decided I needed a middle ground: I would bundle for development and production but in order to make the bundling step fast for dev I needed to break the bundle into two. One for all 3rd party code and one for my application code. So I started looking around and the various tools to do my bidding.

First, per some recommendations from the community, I tried to use systemjs-builder. It works ok for some easier bundling scenarios but I (and I tried pretty hard and long) could not get it to build my two bundles and have it work with sourcemaps. Also the documentation is scarce.

Next, I looked at using webpack. I’d heard its named whispered around the interwebs. Dear God the documentation sucks. There is seemingly a lot of it but its really hard to read. I got tired of trying to parse it and moved on.

Next, I took inspiration from Rob Wormald’s talk during ng-conf about the work they are doing to make application payloads small. He mentions, and gives an example of, using rollupjs. So I went to their docs and looked around. The docs are certainly lacking but I went forward anyway... and succeeded

Sidenote: Rollup does some cool stuff. They accomplish tree-shaking (excluding code from your bundle that you don’t use) by taking advantage of libraries that have their code written in es6.

My Solution

Have a rollup configuration for your vendor code that looks something like:

// rollup.config.vendor.js
import
typescript from ‘rollup-plugin-typescript’;
import nodeResolve from ‘rollup-plugin-node-resolve’;
// Custom Rollup Plugin to resolve rxjs deps
// Thanks to
https://github.com/IgorMinar/new-world-test/blob/master/es6-or-ts-bundle/rollup.config.js
class RollupNG2 {
constructor(options){
this.options = options;
}
resolveId(id, from){
if(id.startsWith(‘rxjs/’)){
return `${__dirname}/node_modules/rxjs-es/${id.replace(‘rxjs/’, ‘’)}.js`;
}
}
}
const rollupNG2 = (config) => new RollupNG2(config);
export default {
entry: ‘vendor.ts’,
dest: ‘dist/vendor.es2015.js’,
format: ‘iife’,
moduleName: ‘vendor’,
plugins: [
typescript(),
rollupNG2(),
nodeResolve({ jsnext: true, main: true }),
]
}

With an entry point that looks like

// vendor.ts
import
* as _angular_common from '@angular/common';
import * as _angular_compiler from '@angular/compiler';
import * as _angular_core from '@angular/core';
import * as _angular_http from '@angular/http';
import * as _angular_platformBrowser from '@angular/platform-browser';
import * as _angular_platformBrowserDynamic from '@angular/platform-browser-dynamic';
import * as _angular_router from '@angular/router';
import * as _angular_routerDeprecated from '@angular/router-deprecated';
import "rxjs/Rx"

export default {
_angular_common,
_angular_compiler,
_angular_core,
_angular_http,
_angular_platformBrowser,
_angular_platformBrowserDynamic,
_angular_router,
_angular_routerDeprecated
};

running `rollup -c rollup.config.vendor.js` will produce a bundle of all your vendor code compiled down to es6 (from Typescript). Now you need to get it down to es5. So we can just use `tsc` for that. All combined you have a bash command that looks something like:

> rollup -c rollup.config.vendor.js && tsc --out ./dist/vendor.js --target es5 --allowJs dist/vendor.es2015.js

And you end up with an ES5 IIFE bundle for all your vendor code.

Next, create a config for your application code that:

  • Excludes the vendor code
  • References the exported globals from the vendor bundle

So your rollup configuration looks like

// rollup.config.js
import
typescript from 'rollup-plugin-typescript';
import nodeResolve from 'rollup-plugin-node-resolve';
// Custom Rollup Plugin to resolve rxjs deps
// Thanks to
https://github.com/IgorMinar/new-world-test/blob/master/es6-or-ts-bundle/rollup.config.js
class RollupNG2 {
constructor(options){
this.options = options;
}
resolveId(id, from){
if(id.startsWith('rxjs/')){
return `${__dirname}/node_modules/rxjs-es/${id.replace('rxjs/', '')}.js`;
}
}
}
const rollupNG2 = (config) => new RollupNG2(config);

export default {
entry: 'app/main.ts',
dest: 'dist/bundle.es2015.js',
format: 'iife',
sourceMap: true,

plugins: [
typescript(),
rollupNG2(),
nodeResolve({ jsnext: true, main: true }),
],
  // This is how you exclude code from the bundle
external: [
'@angular/core',
'@angular/common',
'@angular/compiler',
'@angular/core',
'@angular/http',
'@angular/platform-browser',
'@angular/platform-browser-dynamic',
'@angular/router',
'@angular/router-deprecated'
],
  // This is how you link the referenced module ids to the
// global variables exposed by the vendor bundle.
  globals: {
'@angular/core': 'vendor._angular_core',
'@angular/http': 'vendor._angular_http',
'@angular/platform-browser-dynamic':
'vendor._angular_platformBrowserDynamic',
'@angular/router-deprecated': 'vendor._angular_routerDeprecated'
}
}

With your entry point looking something like an ng2 bootstrap file:

// app/main.ts
import
{bootstrap} from '@angular/platform-browser-dynamic'
import {AppComponent} from './app.component'
import {HTTP_PROVIDERS} from '@angular/http'
import {ROUTER_PROVIDERS} from '@angular/router-deprecated'

bootstrap(
AppComponent,
[
HTTP_PROVIDERS,
ROUTER_PROVIDERS
]
);

And with your build command looking roughly equivalent to the vendor one:

> rollup -c rollup.config.js && tsc --out ./dist/bundle.js --target es5  --allowJs dist/bundle.es2015.js

Now to run your app just serve your main html file that looks like

<html lang="en">
<head>
<script src="/node_modules/zone.js/dist/zone.js"></script>
<script src="/node_modules/reflect-metadata/Reflect.js"></script>
/head>
<body>
<base href="/">
<app></app>
<script src="/dist/vendor.js" ></script>
<script src="/dist/bundle.js" ></script>
</body>
</html>

Viola! It loads fast. If you want to optimize for production just run those same bundles through a minifier and they will be ~1/2 the size and run even faster!

In Conclusion

I went down a meandering path of frustration and I found rollup to be a great solution. If anyone has any sage advice to give along bundling javascript for Angular 2 I’d welcome it.

Bonus

Here is the watcher script I used to auto-build my app bundle while in development. It uses the onchange npm library.

onchange 'app/**/*.ts' -i -- <BUILD_APP_COMMAND>
Like what you read? Give Jonathan Samples a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.