Dual Packages in Node.js with Conditional Exports

With Node.js 13.7.0 we finally have a standard way to make dual-packages which support both ESM and CJS with backwards compatibility with older Node.js versions, via conditional exports.

Well technically it was possible at one point already, back when Node’s ESM loaded would resolve file extensions automatically, however that capability was removed to be consistent with browser loaders.

Anyway, the way to set this up in a fully backwards compatible way doesn’t seem to be documented anywhere, and definitely isn’t obvious, so here’s a full example of how we can do this.

Example

First create the example module (note the different export values, to make it clear which loads):

node_modules/mod/package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"main": "./lib.js",
"exports": {
".": [
{
"import": "./lib.mjs",
"require": "./lib.js",
"default": "./lib.js"
},
"./lib.js"
]
}
}
node_modules/mod/lib.js
1
exports.format = 'cjs';
node_modules/mod/lib.mjs
1
export const format = 'mjs';

Now create the following 2 files at the root of your project, to test out the conditional loading:

main.js
1
2
3
const {format} = require('mod');

console.log(format);
main.mjs
1
2
3
import {format} from 'mod';

console.log(format);

Now in your shell you can run those main scripts in Node.js 13.7.0+, and see the difference in output.

1
2
3
4
5
6
7
$ node main.js
cjs
(node:25469) ExperimentalWarning: Conditional exports is an experimental feature. This feature could change at any time
$ node main.mjs
(node:25573) ExperimentalWarning: The ESM module loader is experimental.
(node:25573) ExperimentalWarning: Conditional exports is an experimental feature. This feature could change at any time
mjs

Update: Or in Node.js 13.10.0+, which no longer shows the conditional exports warning.

1
2
3
4
5
$ node main.js
cjs
$ node main.mjs
(node:25573) ExperimentalWarning: The ESM module loader is experimental.
mjs

You can also run node main.js in older version of Node, and it will work just fine!

Notes

  • Conditional exports are were still experimental, hence the ExperimentalWarning warning.
    • Update: This is no longer the case in 13.10.0+.
    • Versions 13.7 - 13.9 will show the warning, even when only using CJS (no LTS versions though!).
  • To work in 13.0 - 13.2, you need to use the array in the example with the fallback to the CJS module.
    • These versions did not support the object syntax, but do support the list with fallbacks.
    • Without the fallback it will not be possible to load the module at all in those versions.
  • There are some edge-cases to watch out for with dual packages.
    • These hazards were mostly already possible if different modules depend on incompatible versions.
    • The best way to avoid this is to make your modules stateless and avoid instanceof.

Comments