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
We'll use an expression like
firstname.lastname@example.org indicate the published version
1.0.0of the NPM package
calendar. NPM package names sometimes also include a scope, for example
@my-company/calendar, in which case we would write
As a shorthand, where the package name doesn't matter, we may replace the package name with a capital letter variable such as
B@3.2.1. If the extra SemVer parts are unimportant, we may write
A@1as a shorthand for
A@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
email@example.com. Let's suppose that these two libraries
both depend on
fancy-button, but different versions:
Why are the versions inconsistent? Perhaps the latest release of
calendar was published sometime ago,
video-player just got a new release, so it's using the new version 4 of
This creates a "diamond dependency", where
my-app directly depends on
but indirectly depends on
fancy-button. And in this case, the diamond dependency causes side-by-side
fancy-button. Specifically the versions
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
with each other. They may also cause runtime failures, for example initializing multiple instances
of an object that is supposed to be a singleton.
How can we eliminate these side-by-side versions? Here's some possible ideas:
Ideally, we should upgrade
calendarto a newer version that uses
firstname.lastname@example.org. In our example,
1.0.0was 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.
email@example.com. The PNPM package manager provides mechanisms such as
.pnpmfile.cjsthat can override the package.json file for
calendarto force it to use a different version. This can be a handy shortcut, but it is risky:
calendarwasn't tested with
firstname.lastname@example.org. If Version 4 includes a breaking change, for example renaming an API, then the code will malfunction.
video-playerto an older version that uses
email@example.com. This is a less happy solution, since an older release of
video-playermay 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 of
video-playerwas already using
It's not always obvious which approach is best. It can require some trial and error.
Our example here involved only four packages (
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
fancy-button work in different Git repositories.
They publish at different times. They probably don't even communicate with each other. When
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
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
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.