Transpiling dependencies with Babel

[2017-04-23] dev, javascript, esnext, npm, jsmodules, babel
(Ad, please don’t block)

This post is part of a series of three:

  1. Current approaches: “Setting up multi-platform npm packages
  2. Motivating a new approach: “Transpiling dependencies with Babel”
  3. Implementing the new approach: “Delivering untranspiled source code via npm

Figuring out the best strategy for transpiling with Babel is tricky. This blog post proposes a new strategy, made possible by Babel’s preset-env.

The vision  

For a while, the best approach for building web apps via webpack was:

  • App code: transpiled via Babel and preset-latest:
    • Input: JavaScript with stage 4 features or older, in ES modules.
    • Output: ES5 in CommonJS modules.
  • Dependencies: are already transpiled.

Then Babel introduced preset-env, which lets you adapt the code you generate to the platforms it will run on. Now, the best approach would be:

  • App code: transpiled via Babel and preset-env.
    • Input: JavaScript with stage 4 features or older, in ES modules.
    • Output: whatever JavaScript runs best on the target platforms.
  • Dependencies: must be delivered as both...
    • Transpiled code that runs natively on Node.js (meaning CommonJS modules, at the moment).
    • Untranspiled code that is transpiled along with your own code.

The benefits of this new approach are:

  • ES modules enable tree-shaking (which, in general, is impossible with CommonJS modules).
  • The output is upgraded automatically as the target platforms evolve (thanks to preset-env).
  • You can target multiple platforms.

Implementing the vision  

How can we deliver both transpiled and untranspiled code in npm packages?

The status quo  

Three properties in package.json currently let you deliver multiple versions of the same code (for more information, consult “Setting up multi-platform npm packages”):

  • main: points to a CommonJS module (or UMD module) with JavaScript as modern as Node.js can currently handle.
  • browser: swaps out some of the main code so that it works in browsers.
  • module: the same code as main, but in ES modules.
  • es2015: untranspiled ES6 code. Introduced by Angular.

Alas, none of these properties help with delivering untranspiled code beyond ES6 (es2015) or beyond what’s currently supported in Node.js (module).

Proposal: pkg.esnext  

pkg.foo is an abbreviation for “property foo in package.json”.

I propose to use package.json as follows:

  • esnext: source code using stage 4 features (or older), not transpiled, in ES modules.
  • main continues to be used the same way. Its code can be generated from the untranspiled code.
  • Most module use cases should be handleable via esnext.
  • browser can be handled via an extended version of esnext (see next section).

The next blog post in this series describes how to implement this approach.

This is an excerpt of a package.json using the new property:

{
    ···
    "main": "main.js",
    "esnext": "main-esnext.js",
    ···
}

Extended versions of pkg.esnext  

An extended version of pkg.esnext could be a JSON object with two properties:

  • main: points to the untranspiled code.
  • browser: points to browser-specific untranspiled code.

This would look as follows:

{
    ···
    "main": "main.js",
    "esnext": {
        "main": "main-esnext.js",
        "browser": "browser-specific-main-esnext.js"
    },
    ···
}

Other solutions  

webpack can be configured to recognize alternatives to pkg.main (via resolve.mainFields). That enables you to refer to untranspiled code via pkg.module. Note that that is not exactly how module is supposed to be used, but it lets you use packages with that property. And the following solutions become possible:

  • webpack contributor Sean Larkin always transpiles all dependencies. This increases build times, but has the advantage of being easy to configure.
  • Jason Miller proposes to tell webpack to transpile only packages that have pkg.module, via clever use of module.loaders and include. He documented his approach in a Gist.
  • Sam Verschueren has written the Babel plugin babel-engine-plugin, which only transpiles packages that depend on Node.js versions newer than 0.10 (with features beyond ES5). To do so, it checks pkg.engines. This plugin could be adapted to support the new approach outlined in this blog post.

Yet another quick-and-dirty approach is to use file extensions:

  • .js is never transpiled.
  • .esm is always transpiled, even in npm-installed dependencies. It’s best not to use the upcoming file extension .mjs, so that future code doesn’t break packages using this approach.

This approach is especially handy if your own app is spread out across multiple npm packages.

Further reading