2

I have been struggling with packaging an NPM package so that it bundles both CommonJS and ES modules but either are imported with the same absolute module path. Not only the main module.

For example, I might have a where both builds have their or directory within the package

/package
  /modules
    index.js
    submodule.js
  /node
    index.js
    submodule.js

I want to be able to import transparently so that the right module get loaded on either node or browser (or webpack, etc).

Thus, a call to require('package/submodule') would load /package/node/submodule.js and import submodule from 'package/submodule' would load /package/modules/submodule.js

Is that at all possible, especially so that it does not affect consumers and works with older versions of node?

I tried various stuff like conditional exports, type=module but ran into issues where jest + babel-jest tries to import the wrong module.

Tomasz Pluskiewicz
  • 3,622
  • 1
  • 19
  • 42

1 Answers1

2

It's not well-documented or obvious, but it's possible in Node 13.7.0+ using conditional exports not unlike the way you would for the main entry point. Your ES modules will need to use the .mjs file extension though.

node_modules/package/package.json

{
    "main": "./node/index.js",
    "exports": {
        ".": [
            {
                "import": "./modules/index.mjs",
                "require": "./node/index.js",
                "default": "./node/index.js"
            },
            "./node/index.js"
        ],
        "./submodule": [
            {
                "import": "./modules/submodule.mjs",
                "require": "./node/submodule.js",
                "default": "./node/submodule.js"
            },
            "./node/submodule.js"
        ]
    }
}

node_modules/package/modules/index.mjs

export const index = 'mjs-index';

node_modules/package/modules/submodule.mjs

export const submodule = 'mjs-submodule';

node_modules/package/node/index.js

exports.index = 'cjs-index';

node_modules/package/node/submodule.js

exports.submodule = 'cjs-submodule';

Than your package can be used like so:

main.js

const {index} = require('package');
const {submodule} = require('package/submodule');

console.log(index);
console.log(submodule);

main.mjs

import {index} from 'package';
import {submodule} from 'package/submodule';

console.log(index);
console.log(submodule);

Here's the output you would get in Node 13.11.0.

$ node main.js
cjs-index
cjs-submodule
$ node main.mjs
(node:44920) ExperimentalWarning: The ESM module loader is experimental.
mjs-index
mjs-submodule

Node < 13

For older versions of node without submodule support, you will need a file at the submodule path.

For example, you could add this stub:

node_modules/package/submodule.js

modules.exports = require('./node/submodule');
Alexander O'Mara
  • 58,688
  • 18
  • 163
  • 171
  • I would really like a similar solution for node <13 though. – Tomasz Pluskiewicz Mar 29 '20 at 10:46
  • @TomaszPluskiewicz To get `package/submodule` to work in older versions without condition exports, you would have to put `submodule.js` at the root of the package (or a stub that re-exports `node/submodule.js`). That would work for the regular CommonJS loader. – Alexander O'Mara Mar 29 '20 at 19:05
  • That is the point though. I already have the `submodule.js` in the root and it exports modules. – Tomasz Pluskiewicz Mar 30 '20 at 14:28