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!)
module.exports = {
even: n => n % 2 === 0
};
A faithful consumer comes in and uses it in their app.mjs
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.)
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.
// 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] }
// 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?
- Be aware of this :)
- Look into
exports
field in package.json - it might help - Consider wrapping your package in a .mjs that just reexports things under the right names if you could be affected.
Top comments (1)
Bonus: running prettier on the original snippet adds braces around the argument
(n)
which is enough to get lexer to bail out andeven
is no longer a named export :)