---
title: "Helping Babel move to ES Modules"
description:
  "Migrating from CommonJS to ESM in babel internals and the challenges faced"
canonical_url: "https://www.bigbinary.com/blog/helping-babel-move-to-esm"
markdown_url: "https://www.bigbinary.com/blog/helping-babel-move-to-esm.md"
---

# Helping Babel move to ES Modules

Migrating from CommonJS to ESM in babel internals and the challenges faced

- Author: Karan Sapolia Sharma
- Published: May 18, 2021
- Categories: JavaScript

The Babel project recently moved to running builds on Node.js 14, which means
that Babel can now use
[ES Modules](https://nodejs.org/api/esm.html#esm_modules_ecmascript_modules)
(ESM) instead of CommonJS (CJS) to import/export modules for internal scripts.

Also, with the upcoming Babel 8.0.0 release, the team is aiming to
[ship Babel as native ES Modules](https://github.com/babel/babel/issues/11701).
With this goal in mind, the team is shifting all CommonJS imports/exports to ESM
ones. This is where I got the opportunity to contribute to Babel recently.

## Why ES Modules though?

For a very long time, JS (or ECMAScript) did not have a standardized module
import/export syntax. Various independent packages introduced formats to help
work with modules in JS. Most browsers used the AMD API (Asynchronous Module
Definition) implemented in the Require.js package, which had its own syntax and
quirks.

CommonJS on the other hand was the standard used by Node.js, and it was no less
quirky. Inconsistent formatting and
[poor interoperability between packages](https://nodejs.org/api/esm.html#esm_interoperability_with_commonjs)
irked JS developers enough to demand a standard format.

Lately, the ECMAScript Standardization body (TC39) has adopted ESM (ECMAScript
modules) as the standard format for Javascript. Most web browsers already
support this format and Node.js 14 now provides
[stable support for it](https://nodejs.org/api/esm.html#esm_modules_ecmascript_modules).

## The task at hand

The next task was to convert all internal top-level scripts from using CommonJS
to ESM. The finer details of the implementation, along with interoperability
issues with non-ESM files, would trouble CommonJS for some time though.

The simplest of changes was to replace `require()` statements in each file with
`import` statements. For example, files starting like:

```javascript
"use strict";

const plumber = require("gulp-plumber");
const through = require("through2");
const chalk = require("chalk");
```

would be modified like here:

```javascript
import plumber from "gulp-plumber";
import through from "through2";
import chalk from "chalk";
```

to allow modules to be imported as ES modules.

In the above example also note that because ES modules are in strict mode by
default, so `"use strict";` declarations were removed from the beginning of
these top-level scripts.

Almost all current NPM packages are CommonJS packages, exposing their
functionalities using the `module.exports` syntax.

In case a file/package exports more than one value, we need to use named imports
instead:

```javascript
import { chalk } from "chalk";
```

Where the default export object from a CommonJS module was named differently, it
had to be aliased during import to avoid breaking pre-existing variables' names
in the files being converted to ESM. For example,

```javascript
const rollupBabel = require("@rollup/plugin-babel").default;
```

had to be replaced with:

```javascript
import { babel as rollupBabel } from "@rollup/plugin-babel";
```

so we could keep using the variable `rollupBabel` in the file.

For instances where `require()` statements needed to be replaced by the dynamic
`import()` statements

```javascript
const getPackageJson = pkg => require(join(packageDir, pkg, "package.json"));

// replaced by
const getPackageJson = pkg => import(join(packageDir, pkg, "package.json"));
```

the subsequent calls everywhere now needed to be awaited:

```javascript
   .forEach(id => {
      const { name, description } = getPackageJson(id);
   })

   //await added
   .forEach(id => {
      const { name, description } = await getPackageJson(id);
   })
```

Other things like importing JSON modules are currently only supported in
CommonJS mode.
[Those imports were left as-is](https://nodejs.org/api/esm.html#esm_no_json_module_loading).

## Blockers

With all the changes made and committed, we bumped into the next big roadblock:
package dependencies. Babel uses Yarn 2 internally, and particularly the PnP
feature of Yarn 2. Unfortunately, the ESM loader API was experimental at the
time and not being used by PnP. The Babel and Yarn teams
[coordinated to implement it](https://github.com/babel/babel/pull/12296#discussion_r546301877)
soon after.

Similarly, Jest has its own custom loader for ESM, which meant it could not
support testing ESM modules with Babel.
[That issue](https://github.com/facebook/jest/issues/9430) was side-stepped for
the time being.

## Network effects

The good thing about the whole grind of shifting from CommonJS to ESM is that a
lot of other major packages are also considering and implementing ESM support.
The shift to ESM-only by Babel is already building confidence in others to do
the same. Special thanks to the Babel maintainers for setting a great example
and
[encouraging others to move to ESM](https://twitter.com/NicoloRibaudo/status/1356030492546650115?s=20).

## Conclusion

All told, it was a great experience adding a new feature into a well-maintained
and widely-used package. The biggest lesson from this has to be how changes made
in Babel affect and influence other major packages, and how maintainers of
various major open source packages work in tandem to avoid breaking each other's
code. It is a very open and collaborative ecosystem with people discussing and
working through github issues, comments, and even twitter threads.

Check out the [pull request](https://github.com/babel/babel/pull/12296) for more
details.

## Links

- [Human page](https://www.bigbinary.com/blog/helping-babel-move-to-esm)
