Most libraries are meant to be consumed via a package manager. Pip or npm install’d. Pinned to a particular version or checksum (ideally, but rarely in practice). I’ve been deeply embedded in package management for many years — sharing code is a fundamental (if not the most fundamental) aspect of software development.
But what if we didn’t need a package manager for some things? In Reflections on 10,000 Hours of Programming, I wrote #16, don’t take a direct dependency on a small library you could easily rewrite, and #17, sometimes a little repetition is better than a little bit of dependency.
When would it make sense?
Too many degrees of freedom. For some things (like React components), sometimes too many degrees of customization are needed to ship a usable API. Every function or component has tens of arguments and toggles. Nothing works off the shelf but needs to be customized to your application to be functional. Themes will almost always be application-specific and are tightly coupled with the UI elements.
Encapsulated logic. You obviously can’t copy and paste large libraries. Anything over a function or single file is probably out of the question. But smaller chunks of code are probably easier to copy and paste than they are to vendor in via a package manager.
A library that everyone needs some of, but nobody needs all of. You can tree-shake out the logic or divide packages into small sub-packages. Still, sometimes significant developer overhead (consumer and producer) comes with a vast library.
One of the first libraries I’ve seen branded explicitly like this is shadcn/ui, but this has been the practice for decades with frameworks like Tailwind UI and Twitter Bootstrap. There’s still some work to be done on making this more streamlined, but I think there are quite a few optimizations you can build in when you want your library to be a ‘copy and paste’ library.
There are a million reasons why this doesn’t always work in practice:
- Versioning is the problem and the solution to most bugs in software engineering.
- Updates, security fixes, and new functionality must be explicitly pulled into the application. In practice, developers rarely update their production software because it breaks things. This exacerbates the problem — the longer you go in between upgrades, the more challenging it becomes.
- Hard to build software that is copy-pastable. I’m not sure what the half-life of software like this is. I guess it’s short — assumptions are always buried in even the smallest blocks of code.
Maybe there’s an iteration of this idea where the package manager just downloads specific packages to your source directory. The ones that are meant to be “monkey-patched” and modified. Sure, there will be merge conflicts when you upgrade the software to a new version, but at least you still retain some of the versioning information.