A short and simple guide to Babel

By

A short guide to Babel 8: transform modern JavaScript for specific browsers or runtimes with preset-env, Browserslist, the CLI, and webpack.

~~~

Babel is a JavaScript compiler.

It parses your source code, applies the transformations you configured, and prints JavaScript that your target environments can run.

Often you don’t need to configure Babel yourself, because a framework or a build tool already does it for you. Add Babel when your project needs a transform that its toolchain doesn’t provide.

What Babel transforms

Say your source contains an arrow function:

const double = (number) => number * 2

For a target that doesn’t support arrow functions, Babel can output:

const double = function (number) {
  return number * 2
}

The exact output depends on your targets and your configuration. Babel should transform code when one of the environments you support needs it, not just because a feature is new.

Install Babel locally

Install the core compiler, the command line interface and the environment preset:

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

Babel goes in the project’s dev dependencies. This way the toolchain is part of the project, and you don’t depend on a global command.

This installs Babel 8, the current major version. Babel 8 needs Node.js 22.18 or later on the 22 line, or 24.11 or later. That requirement is only for the machine that runs Babel. The code it outputs can still target older browsers and older versions of Node.js.

Check what you got:

npm ls @babel/core

If it prints a 7.x version, look at node --version. On an older Node.js, like 22.14, npm skips the releases whose engine requirement you don’t meet and quietly installs Babel 7 instead. Upgrade Node.js and install again.

What changed in Babel 8

If you’re coming from Babel 7, or you’re following an older tutorial, a few defaults are different now.

Babel itself ships as native ES modules only. You still run it the same way, from the CLI or through a bundler.

The default targets changed. When you didn’t set any targets, Babel 7 compiled for every browser, which meant ES5 output. Babel 8 uses the Browserslist defaults query instead, so a config with no targets now leaves most modern syntax alone. If you support old browsers, say so in a .browserslistrc file or with a top-level targets option.

@babel/preset-env now keeps ES modules by default, because it assumes the environment understands import and export. Nothing changes when webpack or Rollup calls Babel, since they already tell Babel they handle modules. If you compile straight to Node.js code that must be CommonJS, set it explicitly:

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

The loose and spec options are gone from preset-env. The top-level assumptions option replaces them, and the migration guide lists the equivalent settings.

For polyfills, the migration guide recommends babel-plugin-polyfill-corejs3 instead of preset-env’s useBuiltIns and corejs options. More on polyfills below.

@babel/preset-react now defaults to the automatic JSX runtime, so you no longer need import React from 'react' at the top of every file that uses JSX.

To upgrade an existing project, start from the official Babel 8 migration guide. Its advice is to first change your Babel 7 config to match the new defaults, get the build working, and only then upgrade the packages.

Configure @babel/preset-env

Create babel.config.json in the project root:

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

@babel/preset-env picks the syntax transforms based on your target environments. That’s much easier to maintain than listing syntax plugins one by one.

The preset reads your Browserslist configuration. Add a .browserslistrc file:

defaults
not dead

This is only an example. Pick your targets based on who actually uses your app and what you promise to support.

To see which browsers a query resolves to, run:

npx browserslist

Read the @babel/preset-env documentation before you copy a target list from somewhere. Browser versions in old tutorials go stale fast.

Compile files from the command line

Compile one file:

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

Compile a whole folder:

npx babel src --out-dir dist

Add a script to package.json, so you run the same build every time:

{
  "scripts": {
    "build:js": "babel src --out-dir dist"
  }
}

Then run:

npm run build:js

The Babel usage guide covers the CLI and the other integrations.

Syntax transforms are not API polyfills

Rewriting arrow function syntax is different from adding a browser API that’s missing.

For example, a target might understand the syntax in this code but not provide the Array.prototype.at() method:

const last = items.at(-1)

Babel’s syntax transforms don’t fill in every missing global, static method or prototype method for you.

That’s what polyfills are for. They change the runtime environment, so they can grow the bundle and change global behavior. Pick a polyfill strategy for your app and your target browsers, instead of installing an old all-in-one Babel polyfill package you found in a tutorial.

The @babel/polyfill package is deprecated. With Babel 8, the recommended way to add only the core-js polyfills your targets need is babel-plugin-polyfill-corejs3:

npm install core-js
npm install --save-dev babel-plugin-polyfill-corejs3

Then add the plugin to your config:

{
  "presets": ["@babel/preset-env"],
  "plugins": [["polyfill-corejs3", { "method": "usage-global", "version": "3.50" }]]
}

With usage-global, Babel looks at the APIs your code uses, like items.at(), and adds imports for the core-js polyfills your targets are missing.

Set version to the core-js 3 minor version you installed. If you leave it out, the plugin assumes core-js 3.0 from 2019 and skips polyfills for newer features.

Presets and plugins

A plugin does one specific transformation.

A preset groups plugins and options for a common use case. Official presets include:

Install only the presets your source needs:

npm install --save-dev @babel/preset-react

Then add the preset to the config:

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

The TypeScript preset strips the type syntax, but it doesn’t type-check your code. Run the TypeScript checker separately when you care about type safety.

See Babel’s preset documentation.

Use Babel with webpack

Webpack builds the module graph and the bundle, while Babel transforms the JavaScript. You connect the two with babel-loader.

Install the loader:

npm install --save-dev babel-loader

Add a rule to webpack.config.js:

module.exports = {
  module: {
    rules: [
      {
        test: /\.(?:js|mjs|cjs)$/,
        exclude: /node_modules/,
        use: 'babel-loader'
      }
    ]
  }
}

babel-loader uses the project’s Babel configuration. Excluding node_modules means your dependencies don’t get compiled, which is what you want unless you know a specific package needs the transform too.

Current babel-loader releases work with both Babel 7 and Babel 8, and they need webpack 5.

See the current babel-loader documentation and the webpack guide.

Avoid unnecessary transforms

The older your targets, the more code Babel transforms, and the more runtime support you may need.

So keep your targets explicit and review them from time to time. Test the build in the oldest environments you promise to support.

When a framework owns the Babel setup, use the extension points it documents instead of adding a second Babel configuration that competes with it.

Tagged: DevTools · All topics

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about devtool: