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, soimport _ from 'cjs-pkg'
but notimport { 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()
orawait
-
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
- "Modules" chapter of the book Exploring JS, for more information about differences between CommonJS modules and ES modules like dynamic export/import
- ECMAScript modules, from Node.js official docs
Top comments (4)
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:
--experimental-modules
flag (i.e.node --experimental-modules index.js
)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!
import 'lib';
How does this work?
When does Node.js 12 come out?