Notes/NodeJS

Importing ES Modules and CommonJS in NodeJS

With NodeJS supporting ES Modules un-flagged as of 12.7 and up, the adoption of ES Modules throughout the ecosystem is set to ramp up quickly. For existing CommonJS libraries and projects this means some changes may be needed when updating dependencies going forward. For ESM libraries and projects (new and those that are now aligning with the final specification) there isn't much you need to do, though there are some strategies to be aware of when dependencies don't correctly follow the spec.

The best resource on ES Modules in NodeJS is the esm module documentation. It has a lot to cover however (quite a lot covers package creation and the dual package hazard) which generally isn't of interest to package consumers.

Common Issues

There are some common issues that ESM and CJS files both need to consider.

  • Existing TypeScript definitions are often incorrect, citing the packages being consumed from ES Module that that is compiled in a manner that is not consistent with any specification.
  • Sometimes shipped ES Module code is untested, and breaks in unexpected ways (usually with import issues in my experience). As more ES Module code is shipped, the reverse is likely to become an issue as well.

Importing ES Modules from Common JS

There are a few ways this can be done, all involve the dynamic import.

Dynamic Import in Async Function

For the moment this is the most widely compatible strategy, though it won't suit all circumstances (and may change the public API surface in the case of published packages).

async function main() {
    const avaTsLog = await import("@userfrosting/ts-log-adapter-ava");

    console.log(avaTsLog);
    // [Module] { logAdapter: [Function: logAdapter] }
}

main().catch(function (error) {
    console.error(error);
    process.exit(1);
});

If you are lucky, where the package in consumed is already an async scope (or can itself be awaited by an async scope) in which case the public API surface can remain as is. 🎊

Top Level Dynamic Import

CommonJS does not support top-level await (as the feature its tied to ES Modules) and by design Promises cannot be synchronously waited on, but there is a workaround of sorts provided your only consumer will be NodeJS.

One of the major reasons that NodeJS and the broader JavaScript ecosystem is doing so well is thanks to isomorphic JavaScript and tools which make JavaScript isomorphic. This strategy will lock your package into NodeJS as it depends on native modules that hook into the NodeJS internals. This option is only recommended for packages which are already locked to NodeJS, and as a measure to aid maintenance at that. Road map the changes necessary to adopt other options if going this route.

  1. Install promise-synchronizer@^3.0.0
    npm i promise-synchronizer@^3.0.0
    yarn i promise-synchronizer@^3.0.0
    pnpm i promise-synchronizer@^3.0.0
  2. Wrap import response
    const sync = require("promise-synchronizer");
    // @userfrosting/ts-log-adapter-ava 0.1.0 is an ES Module only package
    const avaTsLog = sync(import("@userfrosting/ts-log-adapter-ava"));
    
    console.log(avaTsLog);
    // [Module] { logAdapter: [Function: logAdapter] }

Apotheosis - Migrate to ES Modules

ES Modules at present provide the best interoperability and compatibility. Given this it may make sense (depending on your circumstances) to migrate your package, eliminating CommonJS from the equation. This is a rather extreme measure to take, so if this is for a published package shipping a version with both CommonJS and ES Module sources is recommended. This will give consumers a grace period where they will have the change to prepare for the cut over (just be sure to test both sources, and account for dual package hazards).

Importing CommonJS From ES Modules

There isn't anything special you need to do here, CommonJS can be imported using the standard static and dynamic import syntax.

That said if for any reason you need to use require (e.g. to explicitly use CommonJS version of a package, or import JSON and native modules) you can create it as follows.

import { createRequire } from "module";
const require = createRequire(import.meta.url);
const data = require("./data.json");
Jordan Mele

Jordan Mele

Site owner