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?
I checked caniuse.com which shows it as fully supported - which would mean no polyfills are required.
However, according to Manuel Beaudru's blog post core-js@3, babel and a look into the future
caniuse
,mdn
andcompat-table
are good educational resources but aren't really meant to be used as data sources for developer tools: only thecompat-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 usingcore-js@3
,@babel/preset-env
will use that new package instead ofcompat-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.
So far, so good. It looks like string.replace
does need to be polyfilled for Edge 18.
@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.
@babel/runtime
and @babel/transform-runtime
According to the core-js documentation:
@babel/runtime
withcorejs: 3
option simplifies work withcore-js-pure
. It automatically replaces usage of modern features from JS standard library to imports from the version ofcore-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.
My last attempt was to combine both approaches - and to follow the recommendations:
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
:
Using just useBuiltIns
:
Using just @babel/runtime-transform
:
Using combination of both useBuiltIns
and @babel/transform-runtime
:
@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??)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?
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: