Easy ES2015 compilation with rollup.js, babel.js and gulp.js

As this year is over tomorrow, it's my last chance to sneak in some 2015 stuff. In this post, I want to show you a simple setup I use to compile my JS written in ES2015 for this blog.


Update 🚨

I've published a new version of this article featuring the latest updates of these tools. I recommend reading it since it's easier and uses less dependencies.
Go check it out! :)


ES2015 modules 🚀

One of the features I'm most excited about in ES2015 are modules. While node.js always had its own module and bundle system with CommonJS modules, the frontend development tried to keep up with browserify oder require.js for example.

But now there's an official standard for JS modules so you can use the same system, no matter if frontend or backend development, which is pretty cool. Besides, ES2015 modules can be more efficient, because of an technique called 'tree-shaking':

Normally if you require a module, you import the whole thing. ES2015 lets you just import the bits you need, without mucking around with custom builds. It's a revolution in how we use libraries in JavaScript, and it's happening right now.

rollup.js

If you want to know more about ES2015 modules, I can recommend this article by Dr. Axel Rauschmayer.

Software stack

Babel.js

Because the JS is used in the frontend, we still need to compile the ES2015 code to ES5 as long as the evergreen browsers do not support your favorite new features natively.
For this step babel.js is a good choice, because of its simple setup, wide support and good documentation.

With Babel 6, you need to enable plugins first to tell babel how to transform your code. Fortunately, Babel shares some common configs and 'ES2015' is one of them. To install this preset, simply type in $ npm install babel-preset-es2015 in your shell.

The next step is to tell Babel to actually use this preset. One way to do this, is to configure your presets in your .babelrc file like this:

{
  "presets": ["es2015"]
}

Module bundling with rollup.js

As today's browsers do not support modules natively and to minimize the amount of HTTP requests, I used rollup.js to bundle the various modules to a single file.

Rollup is an ES2015 module bundler, which is easy to configure and it makes use of tree-shaking, so it's quite suitable for this project.
You can use it either programmatically via an API or via CLI.
A possible API use case could look like this:

import { rollup } from 'rollup';
import fs from 'fs';

rollup.rollup({
  // specify 'starting point'
  entry: 'main.js'
}).then(function (bundle) {
  // generate the bundle, various formats are availabe
  var result = bundle.generate();
  fs.writeFileSync('bundle.js', result.code);
});

First, you specify the main entry file (main.js) of your project, which dependencies rollup.js will try to import. The rollup()-method returns a promise and if it resolves, you can generate the code and write it to a file. In this case, I use the filesystem API from node.js, but rollup.js provides an own method to write the bundle. For example, it makes it possible to generate sourcemaps right away which is, in my opinion, a crucial feature if you work with transpiled code.

...
rollup.rollup({
  // specify 'starting point'
  entry: 'main.js'
}).then(function (bundle) {
  // generate the bundle with sourcemaps and write it
  bundle.write({
    dest: 'bundle.js',
    sourceMap: true
  });
});

Just have a look at the official API.

Caveat: import path resolution

One caveat you might encounter with the given setting is the path you set in your import expressions. For instance,
if your main.js file is located in the directory app/js and your helper.js is on the same level, the following code will not work as you might expect:

import { superHelperFunctions } from 'helpers';

Instead of importing the helpers file, you'll get the message Treating 'helpers' as external dependency because the resolution root is . and not app/js. This issue is listed and discussed on github.


A solution is to write the import path as a relative path ... from './helpers;, or to use a workaround I took from the issue thread above:

rollup({
  sourceMap: true,
  plugins: [{
    resolveId: function (code, id) {
      if (id) {
        return 'app/js' + code + '.js';
      }
    }
  }]
})

The workaround makes use of 'rollup's extensibility via plugins. You can install already existing plugins for rollup or write your own to customize the bundling process further.
In this minimal plugin, you can define your custom way to resolve the import paths. In this case, we simply prepend the path app/js. For a simple setup like this such a workaround can be good enough ;)


Update February '16:

Luckily there's a rollup-plugin available for this situation now, which you can find it on github. The installation and setup of this little helper is very simple, so you should give it a shot.

npm install rollup-plugin-includepaths --save-dev

After npm is done, you can put the configuration of the plugin in rollup's plugin section:

const includePathOptions = {
    paths: ['app/js']
};
rollup({
  sourceMap: true,
  plugins: [
    rollupIncludePaths(includePathOptions)
  ]
})

This config instructs rollup to include specific paths when looking for your modules you want to import, so the import expression from above will find the helpers.js file. (\o/)

Gulp task

Since I use gulp as a task runner, it reasonable to look out for gulp-plugins for babel.js and rollup.js to structure the build process in a lean and elegant way.

Luckily, both of them are already available as plugins (gulp-babel, gulp-rollup), so after installing them ($ npm i gulp-babel gulp-rollup) a possible task to compile ES2015 code could look like this:


import gulp from 'gulp';
import sourcemaps from 'gulp-sourcemaps';
import rollup from 'gulp-rollup';
import rollupIncludePaths from 'rollup-plugin-includepaths';
import babel from 'gulp-babel';
import rename from 'gulp-rename';
import util from 'gulp-util';

const includePathOptions = {
    paths: ['app/js']
};

gulp.task('compileES2015', () => {
  return gulp.src('main.js')
        .pipe(rollup({
          sourceMap: true,
          plugins: [
            rollupIncludePaths(includePathOptions)
          ]
        }))
        .pipe(babel())
        .on('error', util.log)
        .pipe(rename('bundle.js'))
        .pipe(sourcemaps.write('.'))
        .pipe(gulp.dest('dist/js'));
});

Things to note

Rollup.js already provides a plugin for babel, which I have intentionally left out to make things simpler and more exchangeable. Though, for projects with a greater codebase and more complex setup this separation of babel.js und rollup.js might lead to problems concerning performance and sourcemap generation, as described in the plugin's readme.

Comments on this post


comments powered by Disqus