DEV Community

loading...

Conditional Exports: Supporting both import and require()

Tierney Cyren
⬡.js - Developer Advocacy at Microsoft. Does: Node.js, Electron, TC39, OpenJS Foundation, npm. Is: twete expert. script kiddie. Stringly typed. Opinions mine. they/he. Demiboy.
・3 min read

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 imported, 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 supporting 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.

Setting up Conditional Exports

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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`
  }
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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.

Consuming a Module that has Conditional Exports

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
Enter fullscreen mode Exit fullscreen mode

To use it in ESM:

import * as lander from "apollo-lunar-module"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

To use it in CommonJS:

const lander = require("apollo-lunar-module")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)