Announcing core Node.js support for ECMAScript modules

Node.js
5 min readNov 21, 2019

--

This article was contributed by the Node.js Module Team.

Node.js 13.2.0 ships support for ECMAScript modules, known for their import and export statements. This support was previously behind the--experimental-module flag, which is no longer required; however the implementation remains experimental and subject to change.

Enabling
Node.js will treat the following as ES modules when passed to node as the initial input, or when referenced by import:

  • Files ending in .mjs.
  • Files ending in .js, or extensionless files, when the nearest parent package.json file contains a top-level field “type” with a value of “module”.
  • Strings passed in as an argument to —-eval or piped to node via STDIN, with the flag —-input-type=module.

Node.js will treat as CommonJS all other forms of input, such as .js files where the nearest parent package.json file contains no top-level “type” field, or string input without the flag --input-type. This behavior is to preserve backward compatibility. However, now that Node.js supports both CommonJS and ES modules, it is best to be explicit whenever possible.

Node.js will treat the following as CommonJS when passed to node as the initial input, or when referenced by import:

  • Files ending in .cjs.
  • Files ending in .js, or extensionless files, when the nearest parent package.json file contains a top-level field “type” with a value of “commonjs”.
  • Strings passed in as an argument to --eval or piped to node via STDIN, with the flag --input-type=commonjs.

For more information, see Package Scope and File Extensions and--input-type flag.

import and export syntax
Within ES module files and string input, import statements can reference JavaScript files. Files can be referenced as:

  • Relative URLs (‘./file.mjs’)
  • Absolute file:// URLs (‘file:///opt/app/file.mjs’)
  • Package names (‘es-module-package’)
  • Paths within packages (‘es-module-package/lib/file.mjs’)

import statements that reference ES module files can specify the default export (import _ from ‘es-module-package’), named exports (import { shuffle } from ‘es-module-package’) and namespace exports (import * as fs from ‘fs’). All Node.js built-in packages like fs and path support all three types of exports.

import statements that reference CommonJS files (all current JavaScript code written for Node.js, using require and module.exports) can only use the CommonJS default export (import _ from ‘commonjs-package’).

import statements that reference other types of files such as JSON or Wasm are experimental and require the--experimental-json-modules or--experimental-wasm-modules flags. The non-experimental module.createRequire can be used to import JSON or native addons.

export statements in ES module files can specify both default and named exports for import statements to reference.

Dynamic import() expressions can be used to import ES modules from either CommonJS or ES module files. Note that import() returns a promise.

import.meta.url provides the absolute URL of the current ES module file.

Files and the package.json “type” field

Add “type”: “module” to the package.json for your project, and Node.js will treat all .js files in your project as ES modules.

If some of your project’s files use CommonJS and you can’t convert your entire project all at once, you can either rename those files to use the .cjs extension or put them in a subfolder containing apackage.json with { “type”: “commonjs” }, under which all .js files are treated as CommonJS.

For any file that Node.js tries to load, it will look for a package.json in that file’s folder, then that file’s parent folder and so on upwards until it reaches the root of the volume. This is similar to how Babel searches for.babelrc files. This new approach allows Node.js to use package.json for package-level metadata and configuration, similar to how it is already used by Babel and other tools.

We encourage all packages to include the“type” field, even if the “type” is “commonjs”.

Package entry points and the package.json “exports” field

There are now two fields that can define entry points for a package: “main” and“exports”. The “main” field is supported in all versions of Node.js, but its capabilities are limited: it only defines the main entry point of the package. A new package.json field “exports” can also define the main entry point, along with subpaths. It also provides encapsulation, where only the paths explicitly defined in “exports” are available for importing. “exports” applies to both CommonJS and ES module packages, whether used via require or import.

This allows statements like import ‘pkg/feature’ to map to a path like ‘./node_modules/pkg/esm/feature.js'. It also causes an error to be thrown for statements like import ‘pkg/esm/feature.js’ unless that path is defined in “exports”.

An experimental feature, conditional exports, would add support for an exported path, including the package root, to map to different files in different environments. This would allow a package to provide CommonJS sources for require(‘pkg’) and ES module sources for import ‘pkg’, though writing such a package is not without hazards. Conditional exports can be enabled via the—-experimental-conditional-exports flag.

Common Gotchas

Mandatory file extensions
A file extension must be provided when using the import keyword. Directory indexes (e.g. ‘./startup/index.js’) must also be fully specified.

This behavior matches how import behaves in browser environments, assuming a typically configured server.

No require, exports, module.exports,__filename,__dirname

These CommonJS variables are not available in ES modules.

require can be imported into an ES module using module.createRequire().

Equivalents of __filename and __dirname can be created inside of each file via import.meta.url.

Writing packages

At the moment, we recommend that packages are written using either entirely CommonJS or entirely ES module syntax for the JavaScript sources intended for Node.js. The modules team is working on better support for “dual” packages, that provide CommonJS sources for package consumers using require and ES module sources for package consumers using import. The current experimental feature covering this use case is conditional exports, and the team hopes to ship that or an alternative by the end of January 2020 if not earlier. See recommended patterns in Dual CommonJS/ES Module Packages.

Works in progress

  • Loaders. Work is ongoing for an API to allow for custom loaders that can support use cases like runtime transpilation, rewriting specifiers (package or file paths) and code instrumentation. The —-experimental-loader API will still change considerably before this is unflagged.
  • Dual CommonJS/ES module packages. We want to provide a standard way for package authors to publish a package that can be both required into CommonJS or imported into an ES module. See Dual CommonJS/ES Module Packages. The plan is for this to be finalized by the end of January 2020 if not earlier.

That’s it! We hope this new support for ECMAScript modules brings Node.js closer to JavaScript standards and increases opportunities for ecosystem-wide compatibility. The modules team’s work is public at https://github.com/nodejs/modules.

--

--

Node.js

Node.js is a collaborative open source project dedicated to building and supporting the Node.js platform. https://nodejs.org/en/