Andrew.

Making the most of JavaScript's future today with Babel

From CoffeeScript to ClojureScript to PureScript to CobolScript to DogeScript (woof woof!), JavaScript is the write-once-run-anywhere target for many forms and styles of programming. Yet, its biggest compile-to-language today actually isn’t any of these adaptations; its JavaScript itself.

Why? Universality!

If you are a client-side developer, it’s likely you’ve already enhanced older platforms with newer features. Take ES5 for example. Supporting Internet Explorer 8 was a pain, but es5-shim made the JavaScript part a lot smoother.

One of the things that attracted me to Node (circa 2009) was the ability to break away from browser support humdrum. It was blissful for a while, but Node is not exempt from legacy JavaScript compatibility issues anymore. Say you are a library author who is using generators in io.js but wants to support Node 0.10. Or you are sharing code between the client and server (like in a React application) and want to use classes or destructuring. The fact is, developers want to utilize new features without having to be concerned about legacy support. Legacy support isn’t a passion of mine, and I bet it isn’t one of yours.

JavaScript should be a write-once-run-everywhere language!

The JavaScript language is developing faster (ES7/2016 anyone?) than it ever has. Libraries are taking advantage of the new features, even though Node and browser’s haven’t yet settled in to the new standards (take React’s adoption of ES6 classes, for example). I expect the trend to continue.

The good news is that it is easy to start taking advantage of new language features now with Babel and have them work across legacy and current platforms. Babel has built in a source-map support for browsers and proper stack traces for Node, so it gets out of the way for you to focus on the ES6 code.

Babel isn’t the only source-to-source compiler for ES. Traceur from Google is another example. For clarity, we will just focus on Babel here.

Practically speaking: Developing with ES6 and beyond

The remainder of this article will be a practical launching point if you haven’t used Babel before and perhaps fill in some gaps if you have. We will build a simple app to provide context. The final product is located on GitHub.

babel is the main core babel project that allows us to set up our development environment

babel-eslint is a parser for eslint that teaches the linter about experimental features that aren’t in ES6.

eslint is a linting tool and eslint-config-standard is a set of configurations for eslint that we’ll write our code against which follows the JS Standard style.

babel-tape-runner hooks in babel when running tape and blue-tape is an extension to tape that adds promise support (which will come in handy in a bit).

Now that we have necessary dependencies to start our development, let’s install one more that will be used for production:

npm install babel-runtime -S

The babel-runtime package allows us to require only the features we need when distributing our application without polluting the global scope.

Configuring Babel

Let’s look at the .babelrc file next. Having a .babelrc file allows you to configure Babel in one spot in your project and it will work regardless how its run. Create a .babelrc file with the following content:

{
"stage": 0,
"loose": "all"
}

There are a number of options for configuration, but we will focus on two:

The stage option defines what minimum proposal stage you want to support. By default, Babel provides the functionality found in the ES6 standard. However, Babel also includes support for language proposals for the next standard. This is pretty cool because it allows you to test drive features and give feedback to implementers as it goes through standardization. Specification proposals are subject to change in breaking ways or completely fizzle out all together. The higher the stage, the further along the specification is in the standardization process. You can view all of the proposals supported on the Experimental page. We will use the async/await proposal in our application.

The loose option will generate cleaner and faster output as it won’t check ECMA specification fringe cases that are likely not to appear in your code. However, make sure you are aware of the edge cases before you use loose mode. This is handy for production performance as well.

Building our application

Now that we have Babel configured, let’s write some code! First, set up the root index.js file for development purposes with the following code:

require('babel/register')
require('./modules')

The require('babel/register') line registers Babel, pulls in our .babelrc configuration and also includes a polyfill for ES6 extensions for native objects like Number.isNaN and Object.assign.

Now that Babel is registered, any file we require after that will be transpiled on the fly. So, in our case, we require our application with require('./modules').

Next, let’s create an entirely lame app that makes use of ES6 and the experimental async/await proposal. Put the following code in modules/index.js:

I told you it was lame. However, we are making use of the new import syntax to pull in the hostname function from the os module and include a sleep module (which we’ll write in a bit). We are also using the async/await proposal to write clean asynchronous code.

This little helper function turns setTimeout into a promise returning function that resolves when a timeout is completed. Since the await syntax we used above awaits promises, this allows us to write a succinct delay code.

Let’s see if our application works! Run the following from the project root to test:

node index.js

You’re output should be similar to this:

time for bed wavded.local
😴
💤

Exciting right?! Don’t answer that.

Now that we have an application to play with, let’s look at a few more tools you likely use in day-to-day development and how they translate when using Babel.

Testing Babel code

Let’s add a test for our sleep utility we developed in the last section. Inside modules/utils/tests/sleep-test.js, add the following:

The build script compiles the app. First, it cleans. Then, it copies any assets (including any .json files) to the build directory so they can be referenced properly. Finally, it runs the babel command to build all the JavaScript files in modules and puts the output in the build directory.

We also include an additional configuration option for Babel called runtime. The runtime optional won’t pollute the global scope with language extensions like the polyfill that is used when called require('babel/register') above. This keeps your packages playing nice with others.

And we should get the same output as we did when we ran in development.

Now that we’ve done a build, poke around at the files in the build directory and see how they compare with the originals.

Source maps in production

Although loose mode (which we enabled in the Configuring Babel section above) will generate cleaner and faster output, you may still want to use source maps in production. This allows you to get at the original line numbers in stack traces. To do this, change your babel command to:

babel --source-maps inline --optional runtime -d build ./modules

You will also need the source-map-support package in npm in order for proper stack traces to appear in your error messages.

npm install source-map-support -S

To enable, add the following at the top of build/index.js

require('source-map-support')

Wrapping up

Babel allows you to write ES6 and beyond today and have it work across different versions of Node and also work across different browsers on the client side (see http://www.2ality.com/2015/04/webpack-es6.html for an example). The most exciting thing for me that has been a joy to work with is universal JavaScript applications that share most of their code and then I get to write it in ES6.

PS: Syntax and Babel

Let’s quickly talk about your text editor before we go shall we? Lots of the new constructs won’t be highlighted properly when you start using Babel. Thankfully, the community has rocked this one and you should definitely switch if you haven’t as a lot of these have good support for things like JSX and Flow.