Clearing up the Babel 6 Ecosystem

With the somewhat recent release of Babel 6 comes a fairly dramatic shift from the older versions in that every component has been split out and thinned down. This simplification is fantastic from a component responsibilities standpoint but has left a number of users confused. Part of the confusion is due to the vast amount of version-unqualified educational resources—including 1st party documentation—that may have been valid for Babel 5 but no longer hold true.

Before I begin, I’ll start by saying the entire suite is extremely legible. You can quite plainly see what each component is responsible for by reviewing the code.

Here’s a quick and dirty run down of each of the main components (as of today with v6.3.23), what they’re responsible for, and how to use them in concert with the rest of the babel tooling. If nothing else, see the section on babel-polyfill versus babel-runtime as that seems to be the biggest source of confusion.


npm install babel

The babel module itself is no longer in use, having been broken out into several other packages. This is not the module you’re looking for.

npm install babel-core

babel-core is, perhaps unsurprisingly, the core compiler for babel. It’s what facilitates running your code through a series of transforms you define. It does not ship with any transforms, by default, which is why you must install and register your own.

It is worth pointing out that babel-core is exclusively concerned with code transforms. Not everything you want will be facilitated exclusively with a transform. for example, if you review the list of 1st party transforms, you will see there is no mention of promises anywhere. If promises are not native to your javascript runtime, you will need to perform an additional step to make them work (via a polyfill or ponyfill—more on this below).

The distinction between what must be facilitated by a transform versus what can be polyfilled—and therefore, what requires a specific babel transform versus one of the polyfilling solutions mentioned below—is whether or not you can reimplement the feature today, in ES5. For example, Array.from can be written in ES5 as demonstrated on MDN but there is nothing I can write in ES5 to add arrow function syntax to JavaScript. Therefore, there is a transform for arrow functions but none for Array.from—it will have to be provided by a separate polyfill like babel-polyfill, or babel-runtime (with the runtime transform, as documented below).

Babel 6 introduced the concept of a preset which is simply shorthand for a set of transforms. If you’re looking for the general case of supporting es2015 features, you’ll want to install and register the babel-preset-es2015 module.

Presets can be used (after installing them) by registering them in your .babelrc file like so:

{
"presets": ["es2015"]
}

npm install babel-cli

If you want to use any of the command line tools that previous shipped with babel, you’ll want to install the babel-cli module.

babel-cli ships with four executables: babel-doctor, babel, babel-node, and babel-external-helpers.

babel-doctor runs a few simple sanity checks against your project. It’s a bit of an ephemeral application as it checks a couple of simply criteria against your application. See the blog post about it for more info.

babel is the command line interface for transforming your code, taking files or folders as input, running your registered transforms against them (using babel-core), and spitting them out, either to the console or to disk.

babel-node is a convenience binary and is not intended for production use. Similar to the node binary, it can either be used to run a script if supplied on the command line, or be used as a REPL if no arguments are supplied. It automatically requires babel-polyfill to polyfill ES2015 features and methods and sets up babel-register to compile (i.e., transform) other modules as they’re required. See below for info on both.

npm install babel-register

babel-register is the require hook for babel. It replaces the mechanism for how node loads *.js files when required with it’s own mechanism. Put simply, if the module isn’t part of node core, and isn’t in node_modules/, babel will attempt to compile it at require time. Otherwise, it falls back to node for requiring the module. In theory, this will result in slightly increased memory usage as your files will be cached twice—once in node’s module cache and once in babel’s—but in practice this is negligible. Additionally, the compiled modules can be cached to disk on process termination which means that, in some cases, you won’t incur the compilation expense the second time.

npm install babel-polyfill / npm install babel-runtime

These are arguably the most often misunderstood modules in the Babel ecosystem.

In Babel 6, nothing (with the exeption of the babel-node binary) will automatically shim ES2015 features for you. You’ll have to polyfill them yourself which means you’ll nearly always need either babel-polyfill or babel-runtime.

The babel-polyfill and babel-runtime modules are used to serve the same function in two different ways. Both modules ultimately serve to emulate an ES6 environment. As mentioned above in the section on babel-core, this is the second half of the equation—the features that need be polyfilled and not simply transformed (like global keywords, prototype methods, etc).

Both babel-polyfill and babel-runtime emulate an ES2015 environment with two things: a slew of polyfills as provided by core-js, as well as a complete generator runtime (which gives you generator/yield support).

babel-polyfill accomplishes this task by assigning methods on the global or on native type prototypes which means that once required, as far as the javascript runtime you’re using is concerned, ES2015 methods and object simply exist. If, for example, you were to required babel-polyfill in a script run under node v0.10—a runtime which does not natively support the Promise API—your script would then have access to the Promise object. As far as you, the author, are concerned, you’re suddenly using an environment that supports the Promise object.

babel-runtime does something very similar, but in a way that does not pollute native object prototypes or the global namespace. Instead, babel-runtime is a module that you can list as a dependency of your application like any other module, which ponyfills ES2015 methods. In other words and continuing the example from above, while you may not have the Promise object available to you, you now have the same functionality available to you from require(‘babel-runtime/core-js/promise’). By itself, this is useful but inconvenient. Fortunately, babel-runtime is not intended to be used by itself. Rather, babel-runtime is intended to be paired with a transform—babel-plugin-transform-runtime—which will automatically rewrite your code such that you can write your code using the Promise API and it will be transformed to use the Promise-like object exported by babel-runtime.

So why use babel-polyfill vs babel-runtime or vice-versa? babel-polyfill offers you the conveniences of globally defined objects without having to transform your code further. However, as with anything that mutates a global, this can introduce collisions between versions, etc.

babel-runtime, on the other hand, will not suffer from collisions as everything is name-spaced. Since the module will be defined in your package.json, it can be versioned like everything else. The tradeoff, however, is that a transform can only do so much. The runtime remaps methods according to a definitions map. Anecdotally, this has covered each of my use-cases but there may be an obscure method or two which is not remapped. There are also certain cases where your intent, as the author of the code, is ambiguous. In such cases, the transform won’t know exactly what to do, so it won’t do anything at all. This is the case for, e.g., instance methods:

// this will not be transformed by babel-runtime because it's
// ambiguous. However, since babel-polyfill assigns methods directly
// on the String prototype, it will work:
"!!!".repeat(3)

The short of it is if you’re writing an app, either babel-polyfill or babel-runtime will serve you just the same. Personally, I prefer the runtime since our code is already being transpiled by babel so tacking on another transform makes no difference, and there is no possibility of namespace collision or some 3rd party code making assumptions based on it’s own poorly written polyfill. In either case, they’ll need to be considered dependencies of your app while, with babel-runtime, you’ll need to include babel-plugin-transform-runtime as, at the very least, a development dependency (as it will need to be available at compilation time, but not at execution time).

If you’re writing a module you intend to be consumed by other projects, never use the polyfill. Since you won’t control the entirety of the context in which you’ll be executing, you cannot guarantee there won’t be multiple versions of various polyfills. Better to play it safe and have all your ES2015 methods and objects be namespaced by babel-runtime.

2015–12–18 at 7:50PM EST: as pointed out by Eric Ferraiuolo, there will be cases when you don’t need the polyfills at all. If you’re writing a library where KBs count (like in the browser), you may be better off avoiding the bloat of these particular, all-in-one polyfills. Simply documenting which ES2015 methods you rely on (as Facebook has done for React), or pulling in individual polyfills will result in a substantially smaller library.


Practically speaking, what does this mean?

To summarize, with the general case for Babel 6, there are two main steps you’ll need to perform:

  • Provide your code with an emulated ES2015 environment by either requiring babel-polyfill, or requiring the babel-runtime module plus the babel-plugin-transform-runtime transform:
// for babel-polyfill, either add:
require('babel-polyfill');
// ... at the top of your root script, or, if on node >= 4,
// you can call your script with the --require flag to include it
// like so:
node --require babel-polyfill myApp.js
// for babel-runtime, install the module, then use the
// babel-plugin-transform-runtime transform by including it in
// your .babelrc file or declaring it when compiling:
babel --plugins transform-runtime myApp.js
  • Run a series of transforms over your code with either the babel cli (provided by babel-cli), the .transform api (provided by babel-core), or implicitly when required (via babel-register)