Rush StackShopBlogEvents
Skip to main content

Installation models

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:

  1. NPM installation model - The classic node_modules layout, 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.

  2. PNPM installation model - The PNPM package manager introduced a completely different node_modules layout 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_modules structure is somewhat convoluted, but its big advantage is excellent backwards compatibility with legacy code that resolves modules by traversing the node_modules tree. (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.

  3. 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 install will generate a patch script .pnp.cjs, and then Node.js gets invoked with NODE_OPTIONS="--require $(pwd)/.pnp.cjs". The script is expected to implement the PnP API contract, although there is currently no spec for how module resolution might be performed by a tool that cannot execute arbitrary JavaScript code (for example a Java service). Because it fundamentally redesigns the semantics of 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.

  4. Rush legacy symlinking - Rush implemented its own symlinking installation model which predates the Yarn and PNPM workspace features. This model is used when useWorkspaces=false in 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.