Native Support for CJS/ESM Interoperability Begins in Node.js 22
Tags: node, webdev, javascript, programming
Published: 2024-05-23 02:13:42

The adoption of ECMAScript Modules (ESM) for development has become mainstream, yet Node.js continues to use the traditional CommonJS (CJS) format. This has posed challenges for library and application developers. However, with the recent release of version 22, Node.js has begun to experimentally support ESM natively, and this support is expected to stabilize in future versions. Although current support has its limitations and is not yet perfect, it is sufficient to improve the existing situation. This article will provide a detailed overview.

# Understanding ESM and Its Synchronous Capabilities

ECMAScript Modules (ESM) are an official standard for structuring and loading modules in JavaScript. Unlike CJS, which executes synchronously, ESM was designed with the capability to load asynchronously. This design supports top-level `await` commands, allowing modules to handle asynchronous operations before they are used.

Despite this capability, ESM can also be **executed synchronously** if the module graph (the structure consisting of all the modules and their dependencies) does not contain any top-level `await`. 

Hold on, you might be as puzzled as I am — aren’t ESM modules executed asynchronously?

Indeed, the ESM standard is mentioned in the TC39 specifications, where it is [designed to be conditionally asynchronous](https://tc39.es/ecma262/?utm_source=webdeveloper.beehiiv.com&utm_medium=newsletter&utm_campaign=native-support-for-cjs-esm-interoperability-begins-in-node-js-22#sec-moduleevaluation) — [only when the module graph contains a top-level](https://tc39.es/ecma262/?utm_source=webdeveloper.beehiiv.com&utm_medium=newsletter&utm_campaign=native-support-for-cjs-esm-interoperability-begins-in-node-js-22#sec-source-text-module-record-execute-module) `await`.

Well, now that we know this, understanding the dual functionality of ESM — both synchronous and asynchronous — is crucial for comprehending the new features in Node.js.

# Node.js Enhancement: Synchronous `require()` for ESM

The recent update in Node.js introduces an experimental feature that **allows** `require()` **to synchronously load ESM graphs that do not contain top-level** `await`. This feature is significant because it allows developers to use `require()` with ESM.

### How It Works

When enabled via the `--experimental-require-module` flag, Node.js can load an ESM if it meets the following conditions:

1. The module must be explicitly marked as an ES module either through a `"type": "module"` field in the nearest `package.json` or by using the `.mjs` file extension.

2. The module must be fully synchronous, meaning it contains **no top-level** `await`.

Under these conditions, `require()` can load the ESM and return the module namespace object directly, much like the dynamic `import()` statement, but executed synchronously.

### Code Example: Synchronous Module Loading

```javascript
// Define a module in a file named 'math-utils.mjs'
export function square(x) {
  return x ** 2;
}
export function cube(x) {
  return x ** 3;
}

// main.js - ​Synchronously require the ES module in a CommonJS file
// $ node --experimental-require-module main.js

const mathUtils = require('./math-utils.mjs');
​
console.log(mathUtils.square(2)); // Output: 4
console.log(mathUtils.cube(3)); // Output: 27
```

# Practical Implications and Benefits

The ability to `require()` ESM synchronously has several practical implications:

1. **Interop Simplicity** : It simplifies the interoperation between CJS and ESM, reducing the complexity of adopting ESM for projects that still rely heavily on CJS.

2. **Backward Compatibility** : It provides a smoother transition for libraries to move from CJS to ESM without breaking changes for consumers still using `require()`.

3. **Module Size Reduction** : It aids in reducing the size of `node_modules` by avoiding the need for dual module packaging (CJS and ESM), which is common in current practices.

# Challenges and Limitations

While this feature is a significant step forward, it comes with its own set of challenges and limitations:

- **Top-Level Await** :

Modules that use top-level `await` cannot be loaded with `require()` and will throw an `ERR_REQUIRE_ASYNC_MODULE` error. This limitation necessitates using `await import()` for such modules. For example:

```javascript
await new Promise(res => setTimeout(res)); // top-level await

export function square(x) {
  return x ** 2;
}
export function cube(x) {
  return x ** 3;
}

// Error: require() cannot be used on an ESM graph with top-level await. 
// Use import() instead. To see where the top-level await comes from, use --experimental-print-required-tla.

const mathUtils = require('./math-utils.mjs');
​
console.log(mathUtils.square(2)); // Output: 4
console.log(mathUtils.cube(3)); // Output: 27

// Pass:
void (async () => {
  const mathUtils = await import('./math-utils.mjs');

  console.log(mathUtils.square(2)); // Output: 4
  console.log(mathUtils.cube(3)); // Output: 27
})();
```

- **Experimental Status** :

Since it is still in the experimental phase, this feature may not be suitable for all production environments. There are still some feature interactions that this implementation doesn’t handle, such as `--experimental-detect-module`, `--experimental-loader`, or `--experimental-wasm-modules`. Some edge cases involving cycles might exhibit undefined behaviors.

# Conclusion

The introduction of synchronous `require()` support for ESM in Node.js marks a pivotal development in JavaScript's modular architecture. However, if you are wondering why it took so long to start supporting ESM, you can find clues in [this issue](https://github.com/nodejs/node/pull/51977?utm_source=webdeveloper.beehiiv.com&utm_medium=newsletter&utm_campaign=native-support-for-cjs-esm-interoperability-begins-in-node-js-22). Finally, with the active participation of the community, Node.js is very likely to achieve perfect support for ESM.

*If you find this helpful, [**please consider subscribing**](https://webdeveloper.beehiiv.com/) to my newsletter for more insights on web development. Thank you for reading!*