Import ESM Modules In CommonJS: A Comprehensive Guide
Hey guys! Ever found yourself banging your head against the wall trying to import an ECMAScript Module (ESM) into your good old CommonJS (CJS) project? You're not alone! This is a common challenge in the JavaScript world, especially as more and more packages are adopting ESM. But fear not! This guide will walk you through everything you need to know to make these two module systems play nicely together. Let's dive in!
Understanding the Module Systems
Before we get into the how, let's quickly recap what ESM and CJS are all about. Understanding their differences is key to solving import issues.
CommonJS (CJS)
CommonJS is the OG module system for Node.js. It's been around for ages and uses require and module.exports to handle dependencies. Think of it as the trusty old workhorse that's been getting things done for years. It's synchronous, meaning it loads modules one after another, which was fine back when Node.js was primarily used on the server.
ECMAScript Modules (ESM)
ESM is the modern, standardized module system introduced with ES6 (ECMAScript 2015). It uses import and export statements and is asynchronous. This means it can load modules in parallel, making it much more efficient for browser-based JavaScript. ESM is the future, and more and more libraries are embracing it.
The main difference that causes headaches is that CommonJS is synchronous, while ESM is asynchronous. You can't directly require an ESM module in CJS because CJS expects modules to be immediately available, while ESM modules need to be loaded asynchronously.
The Problem: Why Can't I Just require() an ESM?
So, you've got this shiny new ESM package you want to use in your existing CJS project. You try the obvious: const someModule = require('that-esm-package'); But then you're greeted with an error message like:
SyntaxError: Unexpected token 'export'
Or maybe:
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /path/to/your/module.mjs
require() of ES modules is not supported.
These errors are Node.js telling you, "Hey, you can't do that!" It's because require() is designed for synchronous loading, and ESM requires asynchronous handling. Direct require() calls simply won't work.
Solutions: Bridging the Gap
Okay, so we know why it doesn't work. Now, let's look at how to make it work. There are several approaches you can take, each with its pros and cons.
1. Dynamic import()
The most straightforward and recommended way to import ESM in CJS is to use the dynamic import() function. This function returns a promise, allowing you to handle the asynchronous nature of ESM. Think of it as saying, "Hey, Node.js, go get this module for me, and let me know when you're done."
Here's how you'd use it:
async function loadEsmModule() {
  try {
    const someModule = await import('that-esm-package');
    // Now you can use someModule
    console.log(someModule.someFunction());
  } catch (err) {
    console.error('Failed to load the ESM module:', err);
  }
}
loadEsmModule();
Explanation:
async function loadEsmModule(): We wrap theimport()call in anasyncfunction because we need to useawait.asyncfunctions allow us to useawaitto pause execution until a promise resolves.await import('that-esm-package'): This is the magic! Theimport()function returns a promise that resolves with the module's exports. Theawaitkeyword pauses the execution of the function until the promise resolves, giving you the module when it's ready.try...catch: It's always a good idea to wrap asynchronous operations in atry...catchblock to handle any potential errors during the import process. If the module fails to load, thecatchblock will catch the error, preventing your application from crashing.
Pros:
- Standard and modern approach.
 - Handles asynchronous loading correctly.
 - Works well with top-level 
awaitin newer Node.js versions. 
Cons:
- Requires 
async/await, which might introduce a bit of complexity if you're not familiar with promises. - You need to handle the promise resolution.
 
2. Using a Transpiler (e.g., Babel)
Another approach is to use a transpiler like Babel to transform your ESM code into CJS code. This allows you to use require() as usual, but it adds a build step to your development process.
Steps:
- 
Install Babel and necessary plugins:
npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/plugin-transform-modules-commonjs - 
Configure Babel:
Create a
.babelrcorbabel.config.jsfile in your project root with the following configuration:// babel.config.js module.exports = { presets: ['@babel/preset-env'], plugins: ['@babel/plugin-transform-modules-commonjs'], }; - 
Transpile your ESM code:
Add a script to your
package.json:{