@ololoepepe/babel-preset-default
v0.3.0
Published
Default Babel preset
Downloads
117
Readme
@ololoepepe/babel-preset-default
A default, browser-first (but universal) Babel preset built on
@babel/preset-env plus a small, curated set of
proposal plugins. It targets Babel 8 and is shipped as an ES module.
The preset deliberately sets no targets — it stays target-agnostic so every consumer
controls its own output via their own browserslist.
It also polyfills modern built-ins — .at(), .toSorted(), Object.hasOwn(), Object.groupBy()
and friends — with pure imports from core-js-pure, but only when you opt in by installing it.
See Polyfills.
Configure browserslist. With polyfills enabled the browser/engine targets stop being a down-levelling detail and start deciding which polyfills get injected into your bundle. See Targets / browserslist.
Requirements
- Node.js ≥ 20 (Babel 8 requirement).
- Babel 8 —
@babel/core@^8.
Installation
npm install --save-dev @ololoepepe/babel-preset-defaultPeer dependencies
npm install --save-dev @babel/core@^8
npm install @babel/runtime@^8@babel/core— required by@babel/preset-envand every included plugin.@babel/runtime—@babel/plugin-transform-runtimerewrites Babel's inline helpers into imports from@babel/runtime, so the output your build produces depends on it at runtime. Install it as a regular dependency of the project being compiled.
Two more peers are optional — they enable and back the polyfill layer, and are declared
peerDependenciesMeta.optional so npm never installs them behind your back:
npm install core-js-pure@^3 @babel/runtime-corejs3@^8core-js-pure— the polyfill implementations the compiled output imports. Installing it is what switches polyfilling on; leaving it out is what keeps a Node-only package free of ballast.@babel/runtime-corejs3— required together withcore-js-pure, not instead of it: once the polyfill plugin is active,@babel/plugin-transform-runtimesources its helpers from@babel/runtime-corejs3rather than@babel/runtime. Both are regular dependencies of the project being compiled.
Usage
Reference the preset by name in your Babel configuration.
babel.config.js:
export default {
presets: ['@ololoepepe/babel-preset-default']
};or .babelrc / babel.config.json:
{
"presets": ["@ololoepepe/babel-preset-default"]
}ESM note: this package is an ES module (
"type": "module"). Babel loads presets asynchronously in bundlers and@babel/cli, so ESM presets work out of the box there. Some tools that resolve Babel config synchronously (for example, certainbabel-jestsetups) cannot load an ESM preset directly — in those cases load your Babel config through an async path, or wrap the preset in a small config that your tool can consume.
What's included
The preset runs the following plugins (in order) and then @babel/preset-env:
| Piece | What it does |
| --- | --- |
| @babel/plugin-proposal-decorators (version: '2023-11') | Decorators per the 2023-11 standard. Class fields are handled by preset-env. |
| @babel/plugin-proposal-export-default-from | export def from './mod'; re-export syntax. |
| @babel/plugin-proposal-pipeline-operator (proposal: 'hack', topicToken: '%') | The Hack-style pipeline operator |> with % as the topic token. |
| @babel/plugin-proposal-do-expressions | do { ... } expressions. |
| babel-plugin-polyfill-corejs3 (method: 'usage-pure') | Rewrites built-ins your targets lack — .at(), .toSorted(), Object.hasOwn(), … — into pure core-js-pure imports. Only added when polyfilling is enabled; see Polyfills. |
| @babel/plugin-transform-runtime | Deduplicates Babel's helpers by importing them from @babel/runtime — or from @babel/runtime-corejs3 when the polyfill plugin above is active (hence the peer dependencies). |
| babel-plugin-add-module-exports | CJS interop: exposes a lone default export as module.exports so CommonJS consumers can require('mod') instead of require('mod').default. |
| @babel/preset-env | Compiles modern ECMAScript (including now-standard proposals such as optional chaining, nullish coalescing, class fields, logical assignment, etc.) down to the consumer's targets. Configured with no targets. |
Syntax examples for the kept proposals:
// decorators (2023-11)
@logged
class Service {
@bound handle() {}
}
// export-default-from
export def from './service.js';
// pipeline-operator (hack style, % topic)
const result = value |> double(%) |> increment(%);
// do-expressions
const label = do {
if (ok) { 'yes'; } else { 'no'; }
};Polyfills
@babel/preset-env compiles syntax. It does not touch built-ins: [1, 2, 3].at(-1) and
array.toSorted() survive compilation untouched and then throw on any engine that lacks them. This
preset closes that gap with
babel-plugin-polyfill-corejs3 in usage-pure
mode, so a source file like
export const last = [1, 2, 3].at(-1);
export const sorted = [3, 1, 2].toSorted();compiles (for targets that need it) to
import _atInstanceProperty from 'core-js-pure/features/instance/at.js';
import _toSortedInstanceProperty from 'core-js-pure/features/instance/to-sorted.js';
export const last = _atInstanceProperty(_ctx = [1, 2, 3]).call(_ctx, -1);
export const sorted = _toSortedInstanceProperty(_ctx2 = [3, 1, 2]).call(_ctx2);Pure, not global: nothing is monkey-patched onto Array.prototype, so the output is safe to
publish as a library. Only the built-ins you actually use are imported, and only when your targets
lack them.
Switching it on and off
Polyfilling follows the presence of core-js-pure. The preset resolves
core-js-pure/package.json at config time:
- resolved → the plugin is added, pinned to that exact installed version.
- not resolved → the plugin is not added at all. A Node-only package that never installs
core-js-puretherefore carries no polyfill machinery whatsoever.
The version is read rather than hard-coded on purpose: babel-plugin-polyfill-corejs3 needs to know
which modules exist in the core-js-pure you have, and a hard-coded number drifts out of sync the
moment you upgrade — producing imports of modules that aren't there.
To disable polyfilling explicitly even though core-js-pure is installed (it may be present as some
other package's transitive dependency), pass corejs: false:
export default {
presets: [['@ololoepepe/babel-preset-default', {corejs: false}]]
};Options
| Option | Type | Default | Meaning |
| --- | --- | --- | --- |
| corejs | boolean \| string \| {version?: string, proposals?: boolean} | true | true — auto-detect the installed core-js-pure. false — no polyfilling. A version string ('3.49.0') — force that version, e.g. when a strict package manager keeps core-js-pure out of the preset's resolution path. An object — the same, with control over proposals. |
Why proposals: true
The preset enables proposals by default, and this is not about running bleeding-edge
proposals. core-js files a number of long-standard methods under esnext.*, and .at() is the
notable one: in pure mode it resolves through esnext.string.at, which proposals: false filters
out as "not stable". The practical effect of turning proposals off is that .at() is silently
left unpolyfilled — no error, no warning, just a TypeError on an old engine at runtime.
The flip side is that esnext.string.at carries no compatibility data, so it counts as missing
everywhere: .at() is polyfilled even on targets that support it natively. That is one tiny module,
and it is the trade the preset takes. corejs: {proposals: false} is there if you want the other
one.
Targets / browserslist
Because the preset sets no targets, the actual output is decided by the consumer's
environment:
- Under a bundler / caller that supports ESM,
preset-env's defaultmodules: 'auto'keeps ES modules; under Node's CommonJS resolution it emits CommonJS. - To control down-leveling, add your own browserslist
config (a
browserslistkey inpackage.json, a.browserslistrc, ortargetsin your Babel config).preset-envpicks it up automatically.
With polyfills enabled, configuring browserslist stops being optional in practice. The targets
no longer merely decide how much syntax gets down-levelled — they decide which core-js-pure
modules end up in your bundle. If you configure nothing, Babel falls back to browserslist's
defaults query (> 0.5%, last 2 versions, Firefox ESR, not dead), which is a moving target: it
is recomputed from the caniuse-lite data of whatever install you happen to have, so the same
source can produce a different set of polyfill imports on two machines, or after a lockfile refresh.
Pin it:
{
"browserslist": ["last 2 versions", "not dead", "since 2020"]
}or, for a package that only ever runs in Node:
{
"browserslist": ["node 24"]
}Scripts
npm run lint— lint./srcwith ESLint v10 (flat config,eslint.config.js, extending@ololoepepe/eslint-config).npm run test— run the test suite with the built-in Node test runner (node --test). The tests assert the preset's shape, transpile-smoke each kept proposal via@babel/core@8, and check the polyfill layer end to end: that modern built-ins turn intocore-js-pureimports that actually resolve, that targets are honoured, and that the helper module follows the core-js switch.
License
UNLICENSED — see package.json.
