The packages nobody knows they install
Name
invisible-packages — the packages nobody knows they install
Synopsis
Some of my smallest packages are installed billions of times a year, almost always by something else. Here is what that asks of me.
Filed
· 1,265 words · 10 min
Description
Last month, escalade was downloaded 929 million times.
You have almost certainly installed it. You have almost certainly never typed its name. It walks up from a directory, one parent at a time, and asks a callback whether it found what it wanted. That is the whole package. It is in your node_modules because yargs needs it, and update-browserslist-db needs it, and a lot of things need one of those.
Most of what I publish lives like this. The packages people know me for are not the packages people install.
#The numbers
Here is one month of downloads for a few of them, from the npm API:
escalade: 929 million. It finds a file in a parent directory.clsx: 573 million. It builds aclassNamestring.kleur: 492 million. It colors terminal output.dequal: 462 million. It checks deep equality.mrmime: 201 million. It looks up a MIME type.resolve.exports: 182 million. It reads theexportsfield of apackage.json.totalist: 160 million. It lists a directory, recursively.empathic: 158 million. It finds paths. More on that one in its own post.mri: 107 million. It parsesargv.
Across everything I track on this site, September came to about 3.6 billion downloads. I did not set out to do that. I set out to write small things I needed, and then other people needed them too, and then the people who needed those things got popular.
#How you got them
Almost nobody installs these on purpose. They arrive through a chain.
escaladecomes in throughyargsandupdate-browserslist-db.kleurcomes in throughprompts.clsxcomes in throughsvelteandastro.dequalcomes in throughswr.mrmimeandtotalistcome in throughsirv, which comes in through@sveltejs/kitand@vitest/ui.dsetandmrmimecome in throughastrotoo.empathiccomes in through@babel/core.
None of those projects asked me, and none of them had to. That is the deal with open source, and it is a good deal. It does mean that my audience is mostly other maintainers, and their audience is everyone.
It also means a download count says very little about what people use. It says what people's lockfiles resolve to.
#The oldest version wins
kleur shows this clearly. Last week, its most-downloaded version was 3.0.3, with about 65 million downloads. Version 4.1.5 had about 49 million.
3.0.3 was published in April 2019. Version 4 came out a year later. The old one keeps winning because prompts still asks for ^3.0.3, and prompts is everywhere. I can ship whatever I want in kleur@4. A large part of the world will not see it until somebody else changes one line in somebody else's package.json.
clsx has the same shape, on a smaller scale: version 2.1.1 leads, but 1.2.1 still gets about 12 million downloads a week.
So "fixing it in the next release" is not a plan for these packages. Whatever I published years ago is still running, at full volume, in projects I will never see. I have to treat every release as permanent, because for someone it will be.
#What I owe the people who never chose me
When you install a package on purpose, you made a choice. You read the README, maybe the code, and you accepted the risk. When a package arrives four levels deep, nobody made that choice. The trust was inherited.
That changes what I think a maintainer owes. Not more features. Fewer surprises.
Zero dependencies. Every package in the list above has none. That is partly about size, which I wrote about before. Here it is about trust: if my package has no dependencies, then installing it means trusting one person, not a tree of people I also never met.
No install scripts. Nothing runs when you install my code. A postinstall hook in a package this deep would run on hundreds of millions of machines, and almost nobody would notice it.
Only ship what runs. The files field in package.json is short and explicit. The tarball has the built code, the types, the license, and the readme. No tests, no fixtures, no config.
Lock down the account. 2FA on npm, and on GitHub, for every package.
Publish from CI. This is the newest rule, and I have not finished applying it. empathic publishes from GitHub Actions when I push a version tag. The job uses OIDC instead of a stored token, and it runs npm publish --provenance, so the registry can show which commit and which workflow built the tarball. clsx, dequal, kleur, and uvu were last published by hand, from my laptop, without provenance. They should not stay that way.
None of this is new advice. It just matters more when the blast radius is measured in billions.
#Saying no
The hardest part is not the security work. It is the pull requests.
A small package used by a lot of people gets a steady stream of reasonable ideas. Each one is useful to the person who sends it. Each one also becomes a line that hundreds of millions of installs carry, forever, and a behavior that every downstream project now depends on, whether it wanted it or not.
So the answer is usually no. Not "no, this is bad", but "no, this belongs in your code, not in mine". A clsx option that helps one framework is weight for every other framework. A dequal branch for an unusual type is a slower path for every common type.
I try to say no early, say why, and point to the place where the idea does fit. I do not always manage the kind part. Saying no to someone who did real work for free is not fun, and I have not found a way to make it fun.
#Done is a feature
dequal@2.0.3 was published in July 2022. clsx@2.1.1 was published in April 2024. escalade@3.2.0 gets about 194 million downloads a week.
People sometimes read that as abandonment. I read it as the goal.
A package that finds a file in a parent directory does not need a roadmap. When it works, the best thing I can do is leave it alone. Every release is a chance to break hundreds of thousands of projects, and the only reason to take that chance is a real bug or a real change in the platform under it.
This does not mean the issue trackers are empty. clsx has 16 open issues, dequal has 18, kleur has 13. Some are real bugs I owe a fix. Many are feature requests that I keep open because closing them feels rude, and I have not decided whether that is kind or just avoidant.
What I want, for most of these, is a long tail of nothing. No news. No releases. A version number that stops moving, and a download count that keeps going up because people keep building on something that does not change under them.
#The strange part
Polka made the biggest splash of anything I have written. Last month it had about 4 million downloads. escalade had over 200 times that.
I do not mind. I like that the most-used code I have written is the most boring. It means it got out of the way. Somebody's build tool found its config file, somebody's terminal printed a red line, somebody's component got the right class names, and none of them had to know why.
That is the job, as far as I can tell: be small, be boring, be there.
See also
open-source(7), npm(7), supply-chain(7), javascript(7), lukeed(1)