Two years ago the xz backdoor reminded everyone that "the ecosystem" is often one tired person. I wanted to know how true that is for an ordinary JavaScript project, so I built a small tool and pointed it at the most popular repositories on GitHub.
The question
A modern web project installs hundreds of packages you never chose one by one. For each of them, someone holds the right to publish the next version. If that someone stops, gets hacked, or simply leaves, what happens to your build?
npx @genamed/busfactor reads a lockfile and answers that per package: who can publish, whether the package is deprecated or archived, when it was last released, whether there is a funding link. Then it aggregates: which humans carry the most packages alone in this project.
The scan
I took the 600 most-starred JavaScript and TypeScript repositories on GitHub and kept the 433 that have a root lockfile (package-lock, pnpm-lock, yarn.lock or npm-shrinkwrap). That gave 27,184 unique npm packages, resolved through the npm registry and ecosyste.ms.
What came out
A median project installs 945 packages. Half of them have exactly one account that can publish. Organisation-owned packages (where the single account is the org itself, or @types) are excluded from that count.
One person is in 99% of the projects. sindresorhus publishes alone 513 of the packages found in the scan, and at least one of them is in 430 of the 433 repositories. The next names are isaacs (98%), juliangruber (95%), ljharb (94%), kevva (93%). The top five together touch every single repo in the set, React, VS Code, Vue and n8n included.
| # | publisher | repos affected | packages alone | examples |
|---|---|---|---|---|
| 1 | sindresorhus | 99% | 513 | supports-color, camelcase, wrap-ansi |
| 2 | isaacs | 98% | 76 | fs.realpath, sax, minipass |
| 3 | juliangruber | 95% | 21 | isarray, balanced-match |
| 4 | ljharb | 94% | 152 | resolve, deep-equal, util.promisify |
| 5 | kevva | 93% | 31 | dir-glob, shebang-command |
| 6 | qix | 92% | 5 | color-convert, color-string |
| 7 | satazor | 92% | 5 | cross-spawn |
| 8 | vitaly | 91% | 31 | argparse, js-yaml, markdown-it |
| 9 | lukeed | 91% | 46 | escalade, kleur, clsx |
| 10 | esp | 90% | 9 | fast-deep-equal, json-schema-traverse |
An account named nopersonsmodules is in 88% of the projects. That is the npm account where packages land when their author leaves the registry. It holds 59 packages in this scan (text-table, wordwrap, commondir, json-buffer...). Nobody publishes from it. If one of them ever needs a security fix, there is no one to ask.
98% of the repositories ship at least one dead dependency, deprecated or archived. path-is-absolute and inflight (deprecated because it leaks memory) are each in about 305 of the 433. prop-types, regenerator-runtime, xtend and through are archived and still everywhere.
Only 24% of the packages carry a funding link. For the people in the table above, most do have a GitHub Sponsors page. The tool prints it.
What this does and does not mean
A publish account is not an active maintainer. One account can hide a team behind a shared login; several accounts can be one person plus two people who left in 2019. Treat the numbers as a prompt to look, not as a verdict.
"Dormant" does not mean broken. A lot of these packages are ten lines long and finished. What the count measures is narrower and, I think, more useful: if a fix were needed tomorrow, whose keyboard would it have to come from?
Everything here comes from public registry data, so it is as fresh as the registries and ecosyste.ms are. Method, raw results and the scripts are in the repository under study/.
The part I actually care about
The CLI is the visible half. The other half is a small open dataset, data/successors.json: "this package is dead, this is the maintained replacement", one sourced entry per package, validated in CI against the registries every week so that a successor that dies gets flagged.
npm is already well served by e18e/module-replacements, so the map defers to them there and focuses on PyPI, Cargo, Go and Packagist, where nothing structured existed (request → undici is documented everywhere; mapstructure → go-viper/mapstructure or serde_yaml → serde_yaml_ng much less so). If you maintain a fork of an abandoned package in one of those ecosystems, a pull request adding it is the most useful contribution you can make.
Try it on your own project
npx @genamed/busfactor
It needs Node 20, has zero runtime dependencies, and takes a minute or two on a typical project (results are cached for a week). --md gives a card you can paste into a README or a pull request; --svg gives a shareable image; --fail-on dead makes it a CI gate.
Repository: https://github.com/GenAmed/busfactor
It is MIT, and, yes, it has a bus factor of 1 itself. GOVERNANCE.md says what happens if I go silent.
Top comments (0)