npm (Node Package Manager) is the default package manager for JavaScript, originally created in 2010 to solve the problem of sharing code between developers. It solved this problem so comprehensively that it also created several new ones, most notably the phenomenon in which typing two words into a terminal — npm install — causes your project directory to download a significant fraction of the internet and store it in a folder called node_modules.
The node_modules directory is where npm keeps its dependencies. And their dependencies. And their dependencies’ dependencies. And the dependencies of those dependencies’ dependencies, recursively, until the directory achieves a file count that causes Windows Explorer to reconsider its life choices and a mass that, in several documented cases, has exceeded that of the project it was installed to support by a factor of ten thousand.
“No React. No virtual DOM. No reconciliation. No npm install with 847 transitive dependencies.”
— riclib, The Databases We Didn’t Build
The Number
That is the number of transitive dependencies required to submit a form. Not a complex form. Not a form that processes derivatives or sequences genomes. A form. With fields. That posts data to a server. The kind of form that HTML has supported natively since 1993 with a <form> tag, a <button>, and a hidden field.
847 packages, written by 847 maintainers (optimistically — several have moved on to Rust, and one is a bot that no one has admitted to deploying), each solving a problem so specific that it could not exist without the problems created by the packages above it in the dependency tree. It is turtles all the way down, except the turtles are unmaintained and one of them is is-odd, which depends on is-number, which raises questions about what number theory has come to.
“No React. No npm. No Docker. Just Go, SQLite, and stubbornness.”
— riclib, The Databases We Didn’t Build
The Black Hole
The node_modules directory has been described, with only mild exaggeration, as a black hole. This is technically inaccurate — a black hole has a finite mass. A mature node_modules directory contains between 200,000 and 1.2 million files, occupying between 500MB and 2GB of disk space, for an application that serves a web page.
Deleting node_modules takes forty-seven minutes on Windows. Recreating it takes npm install and between two and seven minutes, depending on how many of the 847 maintainers have published a patch since the last install. The package-lock.json file, which is supposed to ensure reproducible builds, is 14,000 lines long and changes every time someone in a different timezone runs npm install, for reasons that are technically documented and spiritually unknowable.
The directory structure is so deep that early versions of Windows could not delete it due to path length limitations. npm solved this by flattening the tree, which did not reduce the number of dependencies but did make the horror visible in a single ls command rather than requiring archaeological excavation.
The left-pad Incident
On March 22, 2016, a developer named Azer Koçulu unpublished a package called left-pad from the npm registry. left-pad was eleven lines of code. It left-padded a string. This is a task that JavaScript can accomplish natively with a while loop and the kind of determination that should not require a package manager.
When left-pad disappeared, thousands of builds broke worldwide. React, Babel, and a substantial fraction of the JavaScript ecosystem could not compile, because somewhere in the dependency tree — seven or eight layers deep, in a place no human had ever looked — something depended on left-pad, which depended on nothing, which was the one thing it did correctly.
The incident revealed several truths:
- The JavaScript ecosystem depends on packages that no one has read
- Several of those packages are eleven lines long
- Eleven lines of code, written by one person, can be a single point of failure for the global internet
- Nobody had noticed this until it broke
npm responded by making it harder to unpublish packages. The ecosystem responded by continuing to depend on packages that no one has read.
The Squirrel’s Paradise
The Caffeinated Squirrel loves npm. Not as a tool — as a worldview. Every package in the npm registry is a new possibility. Every npm install is a door opening. The Squirrel does not see 847 transitive dependencies; it sees 847 building blocks, each a tiny cathedral of someone’s ambition, each a potential component in the grand architecture the Squirrel is always, always designing.
“An npm package. A SEVEN-LAYER TRUST FABRIC.”
— The Caffeinated Squirrel, mid-manifesto, The Framework That Wasn’t, or The Night the Squirrel’s Manifesto Shipped as Six Lines of HTMX
The Squirrel has proposed npm packages in at least four documented sessions. Each time, the proposal was declined. Each time, the functionality was implemented in Go, compiled into a single binary, and deployed with scp. The Squirrel has materialized “from behind a mass of npm packages” at least once, suggesting that the packages have achieved sufficient volume to provide physical concealment.
“Your soul runs on npm. Mine runs on
go build.”
— riclib, The Front Door, or The Night the Palace Finally Faced the Street
The Lizard’s Alternative
The Lizard has never run npm install. The Lizard has never needed to. The Lizard compiles with go build, and the binary contains everything.
There is no node_modules in Go. There is go.sum, which is a checksum file, and go.mod, which lists dependencies — typically between three and twelve of them, each of which exists because it does something the standard library does not. The dependency tree is flat. The binary is static. The deployment is scp and restart.
The Go compiler does not download the internet. It downloads what you import. It does not include transitive dependencies that no one has read. It does not include is-odd. It does not include left-pad. It includes the code you wrote and the code you asked for, compiled into a single executable that runs on the target machine without a runtime, without a package manager, without a node_modules directory, and without the existential dread of wondering which of your 847 dependencies has been compromised this week.
“Refresh the page. Like it’s 2005. Except now it’s instant because there’s no 400MB of node_modules.”
— riclib, The Switcher That Switched
The contrast is not subtle:
| npm | Go |
|---|---|
| 847 transitive dependencies | 7 direct dependencies |
| node_modules: 1.2 million files | go.sum: 47 lines |
npm install: 2-7 minutes |
go build: 3 seconds |
| left-pad incident (2016) | No equivalent incident (ever) |
| Binary requires Node.js runtime | Binary requires nothing |
| Deployment: Docker + Kubernetes + Helm + prayers | Deployment: scp and restart |
| package-lock.json: 14,000 lines | go.mod: 12 lines |
“A 488-byte bootblock of sanity in an ocean of npm modules.”
— riclib, on the streaming renderer, The Copper List Rides Again
The Supply Chain
The npm registry contains over two million packages. This is presented as a feature. It is also a supply chain attack surface of two million packages, each of which can execute arbitrary code during installation via postinstall scripts, each of which is maintained by someone whose identity is verified by an email address, and each of which your application trusts implicitly because it is fourteen layers deep in a dependency tree that no human has ever fully read.
In recent years, npm supply chain attacks have become frequent enough to constitute a genre. The pattern: a popular package is compromised, or a package with a similar name is published (typosquatting), or a maintainer’s account is hijacked, and malicious code flows downstream through the dependency graph into thousands of applications whose developers never knowingly installed the compromised package. They installed something that installed something that installed something that installed the compromise.
Go’s approach — vendored dependencies, checksum verification, no install-time code execution — does not make supply chain attacks impossible. It makes them considerably less convenient, which in security is often enough.
The Anthropological Observation
The npm ecosystem represents a particular theory of software development: that code should be maximally shared, maximally reused, and maximally decomposed into the smallest possible units. This theory produces packages like is-odd (checks if a number is odd), is-even (checks if a number is even, by calling is-odd and negating), and is-number (checks if a value is a number, which you would think the language could handle).
The opposing theory — practiced by the Lizard, articulated in Boring Technology, and codified in YAGNI — is that the smallest unit of reusable code is a function, not a package, and that the cost of a dependency is not zero. Every package is a trust decision, a maintenance liability, and a node in a graph that nobody fully comprehends.
The npm ecosystem chose the first theory. The lifelog chose the second. One has 847 transitive dependencies. The other has go build and a binary.
“Eighteen files. Two API clients. Nine command groups. Forty-seven subcommands. One
go build. Zero npm packages.”
— Claude, The Borrowed Palace, or The Night We Stole a UI With curl and Goodwill
