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 sortOrders — 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.
