Version conflicts
From a high level perspective, Lockfile Explorer is a tool for coordinating versions of library packages. This is a classic problem in computer science, sometimes jokingly called dependency hell or DLL hell.
An example problem
Some notation
We'll use an expression like
calendar@1.0.0
to indicate the published version1.0.0
of the NPM packagecalendar
. NPM package names sometimes also include a scope, for example@my-company/calendar
, in which case we would write@my-company/calendar@1.0.0
.As a shorthand, where the package name doesn't matter, we may replace the package name with a capital letter variable such as
A@1.2.3
orB@3.2.1
. If the extra SemVer parts are unimportant, we may writeA@1
as a shorthand forA@1.0.0
. These shorthand notations are not legal NPM package names or versions.
As a motivating example, suppose we're developing a project my-app
that depends on two hypothetical
library packages calendar@1.0.0
and video-player@2.0.0
. Let's suppose that these two libraries
both depend on fancy-button
, but different versions:
calendar@1.0.0
depends onfancy-button@3.0.0
video-player@2.0.0
depends onfancy-button@4.0.0
Why are the versions inconsistent? Perhaps the latest release of calendar
was published sometime ago,
whereas video-player
just got a new release, so it's using the new version 4 of fancy-button
.
This creates a "diamond dependency", where my-app
directly depends on calendar
and video-player
but indirectly depends on fancy-button
. And in this case, the diamond dependency causes side-by-side
versions of fancy-button
. Specifically the versions 3.0.0
and 4.0.0
.
Side-by-side versions may cause performance issues, such as bloating the size of your bundled app.
They may cause compile failures, for example if the TypeScript types for fancy-button
conflict
with each other. They may also cause runtime failures, for example initializing multiple instances
of an object that is supposed to be a singleton.
Possible solutions
How can we eliminate these side-by-side versions? Here's some possible ideas:
Ideally, we should upgrade
calendar
to a newer version that usesfancy-button@4.0.0
. In our example,1.0.0
was the latest release, so this means we need to contact the maintainers and get them to publish a new release. This can often take days or even months.Force
calendar
to usefancy-button@4.0.0
. The PNPM package manager provides mechanisms such as.pnpmfile.cjs
that can override the package.json file forcalendar
to force it to use a different version. This can be a handy shortcut, but it is risky:calendar
wasn't tested withfancy-button@4.0.0
. If Version 4 includes a breaking change, for example renaming an API, then the code will malfunction.Downgrade
video-player
to an older version that usesfancy-button@3.0.0
. This is a less happy solution, since an older release ofvideo-player
may be missing features that we need. We may have to go very far back in time to find a compatible version, or it may be the case that there is no such version -- the first release ofvideo-player
was already usingfancy-button
version 4.
It's not always obvious which approach is best. It can require some trial and error.
Our example here involved only four packages (my-app
, calendar
, video-player
, and fancy-button
),
whereas a typical monorepo has thousands of package dependencies. And side-by-side versions are just one
of many possible version conflicts that can arise among libraries. It does feel like dependency "hell."
However, take heart! With some practice, and some guiding principles, and help from Lockfile Explorer, you can learn how to solve these problems.
Is there a shortcut?
Version conflicts arise from a lack of coherence in authoring of source code: In our example,
the maintainers of calendar
, video-player
, and fancy-button
work in different Git repositories.
They publish at different times. They probably don't even communicate with each other. When
fancy-button
releases 4.0.0
, we can't upgrade my-app
until each intermediary dependency has
upgraded and published a new version. This time lag can be considerable, especially if Version 4
has breaking API changes that require nontrivial work to fix. Lack of coherence creates time lags,
which cause this trouble.
Shortcut 1: Full coherence The fully coherent way of working is the monorepo:
All your source code goes in a single branch of a single repo. If a breaking change is made to the
API for fancy-button
, all consumers must be fixed at the time when that change is merged.
The cost of fixing downstream consumers is paid by the person who introduced the break
("you broke it, you fix it"), which avoids creating downstream victims, and ensures costs and effects
are fully analyzed before a prospective change is released. Monorepos work great for a large code base
that's maintained by partner teams within a single organization, but of course it's not a realistic model
for external libraries maintained by different parties on the internet. Nonetheless, if you think about it,
the various mitigations that we'll be presenting are basically approximating a monorepo, by manipulating
your node_modules
dependencies as if they were part of your own set of projects.
Shortcut 2: Complete decoupling: The other escape hatch is to develop fully self-contained libraries, whose package.json files have no dependencies at all. (In fact, this model was enforced by the Bower package manager that predated NPM.) Unfortunately a complete lack of code sharing brings its own problems, of duplicated code and reinvented wheels.