pnpm is a package manager for JavaScript. It reads package.json, records the resolved tree in pnpm-lock.yaml, and links those packages into node_modules. This page is an overview of that model: why the files live in one shared store, how the other package managers lay out the same project, and a short list of commands. At the time of writing this article the latest pnpm version is 12.17.0.
What pnpm is for
Most package managers give every project its own copy of every dependency. pnpm keeps the file once, on the machine, and links it into each project that needs it. A second repository that asks for the same file links the same copy. The next install is mostly linking, because the download already happened.
The default tree is also strict. Node can load a package you declared in package.json. A package that arrived only because some other package needed it stays inside that package’s own folder. Your application code does not see it.
Hoisting
Hoisting is the opposite layout. npm and Yarn Classic lift dependencies toward the top of node_modules, so a library you installed and many libraries it depends on become neighbors in one folder.
Node looks for a package by walking upward through those folders. Code can then import a name that never appears in your package.json, because some other package happened to bring it along. That import is a phantom dependency. It works until the other package drops the dependency, or until a different installer builds a different tree.
npm - Node Package Manager is the command reference for that hoisted tree. Yarn describes the current release. Classic still hoists. Yarn 2 and later default to a map instead of that folder.
npm, Yarn, Bun, and pnpm
Each tool can install the same package.json. They differ in the lockfile, the shape of node_modules, and whether one copy of a file serves every project on the disk.
| npm | Yarn Classic | Yarn Berry | pnpm 12 | Bun | |
|---|---|---|---|---|---|
| Program | Ships with Node.js | A separate install | A version stored with the project | Installed for the user, pinned by the project | Part of the Bun runtime |
| Lockfile | package-lock.json |
yarn.lock |
yarn.lock |
pnpm-lock.yaml |
bun.lock |
| Project tree | Hoisted node_modules |
Hoisted node_modules |
A Plug’n’Play map | Isolated node_modules |
node_modules from bun install |
| Shared files | A cache, then a copy per project | An offline cache, then project files | A zip cache and a map | One store, linked into each project | Bun’s own cache |
Yarn 2 and later (Berry) skip the hoisted folder. A .pnp.cjs map tells Node where each package is. Yarn shows that per-project Yarn install.
Bun is a JavaScript runtime with its own installer and bun.lock. It fits a repository that already runs on Bun.
What nodeLinker does
nodeLinker is the pnpm setting that chooses the shape of node_modules. It belongs in pnpm-workspace.yaml at the root of the repository.
| Value | What you get |
|---|---|
isolated |
The default. Your direct dependencies are symlinks at the top of node_modules. The rest of the graph lives in node_modules/.pnpm. |
hoisted |
A flat folder, in the style of npm and Yarn Classic. Use it when a tool or a host cannot follow symlinks. |
pnp |
No node_modules walk. A map points at each package, as Yarn Berry does. |
With isolated, an install follows three steps. The registry is fetched into the store. The store is linked into node_modules/.pnpm. Only the packages you declared are then linked at the top of node_modules.
flowchart LR registry[Registry] store[Store] virtual["node_modules/.pnpm"] direct[Your dependencies] registry --> store store --> virtual virtual --> direct
A program for the user, packages for the project
The pnpm program is installed once for your user. The standalone script does not need Node.js afterward:
curl -fsSL https://get.pnpm.io/install.sh | env PNPM_VERSION=12.17.0 sh -If npm is already available, install the same release as a package. That install needs Node.js 18 or newer. On Windows, prefer this command, because Windows Defender sometimes blocks the standalone executable.
npm install -g pnpm@12.17.0pnpm self-update moves that program forward. When package.json already pins a version, the same command updates the pin.
The project records the release it expects:
{ "packageManager": "pnpm@12.17.0"}Packages follow the directory you are in. pnpm add writes a dependency of the current project. pnpm add -g installs a command-line tool for your user, outside any repository. Both read files from the same store.
The store and deduplication
The store is content-addressable: each file is saved under a name taken from its bytes, so two identical files are one entry. Two projects that both depend on lodash@4.17.21 share that single copy. When a later lodash release changes only lodash.js, pnpm adds that one file and leaves the unchanged files where they already are.
| System | Default store |
|---|---|
| Linux | ~/.local/share/pnpm/store |
| macOS | ~/Library/pnpm/store |
| Windows | ~/AppData/Local/pnpm/store |
pnpm store path prints the directory in use. $PNPM_HOME/store wins when PNPM_HOME is set, and $XDG_DATA_HOME/pnpm/store wins when XDG_DATA_HOME is set. The store has to sit on the same disk as the project. A hard link cannot cross filesystems, so a store on another disk means every file is copied. pnpm store prune drops entries that no local project still uses.
packageImportMethod chooses the kind of link from the store into the project. The default, auto, uses a hard link on Linux, a copy-on-write clone on macOS and Windows, and a plain copy when neither is possible. A hard link is a second name for the same file.
The lockfile deduplicates versions on top of the store. Ranges that can share one version do. 1.2.0 next to 2.0.0 keeps both versions. Identical files inside those versions still exist once in the store.
Share a store only with users and jobs that already trust each other. Anyone who can write to it can change the files your projects link.
Setup and configuration
Create a project, pin 12.17.0, and install:
pnpm initpnpm add -D typescriptAdd the packageManager field shown above, then run pnpm install. Commit pnpm-lock.yaml. In CI, a lockfile that is already present is installed frozen: the command fails if the lockfile would have to change.
To carry versions over from yarn.lock, package-lock.json, or npm-shrinkwrap.json, run pnpm import before pnpm install. The import prefers the versions already written in that file.
Settings and the list of workspace packages both live in pnpm-workspace.yaml. Leave packages out and the root package is the whole workspace. pnpm ignores a pnpm field in package.json.
nodeLinker: isolatedpackageImportMethod: autoPaths that belong to the machine, such as where the program itself is installed, go in the global config.yaml (~/.config/pnpm/config.yaml on Linux). pnpm config reads and edits both files.
Registry tokens stay out of the repository. pnpm login writes them to auth.ini. A gitignored .npmrc, or ~/.npmrc, is the other place for those credentials.
Common commands
These are the commands you run most often. Replace <pkg> with a package name and <script> with a name from package.json.
Install and change packages
| Command | What it does |
|---|---|
pnpm install |
Installs the tree recorded in pnpm-lock.yaml. |
pnpm add <pkg> |
Adds a dependency and updates the lockfile. |
pnpm add -D <pkg> |
Adds a dev dependency. |
pnpm add -g <pkg> |
Installs a command-line tool for your user. |
pnpm remove <pkg> |
Removes a dependency from the project. |
pnpm update |
Updates packages inside the ranges in package.json. |
pnpm update --latest |
Moves those ranges to the newest published versions. |
pnpm import |
Builds pnpm-lock.yaml from a Yarn or npm lockfile. |
Run a package
| Command | What it does |
|---|---|
pnpm run <script> |
Runs a script from package.json. pnpm test and pnpm start do this without run. |
pnpm exec <bin> |
Runs a binary from this project’s node_modules. |
pnpm dlx <pkg> |
Downloads a package and runs it once, without adding it to the project. |
See what is installed
| Command | What it does |
|---|---|
pnpm list --depth 0 |
Lists the packages this project declared. |
pnpm outdated |
Shows packages that have a newer release. |
pnpm why <pkg> |
Shows which dependency pulled a package in. |
pnpm store path |
Prints the store directory on this machine. |
A prompt that sets pnpm up
Give this to an assistant that can edit the repository and run commands.
Set up pnpm 12.17.0 in this repository.
1. If `pnpm -v` is missing or is not 12.17.0, install that release. Use `npm install -g pnpm@12.17.0` when npm is available. Otherwise run the standalone script from https://get.pnpm.io/install.sh with PNPM_VERSION=12.17.0. On Windows, use the npm command.2. Create package.json with `pnpm init` when the file is missing. Set the packageManager field to pnpm@12.17.0.3. Add pnpm-workspace.yaml at the repository root when it is missing. Set nodeLinker to isolated. Add a packages list only when the repository is already a monorepo. Keep registry tokens out of this file.4. When yarn.lock, package-lock.json, or npm-shrinkwrap.json exists and pnpm-lock.yaml does not, run `pnpm import`. Then run `pnpm install`.5. Ignore node_modules in .gitignore when it is not already ignored.6. Report `pnpm -v`, the output of `pnpm store path`, and any peer-dependency warnings from the install.