The Current State of Implementation and Planning for ESModules

Foreword by @MylesBorins:

Modules were first standardized in ECMAScript 6 in 2015. As of December 2017 you can now use ESModules (ESM) in 3 out of 4 of the major browsers. Node.js has traditionally shipped an implementation of Common.js (CJS), you use it in your Node.js code today via require().

There are vast differences between the two module systems that make it quite difficult to utilize Common.js code in an ESModule and vice versa. For more information about the lower level details please watch my Node.js Interactive talk on modules.

ESModules have been shipping in Node.js since version 8.9.0, you need to use the flag —-experimental-modules to utilize it. Parts of our implementation have been controversial and received push back from the community. As the implementation of ESM has a number of moving parts, including how we plan to support interoperability, the Node.js TSC thought it would be a good idea to get our current design decisions on paper to share with the general community.

As you will see below the Node.js TSC does not have consensus on the entire implementation, but we do have consensus on our intent. Most important to point out is our commitment “to having the Node.js and Web platform as first class runtimes for modules.” We envision a future where after installing a module, developers will be able to run the code in Node.js or the Browser without requiring a build step.

Thanks for taking the time to read this document, please feel free to reach out in the comments or on Twitter if you have any questions.

Best,
Myles Borins

To Our Community,

The Node.js TSC has met to discuss our implementation and planning for ESModules and wanted to share our thoughts with the community. As a first step we came to consensus on a list of high level intentions for the implementation:

  • We are committed to shipping ESM
  • We are committed to ESM being first class in Node.js
  • We are committed to having the Node.js and Web platform as first class runtimes for modules. Modules installed via npm should be able to run after installation without requiring a build step.
  • We are committed to supporting our current users and offering migration paths as necessary. This can be through interoperability or APIs to improve the developer experience of working between module systems.

Regarding the implementation, there are a number of details where we currently have consensus:

  1. sync vs async loader
  • We are shipping async loader

2. support for hooks

3. resolution algorithm (e.g. bare imports)

  • Replicate CJS resolution algorithm
  • We are willing to discuss changes to our resolution algorithm to align with web platform

4. named exports for core modules

  • We are working on an implementation for how to do this
  • This is a priority
  • No clear path to if core modules will eventually be rewritten in ESM, not a priority
  • Our solution for core modules will most likely not work for ecosystem modules
  • Our goal is to enable individuals to use Node’s API surface as if they were ESM

5. lexically scoped variables (eg __filename vs import.meta)

  • Import.meta is coming in V8 6.4. We already have a tracking issue open for the update https://github.com/nodejs/node/pull/17489
  • We plan to use import.meta for variables that currently are lexically scoped in CJS such as __filename, __dirname, and require.
  • We will implement import.meta.url
  • The variables that are exposed on import.meta are open to debate
  • An early implementation of import.meta can be found at https://github.com/targos/node/tree/import-meta

There appear to be two places in the implementation where we currently do not have consensus:

  1. Transparent Interoperability
  • Some parties believe that import should work for CommonJS modules via the .js extension: This would reserve .js for CJS and utilize .mjs for ESM
  • Some parties want to remove transparent interoperability: They would rather see an API like import.meta.require or import require from ‘nodejs’ used for bringing CJS modules into ESM
  • There are a variety of valid reasons for both approaches and we require more time and research to reach consensus
  • The current implementation supports transparent interop
  • An alternative approach disabling transparent interop is being prepared

2. .mjs extension and .js being reserved for CJS

  • This decision is entirely dependent on how interoperability is implemented
  • Node.js will require .mjs regardless of interoperability: Supporting .mjs as a way to be explicit about ESM does not preclude using .js for the same thing; .mjs and .js are not mutually exclusive. Supporting the use of .mjs is necessary to allow the node binary to run ESM files without a flag, e.g. node my-thing.mjs vs node --use-module my-thing.js

The current plan will involve opening a handful of pull requests to discuss the viability of different approaches. Some of these will be more controversial than others. None of these are guaranteed to land, be included in the final implementation, or even be technically feasible. They are what appear to be the best options based on our current knowledge. The PRs include:

  1. import.meta for lexically scoped variables + import.meta.require for interoperability in ESM
  • Without a lexically scoped require there is no alternative in ESM for CJS modules aside from current transparent interop implementation
  • Import.meta.require can also be implemented as `import require from nodejs` or some other similar namespace. This is a harder / larger change to make but may be a better approach as it would throw during loading phase rather than execution phase in the browser

2. Disable transparent interoperability for import of CJS files

  • This is dependent on another method of requiring CJS modules into ESM (see above)
  • This could result in a different module execution order for CJS as ESM executes bottom up: This could prove to be a non starter for this change

3. Dynamic import() support in ESM + CJS

  • This will offer an asynchronous way to dynamically load ESM dependencies
  • It returns a promise, which makes it not exactly a full solution for interoperability in CJS
  • This has recently landed in master via https://github.com/nodejs/node/pull/15713
  • If you would like to test this out today node needs to be run with two flags node —-experimental-modules —-harmony-dynamic-import
  • Dynamic import will inherit the behavior of static import in regards to resolution and interoperability

As mentioned above, the Node.js project is dedicated to offering First-Class support for ESM + CJS in our ecosystem. This write up represents our current best understanding of the problem space and is subject to change as we do more research. While we are working on these problems the team will be collaborating with stakeholders from V8, the web platform, and npm to ensure that the decisions we make will work towards our intended goal.

Sincerely,

The Node.js Technical Steering Committee