Node.js 12 supports ES modules. Do you know the difference between CommonJS and ES2015+ Modules?
Tags: javascript, node, modules
Published: 2019-07-15 11:44:06

If you are a Node.js developer either by writing Node.js apps or libraries, you probably know that Node.js 12 supports ECMAScript standard modules! ~~(the feature will probably be stable without any experimental flag starting from LTS release this october)~~. **EDIT: Node.js 12 has not dropped the need of the `--experimental-modules` flag. Read more in the [official documentation](https://nodejs.org/docs/latest-v12.x/api/esm.html#esm_enabling)**

---

Do you know what are the differences between CommonJS and ES modules?

## Pre Node.js 12. CommonJS (a.k.a. CJS)

### Export and import

We have two ways of exporting, named and default exports

```javascript
// commonjs/named.js
module.exports.sayHello = function sayHello (name) { return `Hello ${name}` }

// commonjs/default.js
module.exports = function sayHello (name) { return `Hello ${name}` }
```

And two ways of importing:

```javascript
// index.js
// Named import without changing the name
const { sayHello } = require('./commonjs/named')

// Named import changing the name
const { sayHello: say2 } = require('./commonjs/named')

// Default import
const sayDefault = require('./commonjs/default')

console.log(sayHello('World'))
console.log(say2('World'))
console.log(sayDefault('World'))
```

There are some alternatives in both exporting and importing like those but they are equivalent:

```javascript
// Named import
const say2 = require('./commonjs/named').sayHello

// Named export
module.exports = {
  sayHello: function sayHello (name) {
    return `Hello ${name}`
  }
}
```

### Bare-paths. Module resolution in Node.js

**`require` in Node.js** accepts a *bare path* so we can declare/export libraries from a `node_modules` directory:

```javascript
// node_modules/my-lib/package.json
{ "main": "index.js" }

// node_modules/my-lib/index.js
module.exports.sayHello = function sayHello (name) { return `Hello ${name}` }
```

And import them (Node.js resolves `my-lib` to `./node_modules/my-lib/index.js`):

```javascript
// index.js
const say3 = require('my-lib')
console.log(say3('World'))
```

## The future. ES Modules (a.k.a. ESM)

### Export and import

Like in CommonJS, there are two ways of exporting: named and default.

```javascript
// esm/named.js
export function sayHello (name) { return `Hello ${name}` }

// esm/default.js
export default function sayHello (name) { return `Hello ${name}` }
```

And two ways of importing:

```javascript
// index2.js
// Named import without changing the name
import { sayHello } from './esm/named.js'

// Named import changing the name
import { sayHello as say2 } from './esm/named.js'

// Default import
import sayDefault from './esm/default.js'

console.log(sayHello('World'))
console.log(say2('World'))
console.log(sayDefault('World'))
```

Note that the following "alternatives" exist but are not equivalent to a named export. **Do not use them as equivalent to named exports**
 
```javascript
// This is NOT a named export!!
export default {
  sayHello: function (name) {
    return `Hello ${name}`
  }
}
 
// This will not work with the above!
import { sayHello } from './esm/variation.js'

// This works but is NOT a named import
import say from './esm/variation.js'
const { sayHello } = say
```

### Bare paths. Module name resolution

Node.js 12 resolves *bare paths* properly:

```javascript
// node_modules/my-esm-lib/package.json
{ "main": "index.js" }

// node_modules/my-esm-lib/index.js
export default function sayHello (name) { return `Hello ${name}` }
```

And import them (Node.js resolves `my-esm-lib` to `./node_modules/my-esm-lib/index.js`):

```javascript
// index2.js
import say3 from 'my-esm-lib'
console.log(say3('World'))
```

# Interoperability

## Import a CJS module into a ESM project

The dependencies are still in CommonJS:

```javascript
// commonjs/named.js
module.exports.sayHello = function sayHello (name) { return `Hello ${name}` }

// commonjs/default.js
module.exports = function sayHello (name) { return `Hello ${name}` }
```

So you need to know what happens when you ~~`require`~~ `import` them to a ESM file.

All the `module.exports` object in CJS will be converted to a **single ESM default export**. You cannot use ESM named exports when importing CommonJS modules.

All the `module.exports` object in CJS will be converted to a **single ESM default export**. You cannot use ESM named exports when importing CommonJS modules.

> 📝 From the [Node.js roadmap plans](https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md): «Status quo is current `--experimental-modules` behavior: `import` only the CommonJS default export, so `import _ from 'cjs-pkg'` but not `import { shuffle } from 'cjs-pkg')`»

```javascript
// index.mjs
// "Fake named import" without changing the name
import named from './commonjs/named.js'
const { sayHello } = named

// "Fake named import" changing the name
import named2 from './commonjs/named.js'
const { sayHello: say2 } = named2

// Default import
import sayDefault from './commonjs/default.js'

console.log(sayHello('World'))
console.log(say2('World'))
console.log(sayDefault('World'))
```

### Alternative: make an intermediate module.

Enable real ESM named imports by creating an intermediate module:

```javascript
// bridge/named.mjs
import named from '../commonjs/named.js'
export const sayHello = named.sayHello
```

Import it as named import

```javascript
// index.mjs (with bridged modules)
// Named import without changing the name
import { sayHello } from './bridge/named.mjs'

// Named import changing the name
import { sayHello as say2 } from './bridge/named.mjs'
```

## Import a ESM module into a CJS project

Your dependencies are now in ESM:

```javascript
// esm/named.mjs
export function sayHello (name) { return `Hello ${name}` }

// esm/default.mjs
export default function sayHello (name) { return `Hello ${name}` }
```

To `require` them from a CommonJS file, you can use the [npm package `esm`](https://www.npmjs.com/package/esm). This "special" require returns everything as an **object of named imports**. The ESM default export becomes a named import called `.default` on the returned object

```javascript
const esmRequire = require('esm')(module)

// Named import without changing the name
const named = esmRequire('./esm/named.mjs')
const { sayHello } = named

// Named import changing the name
const { sayHello: say2 } = named

// "ESM default export" becomes a named import called "default"
const sayDefault = esmRequire('./esm/default.mjs').default

console.log(sayHello('World'))
console.log(say2('World'))
console.log(sayDefault('World'))
```

If you don't want to use an external package, use the `import()` operator. Notes:

-  `import()` returns a Promise. So you need `.then()` or `await`
-  `import()` returns everything as an object of named imports. To access the default-exported thing, you need to access the property `.default` on the returned object

> ℹ️ Read more about `import()` [here](https://2ality.com/2017/01/import-operator.html)

```javascript
// index.js
;(async function () {
  // Named import without changing the name
  const named = await import('./esm/named.mjs')
  const { sayHello } = named

  // Named import changing the name
  const { sayHello: say2 } = named

  // Default import
  const sayDefault = (await import('./esm/default.mjs')).default

  console.log(sayHello('World'))
  console.log(say2('World'))
  console.log(sayDefault('World'))
})()
```

### Alternative: make intermediate modules using the `esm` package

Enable CJS default export:

```javascript
// bridge2/default.js
require = require('esm')(module)
module.exports = require('../esm/default.mjs').default
```

Make other libraries ready for CJS import

```javascript
// bridge2/named.js
require = require('esm')(module)
module.exports = require('../esm/named.mjs')
```

And require them:

```javascript
// Named import without changing the name
const named = require('./bridge2/named.mjs')
const { sayHello } = named

// Named import changing the name
const { sayHello: say2 } = named

// Default import
const sayDefault = require('./bridge2/default.mjs')
```

---

# That's it!

The next post will be about how to prepare your Node.js apps and libraries to support ES modules as soon as possible!

# Further reading

- ["Modules" chapter of the book *Exploring JS*](https://exploringjs.com/impatient-js/ch_modules.html), for more information about differences between CommonJS modules and ES modules like dynamic export/import
- [ECMAScript modules](https://nodejs.org/api/esm.html), from Node.js official docs