Now that we've both gone over how to make Node.js implicitly and explicitly parse your code as ESM, we can get into some of the more meaty and interesting bits of ESM in Node.js.
To me, one of the most interesting features is Conditional Exports. With conditional exports, you can have a single module export both ESM (allowing it to be import
ed, with all the features of import
that you'd expect) and CommonJS (allowing it to be require()
ed.)
From a broader perspective, this is an amazing tool for transition. Whether you're a maintainer of an open-source module or charged with supporting1 internal end-users on an SDK with a long support cycle, this helps ease the shock of going from CommonJS to ESM, or simply helps you support both use cases for as long as your consumers require.
Let's take the package.json
we used in the Implicit ESM article, and exapand on that:
{
"name": "apollo-lunar-module",
"version": "0.0.1",
"description": "A simple, fast, nice lunar lander module",
"main": "index.js",
"type": "module",
+ "exports": {
+ "import": "./main.js",
+ "require": "./main.cjs"
+ },
"scripts": {
"lint": "standard"
},
"author": "Tierney Cyren <hello@bnb.im> (https://bnb.im/)",
"license": "MIT",
"devDependencies": {
"standard": "^16.0.3"
}
}
You can see we've added the following code:
{
// ...
"exports": {
"import": "./main.js", // doesn't have to be `main`
"require": "./main.cjs" // doesn't have to be `main`
}
// ...
}
You should note that we've got "type": "module"
in our package.json, meaning that .js
will be interpreted as ESM and to use CommonJS in this module, we'll need to use the .cjs
extension.
The utility of having both ESM and CommonJS in the same project becomes apparent here. We're now able to enable both ESM and CommonJS users to consume our package without having to install a different module.
Now, it is worth noting that you can't just copy/paste your code from main.js
into main.cjs
- you'll actually need to make it work as CommonJS code, which probably also means figuring out how to support both use cases in both export styles. If you'd like a solid example of how to do this for realsies, Myles Borins built node-osc and has a rollup configuration that does ESM to CommonJS conversion for this exact use case. Additionally, there are a number of codemods that exist (and I've apparently signed myself up to work on yet another codemod for this) that can help with this.
Thankfully, conditional exports were built in such a way that they're largely invisible to end-users of your module with one caveat.
The caveat: if your end-users are somehow consuming the same module both as ESM and as CommonJS, the instance is of the ESM and CommonJS versions are not the same. Both ESM and CommonJS have been built so the instance is shared, but in the case of using both the instance will not be the same. For most folks this likely won't be problematic for a number of reasons, but it is still a possibility. The most likely way this'll surface is through you using a conditionally exported module one way and a dependency in node_modules
using it a different way.
Outside of that, you'd use modules with conditional exports however you would normally.
Let's take the example of apollo-lunar-module
that we've been using:
npm install apollo-lunar-module
To use it in ESM:
import * as lander from "apollo-lunar-module"
And if we wanted to import (hypothetical) named exports from main.js
with ESM:
import { abortGuidancePanel } from "apollo-lunar-module"
import { plssCondensateContainerAssy } from "apollo-lunar-module"
import { crewLog } from "apollo-lunar-module"
To use it in CommonJS:
const lander = require("apollo-lunar-module")
And, again, if we wanted to consume (hypothetical) named exports, exposed by main.cjs
:
const { abortGuidancePanel } = require("apollo-lunar-module")
const { plssCondensateContainerAssy } = require("apollo-lunar-module")
const { crewLog } = require("apollo-lunar-module")
Either way, as an end-user, conditional exports make support for ESM or for CommonJS effectively invisible to those who are using your modules the other way. This ends up creating a pretty wonderful solution for end-users, enabling maintainers to ensure they're supporting both ESM and CommonJS consumers if they want to.