javascriptbabeljsbabel-preset-env

What is best practice for `@babel/preset-env` + `useBuiltIns` + `@babel/runtime` + `browserslistrc`


I get different output for different configurations of @babel/preset-env with useBuiltIns used in combination with @babel/transform-runtime. I've read the documentation, but haven't been able to figure out what the best practice should be.

For example, @babel/preset-env with useBuiltIns will add a polyfill for string.replace when my targeted list of browsers includes Edge 18.

But when I use @babel/transform-runtime instead, that polyfill doesn't get added.


So, starting out with this question:

Does `string.replace` need to be polyfilled for Edge 18?

caniuse, mdn and compat-table are good educational resources but aren't really meant to be used as data sources for developer tools: only the compat-table contains a good set of ES-related data and it is used by @babel/preset-env, but it has some limitations

And further:

For this reason, I created the core-js-compat package: it provides data about the necessity of core-js modules for different target engines. When using core-js@3, @babel/preset-env will use that new package instead of compat-table.

So I passed my target browsers to core-js-compat and it output all the polfills required. As you can see in the image below, quite a few string methods need to be polyfilled, mostly to support Edge 18.

enter image description here

So far, so good. It looks like string.replace does need to be polyfilled for Edge 18.


Babel config

First approach: @babel/preset-env and useBuiltIns: 'usage'

When I use useBuiltIns: 'usage' to bring in per-file polyfills from core-js:

// babel.config.js

  presets: [
    [
      '@babel/preset-env',
      {
        debug: false,
        bugfixes: true,
        useBuiltIns: 'usage',
        corejs: { version: "3.6", proposals: true }
      }
    ],
    '@babel/preset-flow',
    '@babel/preset-react'
  ],

When debug: true, Babel says it will add the following polyfills to my PriceColumn.js file:

// Console output

[/price-column/PriceColumn.js] Added following core-js polyfills:

  es.string.replace { "edge":"17", "firefox":"71", "ios":"12", "safari":"12" }
  es.string.split { "edge":"17" }
  web.dom-collections.iterator { "edge":"17", "ios":"12", "safari":"12" }

One difference is that it says es.string.replace is to target edge: 17, not edge: 18 as we see in the output from core-js-compat above - might be something I've done, but that's fine for now.

The additions that Babel adds to the top of the transpiled PriceColumn.js file:

// PriceColumn.js

"use strict";

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

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

require("core-js/modules/web.dom-collections.iterator");

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;

Again, so far so good.


Second approach: @babel/runtime and @babel/transform-runtime

According to the core-js documentation:

@babel/runtime with corejs: 3 option simplifies work with core-js-pure. It automatically replaces usage of modern features from JS standard library to imports from the version of core-js without global namespace pollution

Sounds great - let's try it out!

Commenting out useBuiltIns and adding @babel/transform-runtime plugin config:

// babel.config.js

  presets: [
    [
      '@babel/preset-env',
      {
        debug: true,
        // bugfixes: true,
        // useBuiltIns: 'usage',
        // corejs: { version: '3.6', proposals: true }
      }
    ],
    '@babel/preset-flow',
    '@babel/preset-react'
  ],
  plugins: [
    [
      '@babel/transform-runtime',
      {
        corejs: { version: 3, proposals: true },
        version: '^7.8.3'
      }
    ]
  ],

In the console output, I see:

Using polyfills: No polyfills were added, since the `useBuiltIns` option was not set.

Checking what was added to the top of the file:

// PriceColumn.js

"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

var _Object$defineProperty = require("@babel/runtime-corejs3/core-js/object/define-property");

_Object$defineProperty(exports, "__esModule", {
  value: true
});

exports.default = void 0;

var _objectSpread2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/objectSpread2"));

var _map = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/map"));

So, different helpers were added - but no sign of the es.string.* polyfills. Are they no longer required? Are they already brought in by the 'helpers'? It doesn't look like object spread and array map would have anything to do with polyfilling string instance methods, so I think not.


Finally

My last attempt was to combine both approaches - and to follow the recommendations:

enter image description here

a) Set corejs for @babel/preset-env:

// babel.config.js

  presets: [
    [
      '@babel/preset-env',
      {
        debug: true,
        // bugfixes: true,
        useBuiltIns: 'usage',
        corejs: { version: '3.6', proposals: true }
      }
    ],
    '@babel/preset-flow',
    '@babel/preset-react'
  ],
  
  plugins: [
    [
      '@babel/transform-runtime',
      {
        // corejs: { version: 3, proposals: true },
        version: '^7.8.3'
      }
    ]
  ]

and this is the output:

// PriceColumn.js

"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

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

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

require("core-js/modules/web.dom-collections.iterator");

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;

var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread2"));

b) Set corejs for @babel/transform-runtime:


Comparing the output for the different approaches

Using just useBuiltIns:

Using just @babel/runtime-transform:

Using combination of both useBuiltIns and @babel/transform-runtime:


Question

Which - if any - of these is the correct approach?

I'm guessing the @babel/preset-env with useBuiltIns is the best because it brings in the polyfills.

What are the drawbacks to polluting the global namespace? Is this an issue for libraries only?

In combination with @babel/transform-runtime, we also get a polyfill for object spread (even though @babel-preset-env has corejs: { version: '3.6', proposals: true } which should polyfill proposals, so I'm not sure why it doesn't get brought in there without having to use the @babel/transform-runtime plugin too)

Do we need the Array#map polyfill?


Solution

  • Suggested by https://www.jmarkoski.com/understanding-babel-preset-env-and-transform-runtime:

    App: If you are authoring an app, use import 'core-js at the top of your app with useBuiltIns set to entry and @babel/transform-runtime only for helpers (@babel/runtime as dependency). This way you pollute the global environment but you don't care, its your app. You will have the benefit of helpers aliased to @babel/runtime and polyfills included at the top of your app. This way you also don't need to process node_modules (except when a dependency uses a syntax that has to be transpiled) because if some dependency used a feature that needs a polyfill, you already included that polyfill at the top of your app.

    Library: If you are authoring a library, use only @babel/transform-runtime with corejs option plus @babel/runtime-corejs3 as dependency, and @babel/preset-env for syntax transpilation with useBuiltIns: false. Also I would transpile packages I would use from node_modules. For this you will need to set the absoluteRuntime option (https://babeljs.io/docs/en/babel-plugin-transform-runtime#absoluteruntime) to resolve the runtime dependency from a single place, because @babel/transform-runtime imports from @babel/runtime-corejs3 directly, but that only works if @babel/runtime-corejs3 is in the node_modules of the file that is being compiled.

    More Info: