Installing a package involves downloading an archive and extracting its files into a folder on disk,
for example into the
node_modules folder tree. We'll use the term installation model to describe
the mechanics of this process. Unlike most other programming languages, there are multiple installation models
in common use for Node.js today:
NPM installation model - The classic
node_moduleslayout, currently used by NPM as well as Yarn Classic. The fundamental design of this algorithm unavoidably produces phantom dependencies (ability to import files that were not declared in package.json), as well as NPM doppelgangers (duplicate installations of the same version of the same package). NPM's implementation also produces nondeterministic installations depending on the order in which CLI commands are invoked. The later installation models can all be understood as attempts to mitigate these design flaws, with different tradeoffs for backwards compatibility with legacy packages.
PNPM installation model - The PNPM package manager introduced a completely different
node_moduleslayout that relies heavily on symlinks. This completely eliminates the problem of NPM doppelgangers (although peer doppelgangers are still possible). It also can completely eliminate phantom dependencies, depending on the dependency hoisting configuration. The resulting
node_modulesstructure is somewhat convoluted, but its big advantage is excellent backwards compatibility with legacy code that resolves modules by traversing the
node_modulestree. (Compatibility fixes are sometimes required, but in most cases the fix is very simple, and today almost every popular package has been fixed.) Yarn now also optionally supports the PNPM installation model via the @yarnpkg/plugin-pnpm plugin.
Plug'n'Play (PnP) installation model - The Yarn package manager took a different approach, by introducing a hook called Plug'n'Play that allows the Node.js
require()API to be patched with a completely different implementation. The basic concept is that
yarn installwill generate a patch script
.pnp.cjs, and then Node.js gets invoked with
require(), Plug'n'Play offers significant improvements in both features and performance; however adoption has been limited due to compatibility challenges for existing NPM packages. The PNPM package manager now optionally supports the Plug'n'Play installation model via the node-linker=pnp setting.
Rush legacy symlinking - Rush implemented its own symlinking installation model which predates the Yarn and PNPM workspace features. This model is used when
useWorkspaces=falsein pnpm-config.json, or if the Yarn or NPM package manager is selected in rush.json. This algorithm is still supported but not recommended.
Rush Stack recommends the PNPM installation model for monorepos, because it currently seems to provide the best tradeoff between correctness and backwards compatibility. It is the default and best supported installation mode for the Rush build orchestrator. It is the only model supported by Lockfile Explorer, although in the future we expect to implement support for other lockfile formats.
PNPM's Feature Comparison summarizes some other interesting differences between package managers, beyond the installation models that they support.
The acronyms "PnP" and "PNPM" are easy to confuse: PnP (Plug'n'Play) is an installation model, whereas PNPM is a package manager whose default installation model is not PnP.