Transpiling EcmaScript6 code to ES5 using Babel
If you watch this blog, you have probably already heard of ng-admin, the REST-based admin panel powered by Angular.js. Developed in good old EcmaScript5 JavaScript, it was time to make a step forward, introducing ES6 and its wide set of new features.
We, at marmelab, spent the last two weeks updating the ng-admin configuration classes to ES6. We voluntarily restricted the perimeter to do the spadework for this new JavaScript version. And, as admin configuration is independent of Angular.js, it was the the perfect target for our experiment.
As always in web development, we can't use a new technology, no matter how exciting, directly out of the box. To ensure browser compatibility, we need to transpile it into more common JavaScript. So, even if we write our code in ES6, we have to include a phase of transpiling, to convert it into pure ES5 code, understandable by all browsers.
Babel vs Traceur
There are two main ES6 to ES5 transpilers: Babel (formerly known as 6to5) and Traceur. We had to take one of them: Babel.
The main reason we chose Babel is it doesn't need any runtime extra script to run. Everything is done server-side. You just have to execute a compilation task once, and then deploy the compiled sources. At the opposite, Traceur needs to embed such a script, bringing an extra overhead. Yet, it should be nuanced as we need a polyfill even for Babel (core-js) for some missing browser methods, like Array.from
.
Another issue is that Traceur is not compliant with React.js. This is not a big deal in this case, but as we also use the Facebook framework at marmelab, let's accumulate knowledge on a single technology. He who can do more can do less.
And, icing on the cake, Babel has an online REPL if you want to quickly give it a try.
How to use Babel to transpile your code?
Turning your ES6 code to ES5 JavaScript
First, you need to install Babel:
npm install babel --save-dev
Then, let's consider the following simple class:
class View {
constructor(name) {
this._name = name;
}
name(name) {
if (!arguments.length) return this._name;
this._name = name;
return this;
}
}
export default View;
Compiling it into pure ES5 JavaScript is as simple as the following command:
babel View.js
By default it will output the file in standard output. You can of course redirect it to a file using the standard >
operator.
babel View.js > build/View.js
Babel modules
Previous example had no dependencies. Yet, what would happen if we had to import another class? Let's experiment it right now:
import View from "./View";
class ListView extends View {
constructor(name) {
super(name);
this._type = "ListView";
}
}
Compile these two classes using the commands:
babel View.js > build/View.js
babel ListView.js > build/ListView.js
If you open the build/ListView.js
file, you will see some calls to require
function:
var View = _interopRequire(require("./View"));
On ng-admin, we use requirejs to load our dependencies. So, I first thought I just had to embed requirejs
and that everything would work out of the box. Unfortunately, after several hours of debug, I learnt it wasn't the case. Indeed, this require
call is not the same than the require
from requirejs
. While the first is related to CommonJS, requirejs
uses the AMD standard.
AMD? CommonJS? UMD?
I have always been confused about all these standards. I took profit of this project to clarify them. After digging the topic, it is simple.
All of these standards aimed to simplify development of modular JavaScript. Asynchronous Module Definition (AMD) is the requirejs
module loader. It targets browsers only, and is supposed to simplify front-end development (even if I hardly found any more difficult to configure library).
AMD modules are defined through the define
function, such as:
define(['dependencyA', 'dependencyB', function(dependencyA, dependencyB) {
return {
doSomething: dependencyA.foo() + dependencyB.foo();
}
});
CommonJS is based on the Node.js module definition. This is not compatible with requirejs
, but has been brought to front developers thanks to libraries such as browserify
or webpack
. Our previously AMD module would looks like the following in CommonJS:
var dependencyA = require("dependencyA");
var dependencyB = require("dependencyB");
module.exports = {
doSomething: dependencyA.foo() + dependencyB.foo(),
};
Finally, as neither AMD nor CommonJS succeeded in standing out from the crowd, another attempt of standardization emerged: Universal Module Definition (UMD). It has been built to be compatible with both of them.
(function(root, factory) {
if (typeof define === "function" && define.amd) {
// AMD
define(["dependencyA", "dependencyB"], factory);
} else if (typeof exports === "object") {
// Node, CommonJS-like
module.exports = factory(require("dependencyA"), require("dependencyB"));
} else {
// Browser globals (root is window)
root.returnExports = factory(root.dependencyA, root.dependencyB);
}
})(this, function(dependencyA, dependencyB) {
doSomething: dependencyA.foo() + dependencyB.foo();
});
Really ugly and verbose, isn't it?
Using Babel with requirejs
So, we need AMD to be able to use requirejs
with Babel. Yet, Babel exports by default to the CommonJS format. Fortunately, we can specify it a --modules
option:
babel --modules amd ListView.js
Checking generated output shows that we are now compliant with requirejs
:
define(["exports", "module", "./View"], function(exports, module, _View) {
// ...
});
Testing with Babel
ng-admin uses two testing framework: Karma and Mocha. Here is how to configure these frameworks.
Karma
For Karma, we just have to install an extra package:
npm install --save-dev karma-babel-preprocessor
Then, update your karma.conf.js
configuration file:
config.set({
// ...
plugins: [/* ... */ "karma-babel-preprocessor"],
preprocessors: {
"ng-admin/es6/lib/**/*.js": "babel",
},
babelPreprocessor: {
options: {
modules: "amd",
},
},
});
We add the freshly installed plug-in, specifying it to transpile to AMD module. We also have to specify which files should be transpiled, via the preprocessors
option.
Mocha
For Mocha, install process is similar and requires the mocha-traceur
plugin:
npm install --save-dev mocha-traceur grunt-mocha-test
We also installed the grunt-mocha-test
as we are using Grunt. Then, a little bit of configuration:
// Gruntfile.js
grunt.initConfig({
mochaTest: {
test: {
options: {
require: "mocha-traceur",
},
src: ["src/javascripts/ng-admin/es6/tests/**/*.js"],
},
},
});
grunt.loadNpmTasks("grunt-mocha-test"); // enable "grunt mochaTest" command
If you prefer using mocha directly, just specify the compilers
option:
mocha --compilers mocha-traceur --recursive src/javascripts/ng-admin/es6/tests/
Now you got the big picture on how we succeeded to rewrite ng-admin configuration classes using EcmaScript6. There is still a lot of work to do on ng-admin for a full migration. Don't hesitate to give a helping hand!