Porting AngularJS 1.x to ES6

We maintain a rather large site (around 200 files) built in ES5 using an early version of AngularJS. We decided the risk of a full site rewrite was too large. Given how complex a full site port is we approximated a 6–8 month ramping period to gradually introduce a code style that is inline with the ES6 standard.

The end result being a combination of Babel, Gulp and SystemJS to ‘bundle’ all the application javascript, dependencies, css, less, and html into a handful of separate files. We also removed bower as a dependency so we could standardize our package manager using NPM.

Introducing a code style

First step is getting our site to run on AngularJS LTS (v1.6). By upgrading as many dependencies as possible we ensure our transition to ‘newer’ javascript workflows will be more seamless.

We introduced the John Papa style guide to any new feature development. This took time but as we continued to refactor and write new code, less of the code we maintained was the ‘old’ style.

Old style:

Application.js
Controller.js

New style:

Controller.js

Note we had to leave the ‘Application’ variable in app.js alone until the entire site was ready for ES6 due to our dependency of the global name spacing. Most of our files had Application.controller() in them.

This is looking more in line with how we expect to see an ES6 module.

ES6 modules

Now that we have a standard for our ES5 site, lets start to approach the ES6 format. One of the biggest benefits of ES6 is the module formats and global name spacing. No more polluting the global namespace. But, the standardization of the ES6 module is still slightly in flux and not fully supported. That’s why a module loader is required to ‘import’ a javascript module (like the ones from NPM). This process can be a bit confusing but all you need to know is there are several different formats of modules: UMD, AMD, CommonJS, and SystemJS. They all differ a little but provide basically the same functionality.

The important part to remember is all the module formats can be interchanged pretty easily through a transformation.

Because the browser doesn’t understand any of the module formats we need a module loader to interpolate the command:

import $ from ‘jQuery’;

We decided to use SystemJS as a module format and loader in the browser. SystemJS is pretty light weight and provides, out of the box, the perfect amount of flexibly we needed to get our giant site ported to ES6. SystemJS provides ES6 transpilation but we aren't worried about that yet. Aside from our import syntax all our code is still ES5 compliant. All thats required is a little oil in the gears.

One of the larger hurdles of the port process is configuring SystemJS. Lets take a look at the most basic configuration.

systemjs.conf.js

We are taking the node modules folder and turning it into an alias. Anytime ‘npm:’ is used in a path it will resolve to that folder. Now, the map property in the config is used to map a module name to a specific path. In this case we have our app, and angular.

This is what index.html looks like

Notice we only include the SystemJS dependency with a script tag. The other 200 files we used to define in this file are gone. All our files and modules will be loaded via SystemJS by alias or path.

This:

<script src="app/app.js"></script>

Becomes:

System.import('Application')

We are telling SystemJS to load our mapped configuration for that alias. In this case we told our SystemJS config ‘Application’ is the same as ‘app/app.js’. When the page loads our config will be loaded by path and filename, then ‘Application’ will be loaded by alias. The only difference being we are letting SystemJS do the loading of the file through configuration instead of letting the browser load it.

Now lets look at app.js

app.js

When we call import ‘angular’ SystemJS will try to resolve that module name. When we configured SystemJS we told it to look for angular in the node_modules folder. When we load our index you will notice SystemJS will make a call to node_modules to load the dependent library. Also notice we use the relative path to load the controller.

Lets take a look at app.controller.js

controller/app.controller.js

We took our ‘new style’ and just simply export the function using the ES6 module format and removed our angular module definition. This allows us to import the function in any context. Given how we have around 50+ controllers it is required to import and register each one in app.js. Yes we touched every file. Its also required to import all our angular module dependencies in app.js. The new code style we enforced allows us to remove a few lines and be off to ES6 module goodness.

Looking good!

Transpiling to ES6+

SystemJS is just handling modules, nothing more. Plus, when we went to scale SystemJS it took around 30 seconds for SystemJS to resolve all 200 module definitions and files paths. Not to mention adding even more time to transpile ES6 into ES5. This too slow for development and WAY too slow for production. SystemJS has the option to transpile in the bowser using babel but we found the performance of transpiling 200 files took way too long. We need to transpile all javascript before the browser loads and we need caching so we don’t transpile the entire site every time we make a change in development mode.

Insert Gulp and Babel.

We can create a gulp task that will take all our ES6 code and push it through babel in memory ahead of time instead of in the browser. This should speed things up considerately. We need to output the result of babel into a file that SystemJS can use to load as modules.

Welcome to our second hurdle. Fortunately theres a babel plugin that does just this. The ES2015 module to SystemJS plugin for babel will do some serious magic to our files. This will not only transpile our code, but it will take any file we transpile and register it as a SystemJS module. This means SystemJS will not need to go load and transpile the file when the page loads. It will already be loaded and registered in the browser by the time SystemJS goes to import it. This is important because that means we can combine all our JS into a single bundled file loaded ahead of time and then let SystemJS load the modules as we need them.

gulpfile.js

Couple things to note.

  • Use gulp cache to cache all files and only pass through to the next step the files that have changed. This means when we add a watch statement later sequential builds will only transpile the files we updated. Speeds things up significantly.
  • Our babel settings disable the es2015 module export convention because we are exporting all our files as system modules instead.
  • The transform-es2015-modules-systemjs plugin will register each file as a systemJS module. That means every file. For example ‘app/app.js’ becomes a registered system module and will not be requested using XHR when the browser loads (speeding up load time by 50x)
  • Gulp remember will return all the cached files to the stream that were not changed
  • This task will take ALL the javascript under the app folder and concat it into a single file.

Now that we have our bundle-app.js what do we do with it? Well the only thing we have to do is tell SystemJS about the registered modules (so it wont try to load them using XHR).

Our updated index.html

index.html

Notice we are still importing ‘Application’. The difference here being that all the js modules and files are already registered in the ‘bundle-app’.

System.import('bundle-app.js')

HTML Templates

Aside from css/less the other files we maintain in our app is the HTML templates. Because all our JS will be bundled into single file that means the only reason we need our folder structure in our app is to request the HTML templates from the appropriate folder.

Gulp and babel to the rescue! (again)

The module gulp-angular-templatecache is a great tool that we can use to grab all of our templates and register them as angular templates in cache. We can then transpile this file as a ES6 module and register it as a SystemJS. module. This provides a huge benefit because it means we can bundle all our html into a javascript file that loads when the page loads and is cache in angular. It also produces a valid ES6 file.

We create a new task that grabs all our HTML, converts them to angular templates, builds an es6 module, and transpile that module to systemjs es5 bundle.

gulpfile.js

We then have to load our registered SystemJS modules before we bootstrap the app. This may require some of the template paths throughout the app to be modified slightly (in our case we had to remove the top level folder).

Our index now looks like this:

index.html

This should result in no XHR calls being made to load the entire app. The load time performance will significantly improve.

Managing dependencies using NPM

Up until this point we are managing and transpiling all our app files that we maintain. Short of CSS that covers almost the entire site. The final remaining piece is bundling the dependencies. Remember our ‘import angular’ syntax? Well because we registered that as a NPM alias and that path is not a SystemJS module registered in our ‘bundle-app.js’ SystemJS will us XHR to try to load any dependency from the node_module folder. Slightly slow but the real driver here being we don’t want to copy our entire node_modules folder. We only want the app dependencies and node_modules is quite large and we dont know which files our app really depends on.

SystemJS bundler has this problem solved for us. While we could have used the bundler for our app, it tends to be a lot slower due to tree shaking and among other things. The up side is the SystemJS bundler is a lot smarter about managing dependencies.

We create another bundle. Except this time we let system decide which modules to include. We explicitly tell system to NOT include the files in our app (because we want to handle those ourselves through a faster means). SystemJS bundler will then output all of our dependencies (with tree shaking) into another bundle of modules in the SystemJS format.

In production workflows: don’t forget to copy the one node_module folder to dist that is required without a bundle — SystemJs!

CSS dependencies

No real graceful way to handle css dependencies automatically. We created a manual step in gulp to copy all the dependencies into a single file we include in our index.

Conclusion

Theres a lot of different workflow options out there. What we found was a lot of the tools and workflows break apart around the scalability. Don’t force your users to wait seconds while your app loads. When introducing a couple hundred files developing locally can be slow and painful. Also aside from another package manager (JSPM, bower) theres no clear answer for managing web app dependencies using npm. Using the correct tools can save you lots of hair pulling and frustration. Plus it’s rewarding to be contributing towards the advancement of the javascript ecosystem.