How babel preset-env, core-js, and browserslistrc work together

Modern frontend tools like babel seem almost magical. What do they do really?

How babel preset-env, core-js, and browserslistrc work together

Configuration for our beloved frontend tools is hot lava. Today CLI tools as create-react-app or Vue cli abstract away most of the configuration, other than providing sane defaults.

Even then, understanding how things work under the hood is beneficial because sooner or later you'll need to make some adjustment to the defaults.

In this post we're going to see how babel preset-env, core-js, and browserslistrc work nicely together to enable newer JavaScript features for older browsers.

Installing and configuring babel

babel is a JavaScript compiler and "transpiler". Given modern JavaScript syntax as input, babel is able to transform it to compatible code that can run in any browser.

To start off create a new folder and initialize the project:

mkdir how-preset-env && cd $_

npm init -y

Then install babel with:

npm i @babel/core @babel/preset-env @babel/cli --save-dev

Here we installed babel cli with babel core, and the env preset. Next up we're going to configure babel. Create a configuration file with:

touch babel.config.json

Now in babel.config.json configure babel to use preset-env:

{
  "presets": [
    "@babel/preset-env"
  ]
}

Babel can use plugins for transpiling newer JavaScript syntax. Presets instead are collection of plugins.

@babel/preset-env is a collection of babel plugins to transform modern JavaScript code, depending on the target browser we specify in the configuration.

Targets can appear in babel.config.json, but to have more flexibility we can use a .browserslistrc.

Create .browserslistrc in the root project folder, and put in the following configuration:

firefox 73

Now let's test things out!

From shiny new syntax to ECMAScript 5

To see how babel works with .browserslistrc create a new file in src/index.js with the following code:

const obj = {
  arr: [1, 2, 3, 4],
  printArr() {
    console.log(...this.arr);
  }
};

obj.printArr();

Save and close the file, then from the console run:

node_modules/.bin/babel src/index.js

We're targeting a new Firefox version in .browserslistrc, so you should see the following output:

"use strict";

const obj = {
  arr: [1, 2, 3, 4],

  printArr() {
    console.log(...this.arr);
  }

};
obj.printArr();

The code isn't transpiled at all despite going through babel! Now change .browserslistrc to target Internet Explorer:

ie 11

Run again babel with:

node_modules/.bin/babel src/index.js

Now you should see:

"use strict";

function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }

function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }

function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }

function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter); }

function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }

function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }

var obj = {
  arr: [1, 2, 3, 4],
  printArr: function printArr() {
    var _console;

    (_console = console).log.apply(_console, _toConsumableArray(this.arr));
  }
};
obj.printArr();

You can see babel applying the appropriate transformation to our JavaScript code. Since we're targeting Internet Explorer 11 (good for you if you don't have to) babel compiles down all the code down to ECMAScript 5.

Babel does its best, but you can still see in the code some reference to modern JavaScript features like Array.from which could kick in our snippet in case the array consumed by printArr is array-like and not exactly an array.

Older browsers won't run this code. What to do then?

Enter core-js

core-js is a collection of polyfills. A polyfill is a hand-made JavaScript version of some feature that is not yet implemented in all browsers.

In other words is custom code that any JavaScript developer can write and distribute to enable new features in the browser.

For example before the real Fetch API was public, developers eager to try it installed a custom Fetch polyfill. All of that before the "real" Fetch was implemented by browser's vendors.

What core-js can do with babel is intelligent loading of polyfills depending on the target. To install core-js run:

npm i core-js

Then in src/index.js import the library:

import "core-js";

const obj = {
  arr: [1, 2, 3, 4],
  printArr() {
    console.log(...this.arr);
  }
};

obj.printArr();

Now configure babel to use core-js in babel.config.json:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry",
        "corejs": 3
      }
    ]
  ]
}

At this point run again babel (you can also save the resulting file):

babel src/index.js --out-file out.js

In the generated file you should see something along these lines:

require("core-js/modules/es.symbol");

require("core-js/modules/es.symbol.description");

require("core-js/modules/es.symbol.async-iterator");

require("core-js/modules/es.symbol.has-instance");

require("core-js/modules/es.symbol.is-concat-spreadable");

require("core-js/modules/es.symbol.iterator");

require("core-js/modules/es.symbol.match");

require("core-js/modules/es.symbol.replace");

require("core-js/modules/es.symbol.search");

require("core-js/modules/es.symbol.species");

// more and more

What's going on? By importing core-js just in a single file we can load all the polyfills for our target browsers. Only what's really needed is imported in the resulting code.

In case you're targeting newest browsers like Chrome or Firefox > 73, babel loads only esnext polyfills.

Wrapping up

Still, in the code above you can see that babel outputs by default common js modules. Also, core-js polyfills are bare imports. But, that's another story, and really, transforming modules is a task for webpack.

CLI like create-react-app and Vue CLI already offer production-ready configurations out of the box. You don't have to configure all by yourself. Still, a basic knowledge of how tools like babel and core-js work together is always a nice to have.

Thanks for reading and stay tuned.

Resources

Valentino Gagliardi

Hi! I'm Valentino! I'm a freelance consultant with a wealth of experience in the IT industry. I spent the last years as a frontend consultant, providing advice and help, coaching and training on JavaScript, testing, and software development. Let's get in touch!