Native ES Modules in NodeJS: Status And Future Directions, Part I

  1. The obvious one is CommonJS, which is the current module system used by NodeJS (the one that uses requireandmodule.exports). CommonJS already is a module system for NodeJS, and ES Modules has to learn to live side by side and interoperate with it. The whole thing is complicated by the fact that ES Module loading is asynchronous in nature, while CommonJS is synchronous by nature.
  2. The less obvious complication is browser compatibility. The NodeJS ecosystem initially started as a separate ecosystem, but since Browserify and webpack enabled CommonJS modules to be used in the browser, the scene of isomorphic code, which runs both under NodeJS and the browser, has exploded.
  3. The third complication, ironically enough, is babel and webpack (and any other bundler like rollup, browserify, and parcel). These pair of tools enabled both NodeJS and browser code to use ES Modules without native support. Thus, lots of code out there in the wild uses ES Modules as specified by babel/webpack, but which is different from the final way. Why different? Because babel/webpack load ES Modules synchronously, while the specification of ES Modules specify asynchronous loading.

Native ES Modules in NodeJS

To create an ES Module, just create a file with the mjs extension that includes the module code. For example:

mjs, or Michael Jackson Scripts
  • A field in the package.json that defines whether all the files in this package.json are scripts or modules.
  • A header like "use module" in the top of the file, similar to "use strict".
  • The existence of an export statement means that the file is an ES Module, even if it is a dummy export default {}.
  • A different extension for the file.
  1. A file is ESM if and only if the extension is “mjs”
  2. A file is CJS if and only if the extension is “js”
  3. Only ESM is allowed to use export/import statements
  4. Only CJS is allowed to use import CJS using require
mjs and js cannot import one another?

Interoperability between CJS and ESM

This leads us to two additional rules, which I call The Rules of Interoperability:

  1. CJS can import ESM, but only using await import()
  2. ESM can import CJS using the import statement, but only a default import
Bridging the two module systems

Migrating Applications from CJS to ESM

The Rules of ESM Modules and The Rules of Interoperability provide a clear path to understanding how to migrate an application from CJS to ESM. Fortunately, the process is pretty simple, mostly technical, and can be incremental:

  1. Rename its extension to mjs.
  2. Change the exports from CJS ( module.exports) to ESM (export statement).
  3. Change all require-s in it to import statements with default import.
  4. If the require is dynamic, change it to await import(). This is the only non-technical transformation, as the function it is in must now be asynchronous, i.e. using async, promises, or callbacks.
  5. If the module uses __dirname (or __filename) , use the following “polyfill”
    const __dirname = path.dirname(
    new url.URL(import.meta.url).pathname)
    (if you want to know more about import.meta, see here).
  6. Change all require-s in other files that reference it to import.

Bare Imports

What about libraries? Up to now we explored application files importing one another. The ES Module spec says that if the path to the file is a URL, or starts with ".", then the path to the file is according to that path. But libraries in NodeJS are require-ed using a path like "lodash".

npm is the biggest package repository in the world (by far!)

Dual-Mode Packages

Which brings us to the question of writing npm packages that support ES Modules. How does one go about migrating a CJS package so that it can be used as an ES Module? Theoretically, we can use the same steps as migrating an application from CJS to ESM. But once we do that, the package can only be used as an ES Module, and CJS “clients” cannot use it anymore.

  1. require-ing a file with no extension resolves to a .js file
  2. Importing a file with no extension resolves first to a .mjs file (and only if not found, to a .js file)
  3. Resolving bare imports is the same in CJS and MJS (except for which extension is used)
  1. Convert the code to use ESM files (same steps as the steps in the application migration).
  2. The entry point in the package.json should be without extension, e.g. index and not index.mjs.
  3. Write a build step that uses babel to transpile the mjs to js.

Summary

The three rules below encapsulate all you need to know about ES Modules in NodeJS, and all you need to know to migrate your apps from CommonJS to ES Modules, and to write dual-mode packages that work both as CommonJS packages and as ES Module packages.

The Rules

The Rules of ES Modules

  1. A file is ESM if and only if the extension is “mjs”
  2. A file is CJS if and only if the extension is “js”
  3. Only ESM is allowed to use export/import statements
  4. Only CJS is allowed to use import CJS using require

The Rules of Interoperability

  1. CJS can import ESM, but only using await import()
  2. ESM can import CJS using the import statement, but only a default import

The Rules of Migration

  1. require-ing a file with no extension resolves to a .js file.
  2. Importing a file with no extension resolves first to a .mjs file, and only if not found, to a .js file.
  3. Resolving bare imports is the same in CJS and MJS, except for which extension is used.

Visual Testing

I work at Applitools, and what we do is pretty amazing too — we build tools that enable you to visually test your frontend code — meaning automatically test that your components still look like they should, and haven’t gone all whacky due to CSS, HTML, or JS changes. We’ve just finished working on an amazing tool that can visually test all the components in a React Storybook, without writing a single line of code.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Gil Tayar

Gil Tayar

1K Followers

software developer ⚜ dad ⚜ nodejs and javascript fan ⚜ architect & evangelist at applitools ⚜ test all software! ⚜ and lots of love ❤️