loading...

Node.js 12 supports ES modules. Do you know the difference between CommonJS and ES2015+ Modules?

exacs profile image Carlos Saito Updated on ・5 min read

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


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

// 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:

// 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:

// 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:

// 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):

// 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.

// 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:

// 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

// 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:

// 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):

// 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:

// 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: «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')»

// 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:

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

Import it as named import

// 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:

// 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. 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

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

// 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:

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

Make other libraries ready for CJS import

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

And require them:

// 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

Posted on by:

exacs profile

Carlos Saito

@exacs

Developer. I like JavaScript (specially since ES2015) and Elixir. I don't like python. I don't know why, maybe because I haven't tried it enough.

Discussion

markdown guide
 

Carlos, I'm already using Node 12.16 and it does not process ES6 modules.
Neither you add "type": "module" in the package.json or use ".mjs" for file extensions.
Node support for ECMAScript Modules seems really a myth.
Is it there any full example project what actually runs?

 

Yes. You are right. ECMAScript Modules support in Node.js has been almost a myth. When I've written the post (July 2019), it was announced that Node12 will support them.

However, things have been changed and current status is:

  • Node.js 12 supports it if you run it with the --experimental-modules flag (i.e. node --experimental-modules index.js)
  • In Node.js 13 and 14, ESM is supported without flag by default.

Node.js 14 will be in active in LTS (Long Term Support) by October. It is very probable that ESM will be without the flag by default as announced. However, the same was said for Node.js 12 when I've written this (3 months before Node.js 12 reached LTS)

If you want to be 100% sure about Node.js 14 you have to wait until its release in October.

Docs:

Node.js 12: nodejs.org/docs/latest-v12.x/api/e...
Node.js 14: nodejs.org/docs/latest-v14.x/api/e...

I'll edit the post to reflect this. Thanks for your comment!