This PR will allow us to write ES6 code for our browser applications, 'transpile' (transform) it into ES5 for backwards-compatibility, and use new language features in even the likes of IE9.
You can see this working for yourself, end to end, on (redacted URL). We're using ES6 arrow functions right on the homepage!
If you open the sources panel and navigate to all.min.js
, though, you can see that this gets served to the browser as good ol' ES5:
Not only that - but the sourcemaps still point to the original source code:
And because the code is plain old ES5, it runs just fine in Internet Explorer 9:
Transpilation is a portmenteau of translate and compile. When we compile a programming language we turn its human-readable terms into assembly-level instructions for a processor to execute. By the same vein, when we transpile code we translate it into plain JavaScript for a browser to run natively.
A simple example is the new const
keyword, which signifies an immutable. Once in the browser, we can just use a plain old var
:
Similarly simple are arrow functions, which can (often) be implemented with a straightforward text replacement:
Some transformations are a little more complicated, though...
To make life easy, we need a third party tool. This is where Babel comes in.
Babel is a generic JavaScript transpiler. It can handle many JS-like input-languages - TypeScript and React JSX scripts among them - but ES6 transformation is its forté. By passing code through Babel and appending a special polyfill file, we can use any features in the ES6 in IE9 upwards.
Yes, slightly. The Babel polyfill is about 32.7kb minified and gzipped. As implemented right now, this makes our bundles generally approximately 10% larger:
File | Before size (gz, raw) | After size | Size change |
---|---|---|---|
app | 368,645 (1,412,628) | 401,423 (1,506,370) | 32,728 (+8.8%) |
article-preview | 295,071 (1,018,121) | 327,937 (1,115,129) | 32,866 (+11.13%) |
embedded | 297,990 (1,030,705) | 330,824 (1,127,583) | 32,834 (+11.0%) |
login | 300,484 (1,047,435) | 333,328 (1,144,196) | 32,844 (+10.9%) |
unsupported | 257,759 (863,333) | 290,722 (961,488) | 32,963 (+12.8%) |
Nevertheless - I believe this is an acceptable penalty. 30kb is the size of a medium-scale image. If filesize matters that much, there are likely better ways of saving space than eschewing ES6 - a tree-shaking JS bundler, for one.
Our original JavaScript pipeline was simple: compile the Angular templates, concatenate all the scripts and minify the lot with uglify.
Initially, I tried to use grunt-babel
after the concat stage to transform the whole bundle in one fell swoop. This worked, but was tremendously slow - building app.min.js
took over two minutes! More importantly, it broke sourcemaps.
So instead, I wrote a self-contained compileJS
task that wires together concatenation, transpilation and minification using 'files' that live almost entirely in-memory, caching results and making the most use I can of concurrency. By avoiding disc I/O as far as possible, compileJS
can generate a bundle like app.min.js
in a little over fifteen seconds:
By default, the grunt start
task tells the application to serve the original, unminified files. I haven't changed that. This means that you'll be running and debugging ES6 code directly in Chrome or Firefox.
One disadvantage of this, however, is that you can't directly develop in old browsers like IE9. You'll have to use the TTW_UNCOMPILED=false
environment flag to run the minified, transpiled bundle. This will make debugging with browser tools a bit harder, because if the browser can't support sourcemaps you have to debug the minified code instead.
Let me know if you need do this often and we can think up some better development flow.
We run our unit tests on PhantomJS, a headless browser. Unfortunately this doesn't yet support ES6, so we transpile code for this instance too. This does impact coverage reports slightly - see below.
You can use everything in the ES6 standard, including the new class, generator and function syntaxes.
Our karma-coverage
plugin doesn't understand ES6, so it has to be run against the transpiled ES5. This doesn't affect coverage percentages, but it does mean that when you look at the reports in detail, you'll be eyeballing ES5 rather than ES6:
This means that as we use more ES6 features in our code, these in-detail reports will become less useful. There are solutions to this - like the Istanbul plugin for Babel - but I'd like to understand exactly how much people use these reports before investing time fixing this.
This is unavoidable, really - Grunt's paradigm is to chain plugins that do nothing but file transformations. You can't 'communicate' between tasks except by writing temp files, and tasks run purely in serial, so implementing this over multiple tasks would be very slow indeed.
To help make the compileJS tasks a little simpler to debug, however, I've added an error handler that prints out exceptions and their stacktraces. This should help anyone who intends to do further work with this file:
As mentioned above, to develop on non-ES6 browsers you'll have to use the TTW_UNCOMPILED=false
environment flag, and run the minified, transpiled code locally. This will be a lot less easy to debug. Again - let me know if you think this impacts you, and maybe we can think through a solution.
Run grunt build
then TTW_UNCOMPILED=false grunt start
.
The TTW_UNCOMPILED=false
property tells the server to use the minified bundle rather than the individual raw files.