ES6 + Grunt + Traceur + AngularJS = CLUSTER! (OUTDATED)

Getting started with simple ES6 apps is trivial - just add script tags referencing traceur and bootstrap and you are off. This is a great way to get started and get a feel for ES6. Add AngularJS to the mix and it can still be relatively easy as long as you wire up your application correctly.

Though this is fun and gets you going, it is not practical in a real production app. ES6 scripts are compiled into ES5 on the fly, which, as you might guess, is going to come at the cost of performance. For a production app, the sensible option is to compile all of your scripts ahead of time and reference the compiled file(s) in your application. This is however easier said than done. Especially if it is an AngularJS app.

Erik Arvidsson from Google was on Javascript Jabber recently talking about Traceur and even he mentioned that it does take some effort to get your builds going and believe you me, it did take quite some serious digging around, especially for a journeyman like me. Here is the list of issues I ran into, how I mitigated a few and a few that I gave up on because they were beyond my control. First, the big one and the one that I figured out:

Compile ES6 scripts to ES5

Phew! Sounds simple, you either use the traceur command line tool or if you use Grunt/Gulp for your builds use one of the traceur plugins or create a custom task and call the command line tool. All of these approaches directly or indirectly call the traceur tool, so it is a good idea to learn a little bit about the tool first. You can find the briefest and most cursory of helps here and here. It took some toying around with different options to understand the intricacies and what the different options meant.

I wanted to shed some light on one particular option: the script option compiles the file down to JS in the global scope and will it run immediately like we are used to. If a file is not preceded by the script option, it is wrapped inside a module of your choice (System, AMD or CommonJS). This is important if you want to just run some bits of JS immediately after loading it. Say you set up all your AngularJS modules in a single file:

angular.module('myapp', ['myapp.users', 'myapp.products', 'myapp.orders']);

angular.module('myapp.users', [...]);  
angular.module('myapp.products', [...]);  
angular.module('myapp.orders', [...]);  

Wrapping this in a module does not make sense. You just want to execute this bit of JS once you run into it. This is where the -script flag comes in handy. This means that depending on the Grunt/Gulp plugin you use, you might have to add multiple tasks. This also means that the script tags on your page need to be ordered.

I use Grunt for my builds so I will elaborate on my experience with Grunt. There are three or four grunt traceur plugins that pull up upon searching npm, the most popular being grunt-traceur. I started off with grunt-traceur seeing that it was downloaded over 1500 times in October alone, while the others were in the hundreds. The repo too seemed to be considerably more active on Github.

grunt-traceur accepts a glob format and compiles the files that match the specified pattern into individual ES5 files. This plugin expects you to concatenate and minify (more on minification below) as separate steps (not a big deal). It only registers the modules and does not get them or in layman's terms - run them. My assumption is that you will have to concat the files and then get the main module to get things rolling. I wasn't sure what the right way to do this was and I couldn't find any help and I wasn't getting anywhere.

I then watched John Lindquist's video about Traceur and Grunt (about 7 months old at the time of writing this post). John suggested using grunt-traceur-latest. I didn't get anywhere with that either. Even that accepted a glob and relied upon the dev to add get's. Then I ran into this Github repo which uses grunt-traceur-simple. The thing I liked about this plugin was that you point it to a file that is the entry point of your application, it will find all the dependencies, compile them, concat them into your desired destination file and call the main file to kick things off - very much like one of the supported options if you directly use the traceur command line tool. In addition to this, grunt-traceur-simple also provides a handy option to include the traceur-runtime.js which is nice - saves an extra HTTP request compared to adding it as a separate script tag or could potentially save an extra concat during builds (small mercies).

traceur: {  
            options: {
                includeRuntime: true,
                traceurOptions: "--experimental --source-maps"
            },
            dist: {
                files: {
                    '<%= distFolder %>/app/app.es5.js': '<%= publicFolder %>/src/app/app.js'
                }
            }
        },

grunt.loadNpmTasks('grunt-traceur-simple');  

Now that I was able to have my grunt task compile my ES6 files and have the necessary code to kick it off, all I had to do was include the compiled ES5 file on my page. Instead of having a separate file that tied together the different AngularJS modules and compile it using the script option, I exported my modules as follows:

import modulesINeedAsDependeny from "./pathToModule";

class Security {  
    constructor(myAngularJSDependcy) {
        this._myAngularJSDependcy = myAngularJSDependcy;
    }

    get user() {
        return this._myAngularJSDependcy.user;
    }
}

export default angular.module('myapp.security', [modulesINeedAsDependeny.name])  
    .service('security', Security);

Here is an example of a security service that depends on another AngularJS service which lives in a separate module. I import the other module (modulesINeedAsDependeny) and inject it as a dependency in to my new module (using the module's name property). This new module is what gets exported so it can be imported in other places. The way this would be consumed into my main entry point file (app.js) is as follows:

import securityModule from "./pathTosecurityModule";  
...
var app = angular.module('myapp', [securityModule.name, ....]);  

When my grunt traceur task runs, it compiles all the dependencies. All I do now is add a reference to my app.es5.js file in my page and I'm all set.

Minification

This could be a deal breaker for some. As of this moment, if you rely on ng-annotate for annotations you should know that it will not work on code that has been compiled down by traceur. This means that you cannot minify the code. One of the commentors on ng-annotate's GitHub issues page suggested switching the controllers to use functions instead of classes to get around this problem but I did not want to go down that path. This is an open issue on ng-annotate's Github page so I am sure this will be resolved sooner than later. For the time being, I have forgone minification and crossing my fingers, hoping that the fix will be in soon. I can afford to do live without minification because at this point the application that I am working on is being tested internally.

PhantomJS

Another issue that I ran into is that PhantomJS does not like the code that Traceur compiles. It throws the following error:

PhantomJS 1.9.7 (Windows 7) ERROR  
  TypeError: 'undefined' is not a function (evaluating 'ModuleStore.register.bind(ModuleStore)')
  at D:/pathtomyapp/app/app.js:920

PhantomJS 1.9.7 (Windows 7): Executed 0 of 0 ERROR (1.038 secs / 0 secs)  

This means that for the time being I have forgo using a headless browser like PhantomJS and use either Chrome or Firefox. This, even though a little annoying, is not a deal breaker for me.

Visual Studio Intellisense

There is no ES6 intellisense support in Visual Studio at the time of this writing. If you are on the Microsoft stack and use Visual Studio as your IDE, then for the time being you either have to live without intellisense support and live with the squiggly lines and formatting that VS forces upon you or like me use Sublime Text for JS and Visual Studio for the rest. I don't see this being a problem for too long but for the time being it is a pain in the neck.

What next?

Now that I have some semblance of a build going I want to try compiling my ES6 files into CommonJS format and then pass the output through Browserify and see how that workflow goes.