Frontend Infrastructure: ESModules & CommonJS
We took a closer look at the past when we used JavaScript as simple scripts and when developers were in search for a module system trying to use JavaScript syntax and features (Closures and Scope, IIFEs, Namespaces) to come up with module patterns.
Now let's talking about real module systems: CommonJS and ESModules.
Among other experiments (AMD, UMD, etc), CommonJS was defined and implemented in NodeJS as a standard module system. It has a very simplistic and easy-to-read syntax.
Exporting values using module.exports
syntax: named exports
// ModuleX.js
module.exports.A = 1;
And importing values from other modules using the require
syntax:
const { A } = require('./ModuleX.js');
A; // 1
Exporting values using module.exports
syntax: default exports
// ModuleX.js
module.exports = 1;
And importing values from other modules using the require
syntax:
const A = require('./ModuleX.js');
A; // 1
Now, ESModule is the standard module system for JavaScript. The syntax's pretty simple as CJS. Let's take a look at it.
Exporting values using the export
syntax for named exports
// A.mjs
export const A = 1;
And importing values from other modules using the import
syntax:
import { A } from './A.mjs';
A; // 1
ESM vs CJS: TLDR
A TLDR on how ESM and CJS work differently and limitations.
ESM: ESModules
- It only imports modules using the
import
syntax. You can't userequire
syntax. It would throw an errorReferenceError: require is not defined in ES module scope, you can use import instead
. - Bundlers don’t know how to work with ESM modules that use the
require
syntax. - CJS is the default module system. To change it to ESM, set the
type
frompackage.json
tomodule
. - If module is using only named exports (no default export) and the other module is defaul importing the module, it throws an error
SyntaxError: The requested module './A.mjs' does not provide an export named 'default'
. The module exporting should export default too or the second module should named import the first one.
CJS: CommonJS
- It only requires modules using the
require
syntax. You can't use theimport
syntax. It throws an exceptionSyntaxError: Cannot use import statement outside a module
. - Can’t use ESM in CJS. It throws an exception: Must use import to load an ES Module.
- In Node, CJS is the default module system. By default, the
package.json
usescommonjs
as thetype
's value. - The
require
syntax is synchronous: there's no callback or promise.
Interoperability between ESM and CJS
How it works to import from one module system to another, the interoperability, limitations, and so on.
Importing CJS from ESM
ESM can default import CJS modules even when the CJS module has no default exports (only named exports). It treats the module as an object, so it need to access its "attribute".
// ModuleA.js
module.exports.A = 1;
// ModuleB.js
import ModuleA from './ModuleA.js';
ModuleA.A; // 1
ESM can named import CJS modules.
// ModuleA.js
module.exports.A = 1;
// ModuleB.js
import { A } from './ModuleA.js';
A; // 1
Importing ESM from CJS
CJS can't import/require ESM. It throws an exception: Must use import to load an ES Module
. If trying to use the import
syntax, it will throw another exception: SyntaxError: Cannot use import statement outside a module
.
Migration: from CommonJS to ESModule
What are common patterns for migration from CommonJS to ESModule? We need to think about 4 main things: named export, default export, named import, and default import.
We also have some caveats through this process, but most of the time, the focus is these 4 things. Let's see the patterns.
Named exports
From CommonJS:
const a = 1;
const b = 2;
const c = 3;
module.exports = {
a,
b,
c,
};
To ESModule:
export const a = 1;
export const b = 2;
export const c = 3;
Default exports
From CommonJS:
const a = 1;
module.exports = a;
To ESModule:
const a = 1;
export default a;
Named imports
From CommonJS:
const { a, b, c } = require('./module');
To ESModule:
import { a, b, c } from './module';
Default imports
From CommonJS:
const a = require('./module);
To ESModule:
import a from './module';
Caveats: imports and object destructuring
From CommonJS:
const {
a: { b },
} = require('./module');
From ESModule:
// import { a: { b } } from './module'; --> Syntax Error
import { a } from './module';
const { b } = a;
Final words
With the development of JavaScript, we end up with two main module systems: ESModules (import syntax) and CommonJS (require syntax). Even though ECMAScript standard module system is ESModules, we currently have a lot of apps, libraries, and systems written using CommonJS.
We still live in a world where the two module systems need to work together. In this piece we've learned the differences between the two, how to make them interop, and understand compatibility limitations between them.