The Move from CoffeeScript to ES6
CoffeeScript, no more
The biggest source of pushback as we onboard new developers has been that our front-end code was written in CoffeeScript. There was a distinct lack of enthusiasm to read up and learn CoffeeScript, since the perception is that it’s a dead-end technology.
The Challenge: converting 20,000 lines of CoffeeScript to ES6
When we finally decided to port our CoffeeScript code to ES6, the actual task of going through the translation seemed like a herculean effort. We tried converting a script around a thousand lines long and that took about a day. That sample exploration included getting up to speed with ES6 and deciding what our Backbone code would look like. If we are to say that pace is about right, that means it would take about one man-month to convert the entire codebase. Beyond the time and pain that effort would take, we were also concerned about the quality of code produced if someone were to convert it all by hand.
We then thought that there must be a script that would translate CoffeeScript to ES6 code. While two projects claim they can do the deed, neither actually worked for us. So, we wrote our own.
Our CoffeeScript to ES6 transpiler
Luis Artola, one of our developers, took about a week converting the CoffeeScript compiler to generate ES6 code from CoffeeScript. You can find the code here: Unbrew.
We’re planning a followup post where Luis will go into the details on the different issues he came across when editing the transpiler and the overall effort involved.
How we came to our decision
CoffeeScript: the good parts
When we first introduced CoffeeScript into the Evite codebase back in 2012, we loved it. To that end, the team that first was involved with using CoffeeScript still loves CoffeeScript. Also, we know how much CoffeeScript has impacted the front-end world and ES6 specifically. At the time, it was the best, and now, it’s still great. We just have another option that has a lot going for it.
CoffeeScript: the bad parts
Implicit returns, moving on. (discussion here)
ES6, the sales pitch
A lot of the details that make CoffeeScript compelling worked their way into the ES6 spec. Also, a couple things that we got when including Underscore also come for free in ES6. Examples of things we like:
Module support
This is one item that CoffeeScript did not have. You could use whatever module system you wanted (CommonJS/AMD/…) and tack it onto CoffeeScript, but it wasn’t a built-in language feature. In ES6, it’s actually part of the language spec. This is one of the biggest selling points for us. When developing in modern languages, we’ve come accustomed to breaking up our code into smaller, more digestable chunks. Babel has a bunch of different plugins available where people have worked on support for all the diffferent mainstream module systems. This is great since you can write ES6-style modules and then convert to whichever module system fits the problem you’re trying to solve that day.
Classes
Once again OOP is one of things we’re used to playing with in all the other languages we use, and part of the reason we used CoffeeScript is just so we could have objects to play with. So adding classes to JavaScript itself just makes life that much sweeter.
Arrow funtions
In CoffeeScript we have ‘->’ and ‘=>’ as the syntax for functions. For normal functions you use the ‘->’ syntax. And then there’s the ‘=>’ syntax which is lovingly referred to as the “fat arrow.” The fat arrow’s great because it keeps the scope of the surrounding code when the function is called. The ‘=>’ in ES6 (called an “arrow function”) follows in the footsteps of its CoffeeScript brethren and also keeps scope around.
Promises
We had been using rsvp.js to polyfill promises into our codebase. When we worked in ES6, we were able to tear out that library and just use the built-in support ES6 provides.
String interpolation
Assembling strings is really important for the web. Being able to do that in a nice, clear way is helpful to write cleaner code:
`Here's the value for 'blah': ${blah}`
‘let’ and ‘const’
In JavaScript, when you declare a variable using ‘var’ the scope is function level scope. Where this isn’t ideal is where you want to define a variable within a condition statement or within a loop and want the scope of the loop…
function sampleFunction() { // function scope. for (let i=0; i<10; ++) { let j = 0; } }
Normally the scope for both ‘i’ and ‘j’ would be at the function scope, but ‘let’ keeps the scope at the loop level.
And then let’s not forget about the ‘const’ keyword. Seems powerful to have an immutable variable type.
Default function parameters
Default function parameters allow parameters to be initialized with default values if no value or undefined is passed. In JavaScript, parameters of functions default to undefined. However, in many situations it’ useful to set a different default value.
Generators
This is another one of those language features I’ve come to expect from modern languages. In Python, this is a feature we use where possible. In browsers, memory concerns can be a real issue for some people. Limiting the amount of data we have to keep in memory to get something done can be a really powerful thing and let more functionality work for more people.
Browser compatability
Browsers are aggressively adding support for ES6. It’s possible that we won’t have to transpile down to ES5 soon. (discussion here)
ES6: the not so pretty parts
Classes are not like CoffeeScript’s
This was the single biggest drawback moving to ES6 for us. The ES6 spec does not have support for class-level properties, which is something that is core to Backbone’s conciseness. (discussion here)
In Backbone you would do:
class BaseMessageView extends Backbone.View tagName: 'li' events: 'click a.view-all-comments' : 'view_all' 'click a.reply' : 'show_reply_form' 'click .meta .remove' : 'on_remove' 'submit div.reply-form form' : 'on_submit_reply' ...
In ES6, we went with:
class BaseMessageView extends Backbone.View { get tagName() { return 'li'; }; get events() { return { 'click a.view-all-comments': 'view_all_replies', 'click .meta .remove': 'on_remove', 'submit div.reply-form form': 'on_submit_reply' }; }; ...
Also, in CoffeeScript you can have class-level functions with fat arrows, but in ES6 you can’t have arrow functions at the class-level. This means that wherever we called said functions, we had to manipulate the code to pass in the appropriate scope.
Verbosity
Based on our numbers, code written in ES6 will be about 2.5x the size of the same written in CoffeeScript.
ES6 in production: learnings
As mentioned earlier, we started with 20,000 lines of CoffeeScript code. When translated that resulted in approximately 50,000 lines of ES6 code.
During testing our QA team caught a semi-serious gotcha with Babel, the ES5 it renders does not work in most IE browsers. We then found this list of known issues. So we had to polyfill a couple things for IE (and a couple other) browsers. This is what we had to our base template files.
<script>
if(window['Symbol'] === undefined ||
window['Promise'] === undefined ||
typeof Object.assign !== 'function') {
document.write('<scr'+'ipt src="//cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.7.4/polyfill.min.js"></sc'+'ript>');
}
</script>
How to transpile ES6 code
First install these Node packages:
$ npm install babel-cli
$ npm install babel-preset-es2015
Then run babel like so:
$ ./node_modules/.bin/babel <name of script> --presets es2015
Module support in the browser
We recently also turned on module support. It was fairly simple. All you have to do is turn on the CommonJS Babel plugin. And then run the code that generates through Browserify.
Install the following Node packages:
$ npm install babel-plugin-transform-es2015-modules-commonjs
$ npm install browserify
Our .babelrc file looks like this:
{
"presets": ["es2015"],
"plugins": [
"transform-proto-to-assign",
[
"transform-es2015-classes", {
"loose": true
},
"transform-es2015-modules-commonjs", {
"allowTopLevelThis": false,
"strict": false,
"loose": false
}
]
]
}
Then run the following commands
$ babel <path to ES6 file>.es6 --out-file <path to transpiled JS file>.js
$ browserify <path to transpiled babel generated JS file>.js --outfile <path to Browserify JS file>.js
Conclusion
Converting all the code over was a serious undertaking. With that out of the way, playing with ES6 has been great. There are a lot of nice tools and libraries we can now play with. CoffeeScript – for the most part – is a more elegant, more concise language, but ES6 is a bit more explicit. Having played in both lands, explicit seems to be more important when working on a project at our scale. There was nuance to what CoffeeScript was doing that became more obviuos after we converted to ES6.
Resources:
CoffeeScript at Evite (b. 9/27/2012 d. 5/4/2016)