The npm package sort-package-json
migrated to ES Modules in their v2 release and indirectly broke their named exports which our packages have been consuming. It was a major semver bump, but breaking changes slipped from their changelog and readme radar, so I wanted to explain what actually happened there.
Sub-key imports breaking on ESM
Consider this tiny npm package:
const extras = "bar";
function foo() {
return true;
}
module.exports = foo;
module.exports.foo = foo;
module.exports.extras = extras;
module.exports.default = foo;
In a non-ESM program, you can consume it via a require
:
const foo = require("foo");
// consume the default:
console.log(foo());
// => true ✅
// consume other exports:
console.log(foo.extras);
// => "bar" ✅
Now, we migrate our foo
package to pure ES modules, and we get the following:
const extras = "bar";
function foo() {
return true;
}
export default foo;
export { extras };
The foo
is still a default export, and extras
is a named export.
But now, people can’t consume foo.extras
anymore!
import foo from "foo";
console.log(foo());
// => true ✅
console.log(foo.extras);
// => undefined! 😱
It’s because now, people must import extras
separately:
import foo, { extras } from "foo";
console.log(foo());
// => true ✅
console.log(extras);
// => "bar" ✅
That’s what happened to sort-package-json
; the v2 release inadvertently broke the default sort options array export sortPackageJson.sortOrder
, which in turn, broke our json-sort-cli
after bumping dependencies.
Testing all exports
If we dig deeper, sort-package-json
readme is still implying one can import sortPackageJson.sortOrder
:
My hypothesis is if maintainers had added tests for each export entry during the ESM migration, those tests would have failed and, thus, helped them to remember to update the documentation.
The following test was missing from their test suite from the beginning:
// sort-package-json/tests/main.js
import test from "ava";
import sortPackageJson from "../index.js";
test("main", (t) => {
t.true(Array.isArray(sortPackageJson.sortOrder), "sortOrder is exported");
}); // ❌
Always test each API export.
For example, here’s yours truly testing the detergent.js, checking that the version
is still being exported. If you think it’s redundant, it’s not — many things can go wrong there.
A friendlier readme
A friendly readme has three components:
- an install —
npm install bar;
- an import —
import { foo } from "bar";
— importing everything that’s available! - a consumption example —
const result = foo("bar"); console.log(result);
Let’s check the sort-package-json
readme:
The import statement containing all exports is missing, and the sortOrder
is not mentioned. This readme is like a cigarette counter in the UK — it’s shut, and you have to ask what you want first.
I’ll raise a PR later but let’s edit this on Web Developer tools to how it should be:
At this point, we can notice the confusion between two sortOrder
s — the default order config export and the options setting. I think it was not a wise decision to rename defaultSortOrder
in export { defaultSortOrder as sortOrder }
here. But that’s a next level.
We could also link the text “Pure ES Modules” to where Sindre Sorhus links his, https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c.
Takeaway
- Let’s get rid of default exports from our APIs, especially from npm packages-programs (front-end-related npm packages such as React components are a different story).
- Since the ESM move is a major semver change, it’s a good moment to get rid of default exports during the same release, during the same major semver bump.
- Let’s unit-test all exports — and it’s irrelevant, is program ESM or not. Unit test coverage won’t catch missing export unit tests, so you must review all exported things manually and make sure they all are covered by unit tests.
- Consider developers who are starting to learn the craft — they will struggle if you don’t include all three “ingredients” in your readme: an install, an import and a consumption example.