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

5.3k views Asked by At

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:

  • same as second approach (see above)

Comparing the output for the different approaches

Using just useBuiltIns:

  • Brings in the required string polyfills, but pollutes the global namespace.

Using just @babel/runtime-transform:

  • Doesn't bring in any string polyfills, but brings in other helpers/polyfills ??, for Array map and Object spread

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

  • Brings in the required string polyfills, but pollutes the global namespace.
  • Also brings in Object spread polyfill (but not the Array map polyfill)
  • Imports from @babel/runtime/helpers/objectSpread2, not @babel/runtime-corejs3/helpers/objectSpread2 (runtime vs runtime-corejs3) - could be the reason that Array map polyfill was not brought in??)

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?

2

There are 2 answers

3
Per Quested Aronsson On BEST ANSWER

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:

0
Qiulang On

The linked article in the accepted answer was dead. Checking from the content I believe it came from the comment posted by JMarkoski at this issue https://github.com/babel/babel/issues/9853, take a look at his detailed explanation if you want to know more.

But I believe there is one error in this answer (or an outdated opinion), I think we should use useBuiltIns: 'usage' instead of useBuiltIns: 'entry' because usage is easier and the bundled size is much smaller, the answer from the babel maintainer I still don't get the differences between @babel/preset-env.useBuiltIns: entry and useBuiltIns: usage confirmed this.

I see using useBuiltIns: 'usage' always comes with corejs: { version: "3.8", proposals: true }, after all https://babeljs.io/docs/babel-preset-env#corejs says that and I see most of the answers in SO use proposals: true.

But I think we better set proposals: false (which is the default value) when you are targeted to the production environment because it is safer, proposals: true is for experimenting.

Also, I think most of the time we only want to polyfill the feature(s) the target browsers lack but we don't need to polyfill the features in the proposal stage unless we know we use that feature.

Here is an example to show what I mean, my .babelrc

   "presets": [
     [
         "@babel/preset-env",
         {
             "targets": {
                 "chrome": "84"
             },
             "modules": "commonjs",
             "debug": true,
             "useBuiltIns": "usage",
             "corejs": {
                 "version": 3.8
                 // "proposals": true
             }
         }
     ]
  ]

To turn off "proposals", I see, from the log, babel polyfill what I need (refer to Is it possible to break away from await Promise.all when any promise has fulfilled (Chrome 80)),

[/Users/langqiu/xxx/getLoginInfo.js]
The corejs3 polyfill added the following polyfills:
  es.aggregate-error { "chrome":"84" }
  es.promise.any { "chrome":"84" }

But with "proposals": true babel polyfill more than what I need. I am sure I don't need those esnext stuff and I am happy with what Chrome has.

[/Users/langqiu/xxx/getLoginInfo.js]
The corejs3 polyfill added the following polyfills:
  es.aggregate-error { "chrome":"84" }
  es.promise.any { "chrome":"84" }
  esnext.async-iterator.map { "chrome":"84" }
  esnext.iterator.map { "chrome":"84" }
  esnext.async-iterator.filter { "chrome":"84" }
  esnext.iterator.constructor { "chrome":"84" }
  esnext.iterator.filter { "chrome":"84" }