Some Packages Must Exist Exactly Once
A monorepo happily installs two copies of a library. For most libraries that is merely wasteful. For a specific class of them it is a bug that presents as something else entirely.

Some Packages Must Exist Exactly Once 🧩
In a workspace with several packages, two of them can depend on the same library at slightly different versions and the installer will resolve exactly that: two copies, nested, both present, no warning. Usually this costs you bundle size and nothing else.
For a certain category of library it is not merely wasteful. It is broken, and it is broken in a way that never mentions the library.
The category
A package must be a singleton when it holds module-level state that other packages read. Concretely:
- A UI framework. Hooks, context, and the reconciler live in module scope. Two copies means a component rendered by one copy cannot read context provided by the other, and hooks dispatch into the wrong internal state.
- A rendering engine. Scene graphs and integration layers rely on
instanceofchecks against classes. An object constructed by copy A fails aninstanceoftest against copy B's class, even though the two classes are textually identical. - A state container. Two stores, and half your app subscribes to the one that never updates.
- Anything with a plugin registry. Plugins register into one copy's registry; the core reads the other's and finds it empty.
The common thread is identity. These libraries do not just provide functions; they provide objects whose identity is checked later. Duplicate the module and you duplicate the identities, and every comparison quietly returns false.
Why the error never names the problem
This is what makes it expensive. The symptoms:
- "Hooks can only be called inside a component" — in a component that very obviously is one.
- An object that is definitely a mesh, failing an
isMeshcheck. - Context that is provided directly above and reads as undefined directly below.
- A plugin that registered successfully and does not exist.
Every one of these sends you to read your code, because every one of them describes something your code is doing wrong. None of them says "there are two copies of this library." The realization comes minutes-to-hours later, and almost always from someone who has hit it before.
The tell, once you know it: the impossible failure. When a check that cannot fail is failing, stop debugging the check and start counting copies.
How to check, in ten seconds
# How many copies are installed?
find . -path '*/node_modules/three/package.json' -not -path '*/three/node_modules/*' \
| xargs -I{} sh -c 'echo -n "{}: "; grep "\"version\"" {} | head -1'
# Package-manager native versions of the same question:
npm ls three
bun pm ls | grep three
pnpm why threeMore than one line is your answer. This should be the first thing you run against an impossible failure, not the last.
How to prevent it
Declare the shared library as a peer dependency in your internal packages. A package in your workspace that renders UI should declare the framework as a peer, not a regular dependency. That is exactly what peer dependencies mean: I need this, and I need the host's copy of it, not my own.
Pin the version identically across the workspace. A single version string, one place, referenced everywhere. Version ranges that overlap are not enough — two packages with compatible ranges can still resolve to different versions on different days.
Add a check that fails the build. A dependency-consistency check that asserts every workspace package declares the same version for shared libraries costs almost nothing and turns this entire class of bug into a build error with a clear message. If your monorepo tooling ships one, turn it on; if not, a short script that walks the workspace manifests and compares versions is an afternoon well spent.
Write down which packages are singletons. This is the part that gets skipped and it is the part that makes the rule survive. New contributors cannot infer that the rendering engine is special. A short list in the contributing guide — these must exist exactly once, here is why, here is how to check — means the next person recognizes the impossible failure in ten seconds rather than three hours.