Additive changes to your cjs exports are now breaking changes.
Tags: javascript, node, todayilearned
Published: 2022-03-03 21:27:43

> The title is a bit of a clickbait, but it's not false! :D 

So you're providing a module with a very useful utility function in `even.cjs` version 1.0.1 _(improved performance since 1.0.0!)_

```js
module.exports = {
  even: n => n % 2 === 0
};
```

A faithful consumer comes in and uses it in their `app.mjs`
```js
import * as utils from './even.cjs'; 
utils.even(13) // false
```
Looks good. Time for feature creep.

A contributor comes and makes a pull request to add a very helpful field allowing them to look up the version at runtime. _(jQuery has it, it must be good.)_

```js
module.exports = {
  version: '1.1.0',
  even: n => n % 2 === 0
};
```

> Lovely, thanks for the contribution! Published as v1.1.0!

1 hour later:

> Issue: You broke my app! TypeError: utils.even is not a function

## Wait, what?

Yeah, the named export is gone.

If we switch to named imports, the error message is more helpful:
```
import { even } from './even.cjs';
         ^^^^
SyntaxError: Named export 'even' not found. The requested module './even.cjs' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from './even.cjs';
const { even } = pkg;
```

Here's the before and after for all 3 cases.

```js
// import * as utils from './even.cjs';
    [Module: null prototype] {
      default: { even: [Function: even] },
      even: [Function: even]
    }
// import { even } from './even.cjs';
    [Function: even]
// import utils from './even.cjs';
    { even: [Function: even] }
```


```js
// import * as utils from './even.cjs';
    [Module: null prototype] {
      default: { version: '1.1.0', even: [Function: even] }
    }
// import { even } from './even.cjs';
    ERROR
// import utils from './even.cjs';
    { version: '1.1.0', even: [Function: even] }
```

## But why??

When you import modules, you're supposed to get the exports without actually running the code. For ESM modules the limitation to how `export` keyword can be used it's fairly easy and quick to provide a complete and correct list without fully parsing and tracing the execution of the entire file. It's not possible for CommonJS. 

> BTW, that's why it took so long to provide ESM in Node too - figuring out ESM<->CJS crossover was *difficult*.

So to get importing CommonJS to work at all, a good-enough pass through the cjs file is made to try and detect exports without running the code.

It ships with Node.js: https://github.com/nodejs/node/tree/fdf625bae8f8b29a25cbd7c8a5d8220af6292cea/deps/cjs-module-lexer

The readme there offers a few cases where the lexer bails out of listing exports, but I think the tests are better at documenting it precisely.  
This is the exact test explaining what we're seeing here:
https://github.com/nodejs/cjs-module-lexer/blob/main/test/_unit.js#L532

Why do I know all this? Well, I'm trying to provide a consistent cjs import implementation in a different engine's  import implementation. And now I have to fake the differences even though I could provide more reliable exports listings. 

Oh, and guess what - this differs between Node, webpack, parcel etc.

## What should a package maintainer do?

1. Be aware of this :) 
2. Look into `exports` field in package.json - it might help
 - https://webpack.js.org/guides/package-exports/
 - https://nodejs.org/api/packages.html#exports
3. Consider wrapping your package in a .mjs that just reexports things under the right names if you could be affected.