The ECMAScript modules (in short ES modules) is a JavaScript modules format that uses import
and export
statements:
// An ECMAScript module// import statementimport myFunc from './my-func';// export statementexport myOtherFunc(param) { const result = myFunc(param); // .... return otherResult;}
Starting version 13.2.0, Node.js has stable support of ES modules.
In this post, you'll learn how to enable and use ES modules in Node.js.
Table of Contents
1. Enabling ECMAScript modules in Node.js
The default format of modules in Node.js is the CommonJS.
But Node.js will treat a JavaScript file as an ECMAScript modules format if:
- The module's file extension is
.mjs
- Or the module's nearest parent folder has
{ "type": "module" }
inpackage.json
- Or the argument
--input-type=module
is present, and the module's code is passed as a string using--eval="<module-code>"
argument or fromSTDIN
.
Let's detail into the first (.mjs
extension) and second ({ "type": "module" }
in package.json
) ways.
1.1 .mjs file extension
If you create a JavaScript file with the extension .mjs
, then Node.js will consider the file an ES module.
The following ES module month-from-date.mjs
(note the .mjs
file extension) exports a function monthFromDate()
, which determines the month name of an arbitrary date:
// month-from-date.mjs (ES Module)const MONTHS = ['January', 'February', 'March','April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];export function monthFromDate(date) { if (!(date instanceof Date)) { date = new Date(date); } return MONTHS[date.getMonth()];}
Same way another module month.mjs
uses the ES module import
syntax to import monthFromDate()
function from 'month-from-date.mjs'
module. This module also runs as a CLI script, and prints the month name of the date string passed as an argument:
// month.mjs (ES Module)import { monthFromDate } from './month-from-date.mjs';const dateString = process.argv[2] ?? null;console.log(monthFromDate(dateString));
That's all you need to make Node.js use ES modules!
Let's run month.mjs
module in command line:
node ./month.mjs "2022-02-01"
February
is printed in the terminal.
1.2 { "type": "module" } in package.json
By default .js
files in Node.js are considered CommonJS modules. To make .js
files as ES modules simply set "type"
field as "module"
in the package.json
:
{ "name": "my-app", "version": "1.0.0", "type": "module", // ...}
Now all .js
files inside the folder containing such package.json
execute as ECMAScript modules.
Regarding the month modules, let's rename month-from-date.mjs
to month-from-date.js
and month.mjs
to month.js
(while still keeping the import
and export
syntax), set "type"
field as "module"
in the package.json
, and Node.js is going to execute these modules as ECMAScript ones.
node ./month.js "2022-03-01"
March
is printed in the terminal. Node.js ran successfully the ES modules month.mjs
and month-from-date.mjs
.
2. Importing ECMAScript modules
The specifier is the string literal having the path from where to import the module.
In the example below 'path'
is a specifier:
// 'path' is the specifierimport module from 'path';
There are 3 kinds of specifiers in Node.js: relative, bare and absolute.
2.1 Relative specifier
Importing a module using a relative specifier resolves the path of the imported module relative to the current (importing) module location.
Relative specifiers usually start with '.'
, '..'
, or './'
:
// Relative specifiers:import module1 from './module1.js';import module2 from '../folder/module2.mjs';
When using relative specifiers indicating the file extension (.js
, .mjs
, etc.) is obligatory.
2.2 Bare specifier
A bare specifier starts with a module name (doesn't start with '.'
, './'
, '..'
, '/'
), and imports modules from node_modules
or the built-in Node.js modules.
For example, if you've installed the lodash-es
package in node_modules
, then you can access that module using a bare specifier:
// Bare specifiers:import lodash from 'lodash-es';import intersection from 'lodash-es/intersection';
Using bare specifiers you can also import the Node.js built-in modules:
import fs from 'fs';
2.3 Absolute specifier
An absolute specifier imports modules using an absolute path:
// Absolute specifier:import module from 'file:///usr/opt/module.js';
Note the presence of the file://
prefix in the absolute specifiers.
3. Dynamic import of modules
The default importing mechanism of ES modules always evaluates the imported module: no matter if you use the module or not.
If you want to import the modules dynamically, then invoke the asynchornous function import('./path-to-module')
:
async function loadModule() { const { default: defaultComponent, component1 } = await import('./path-to-module'); // ...}loadModule();
import('./path-to-module')
loads asynchronously the module. import()
returns a promise that resolves to the imported module components:
default
property as the default import- the named imports as properties with the same names
For example, let's improve month.js
script to load month-from-date.js
module only when the user sets the date argument:
// month.js (ES Module)const dateString = process.argv[2] ?? null;if (dateString === null) { console.log('Please indicate date argument');} else { (async function() { const { monthFromDate } = await import('./month-from-date.js'); console.log(monthFromDate(dateString)); })();}
const { monthFromDate } = await import('./month-from-date.mjs')
loads the module dynamically, and assigns the named export monthFromDate
to a variable with the same name.
node ./month.js "2022-04-01"
April
is logged in the terminal.
4. Mixing module formats
You can be in a situation when you need to import a CommonJS module from an ES module, and vice-versa.
Fortunately, Node.js allows an ES module to include a CommonJS module as a default import:
// ES moduleimport defaultComponent from './module.commonjs.js';// ...
When imported in an ES module, the module.exports
of the CommonJS module becomes the default import. The named imports from a CommonJS module, however, is not supported.
The require()
function of the CommonJS format cannot import an ES module. Instead, you can use the async function import()
inside CommonJS to load an ES module:
// CommonJS moduleasync function loadESModule() { const { default: defaultComponent, component1 } = await import('./module.es.mjs'); // ...}loadESModule();
I recommend as much as possible to avoid mixing module formats because it is confusing.
5. ECMAScript modules and Node.js environment
Inside the ECMAScript module scope are not available the CommonJS specific variables like:
require()
exports
module.exports
__dirname
__filename
However, you can use import.meta.url
to determine the absolute path of the current module:
// An ES module at path "/usr/opt/module.mjs"console.log(import.meta.url); // "file:///usr/opt/module.mjs"
6. Conclusion
Node.js supports ES modules when the module extension is .mjs
, or the nearest folder of the module has a package.json
containing { βtypeβ: βmoduleβ }
.
Then you can import modules using:
- Relative path, e.g.
import module from './module.js'
- Absolute path, e.g.
import module from 'file:///abs/path/module.js'
- Modules installed in
node_modules
, e.g.import lodash from 'lodash-es'
- Or built-in Node.js modules like
import fs from 'fs'
.
You can import dynamically a module using import('./path-to-module')
syntax.
While not desirable, but sometimes necessary, you can import a CommonJS module from an ES module using the import defaultImport from './common.js'
statement. The module.exports
of the CommonJS becomes the default import defaultImport
inside the ES module.
How to write quality ECMAScript modules? To find out, follow my post 4 Best Practices to Write Quality JavaScript Modules.
Do you think Node.js should migrate completely to ES modules format?