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 IE8 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.
Here is a starting structure.
├── build
├── modules
│ ├── utils
│ │ ├── __tests__
│ │ │ └── sleep-test.js
│ │ └── sleep.js
│ └── index.js
├── .eslintrc
├── .babelrc
├── package.json
└── index.js
-
The
build
folder contains the built assets for Node. This is used in production (or for published modules) and typically ignored in version control. -
The
modules
folder contains the application components themselves. -
The
.eslintrc
file is lint configuration. -
The
.babelrc
file is Babel configuration. -
The
package.json
contains scripts for running and building the project -
The
index.js
file sets up Babel hooks for development.
Go ahead and create these files and directories. To create a package.json
file quickly, just run npm init -y
.
Installing dependencies
Now let’s get our dependencies installed and saved. Run the following to install our development dependencies:
npm install babel babel-eslint eslint eslint-config-standard babel-tape-runner blue-tape -D
-
babel
is the main core babel project that allows us to set up our development environment -
babel-eslint
is a parser foreslint
that teaches the linter about experimental features that aren’t in ES6. -
eslint
is a linting tool andeslint-config-standard
is a set of configurations foreslint
that we’ll write our code against which follows the JS Standard style. -
babel-tape-runner
hooks inbabel
when running tape andblue-tape
is an extension totape
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 theasync/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 likeNumber.isNaN
andObject.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
:
import { hostname } from 'os'
import sleep from './utils/sleep'
async function runApp () {
console.log('time for bed', hostname())
await sleep(200)
console.log('😴')
await sleep(1000)
console.log('💤')
}
runApp()
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 (currently at stage 1 in the standardization process).
Let’s write our sleep module next. Add the following code to modules/utils/sleep.js
:
export default function sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
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
let’s add the following:
import test from 'blue-tape'
import sleep from '../sleep'
test('sleep', async function (t) {
let start = Date.now()
await sleep(20)
let end = Date.now()
t.ok(end - start >= 20, 'takes about 20 milliseconds')
})
Notice how we are using async/await
and ES6 syntax in our test suite just like in our application code. Let’s add the following script to our package.json
file in order to run this:
scripts: {
"test": "babel-tape-runner \"modules/**/__tests__/*-test.js\""
}
Now we can run:
npm test
And we will get the following output:
TAP version 13
# sleep
ok 1 takes about 20 seconds
1..1
# tests 1
# pass 1
# ok
Groovy. We can use Babel for tests as well as application code.
Linting Babel
Let’s turn to our .eslintrc
file next and add the following:
{
"extends": "standard",
"parser": "babel-eslint",
"env": {
"node": true,
"es6": true
},
"emcaFeatures": {
"modules": true
}
}
-
The
extends
line hooks up JS Standard rule definitions. -
The
parser
line tellseslint
to use thebabel-eslint
for parsing instead of the default parser allowing us to parse experimental JavaScript features. -
The
env
lines leteslint
know that we are using Node and ES6 features. -
By default the
es6
environment enables all ES6 features except modules, so we enable that as well in theecmaFeatures
block.
Let’s add a script to our package.json file for linting.
scripts: {
"test": "babel-tape-runner \"modules/**/__tests__/*-test.js\"",
"lint": "eslint modules index.js"
}
And we then can run:
npm run lint
Which will give us no output currently as there aren’t any linting errors.
Running Babel in production
Our index.js
is handy for running Babel in development as its all in-memory and we don’t need a manual compilation step. However, that isn’t ideal for production for a couple reasons:
- Start up times are slower as the code base needs to be compiled in-memory first. Time increases with larger code bases.
- Second is that “in-memory” bit. We will have extra memory overhead if we do it this way; it will vary depending on the project size and dependencies.
We can add a build step for production that can be run before publishing to npm or as part of continuous integration. Let’s add a couple more scripts to our package.json
file:
scripts: {
...
"clean": "rm -rf build || true",
"build": "npm run clean && cp -rf modules build && babel --optional runtime -d build ./modules"
}
-
The
clean
script just cleans out our previous build. -
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 thebabel
command to build all the JavaScript files inmodules
and puts the output in thebuild
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.
Let’s try a build by running:
npm run build
You should get the following output refering the compiled files:
modules/index.js -> build/index.js
modules/utils/__tests__/sleep-test.js -> build/utils/__tests__/sleep-test.js
modules/utils/sleep.js -> build/utils/sleep.js
Now we can run our precompiled version with the following command:
node build
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.