GENERATION.md
# Generation Info - **Source:** `sources/pnpm` - **Git SHA:** `398f45f429a9f51e249d8bb91a4e12882cb9b075` - **Generated:** 2026-11-05
antfu/skills · GitHub
엄격한 종속성 해결 기능을 갖춘 Node.js 패키지 관리자입니다. pnpm 전용 명령어를 실행하거나, pnpm-workspace.yaml을 통해 작업 공간을 구성하거나, 카탈로그, 패치, 오버라이드, 구성 종속성 또는 전역 가상 저장소를 사용하여 종속성을 관리할 때 사용합니다.
프로젝트 폴더에서 아래 명령어를 실행하고, 설치할 에이전트를 선택하세요.
npx skills add antfu/skills --skill pnpm설치 명령을 직접 실행해야 적용됩니다. 지원 에이전트와 필요한 권한·라이선스는 제작자의 안내를 확인하세요.
GENERATION.md# Generation Info - **Source:** `sources/pnpm` - **Git SHA:** `398f45f429a9f51e249d8bb91a4e12882cb9b075` - **Generated:** 2026-11-05
references/best-practices-ci.md---
name: pnpm-ci-cd-setup
description: Optimizing pnpm for continuous integration and deployment workflows
---
# pnpm CI/CD Setup
Best practices for using pnpm in CI/CD environments for fast, reliable builds.
> **CI auto-behaviors:** When pnpm detects a CI environment it switches to **frozen-lockfile** mode automatically (opt out with `--no-frozen-lockfile`; force or disable detection with the `ci` setting) and **fails on an incompatible lockfile** written by a newer pnpm major instead of rewriting it — keep the CI pnpm version in sync with the one that generated the lockfile. The global virtual store is auto-disabled in CI (no warm cache).
## Installing pnpm in CI
On CI systems that run jobs in a container image (GitLab CI, Bitbucket Pipelines), use the **official image** `ghcr.io/pnpm/pnpm:<major>` as the job image: pnpm is already on `PATH`, and `pnpm runtime set node lts -g` installs Node.js. For your own Dockerfile, copy pnpm in: `COPY --from=ghcr.io/pnpm/pnpm:12 /opt/pnpm /opt/pnpm` (use the `-alpine` tag for musl bases), then symlink `pnpm pn pnpx pnx` from `/opt/pnpm` into `/usr/local/bin`.
Elsewhere outside GitHub Actions (which has its own action), use the **standalone install script**, never Corepack. Save it to a file first, with a fallback download so a failed download never runs a partial script:
```sh
{ curl -fsSL https://get.pnpm.io/ -o /tmp/pnpm-install.sh \
|| curl -fsSL https://github.com/pnpm/get.pnpm.io/releases/latest/download/install.sh -o /tmp/pnpm-install.sh; } \
&& sh /tmp/pnpm-install.sh
```
- **No Node.js needed** — pnpm is self-contained and can install a runtime afterwards (`pnpm runtime set node lts -g`).
- **Follows the project's version** — after bootstrap, pnpm switches to the `packageManager`/`devEngines.packageManager` pin on first use, so you don't pin twice.
- The script sets `PNPM_HOME` and edits a shell profile CI never reloads — export `PNPM_HOME` and put `$PNPM_HOME/bin` on `PATH` yourself.
> Corepack installs a JS shim, so every `pnpm` call starts Node.js to run the shim first — a cost paid on each of CI's many invocations. Installing pnpm itself avoids it.
### Cache the metadata cache too
`pnpm cache path` prints pnpm's metadata cache directory. Caching it (alongside the store) also caches the lockfile verification log, letting a job skip re-checking an unchanged lockfile against supply-chain policies — the dominant install cost once the store is warm.
## GitHub Actions
### Basic Setup
```yaml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install --frozen-lockfile # or: pnpm ci
- run: pnpm test
- run: pnpm build
```
> `pnpm ci` (aliases `clean-install`, `install-clean`) = `pnpm clean` + `pnpm install --frozen-lockfile`, ideal for fully reproducible CI builds.
### With Store Caching
For larger projects, cache the pnpm store:
```yaml
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- run: pnpm install --frozen-lockfile
```
> **Trust:** only cache/restore the pnpm store and cache dir between *trusted* jobs. A store an untrusted job can write to must not be reused by trusted jobs — it is part of pnpm's trust domain.
### Matrix Testing
```yaml
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm test
```
## GitLab CI
```yaml
image: node:20
stages:
- install
- test
- build
variables:
PNPM_HOME: /root/.local/share/pnpm
PATH: $PNPM_HOME:$PATH
before_script:
- corepack enable
- corepack prepare pnpm@latest --activate
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .pnpm-store
install:
stage: install
script:
- pnpm config set store-dir .pnpm-store
- pnpm install --frozen-lockfile
test:
stage: test
script:
- pnpm test
build:
stage: build
script:
- pnpm build
```
## Docker
> **PATH:** global pnpm binaries live in `$PNPM_HOME/bin`. In Docker set `ENV PATH="$PNPM_HOME/bin:$PATH"` (not `$PNPM_HOME`). The official image `ghcr.io/pnpm/pnpm:<version|major|latest>` (Debian slim, or `-alpine` variants; pnpm + bundled `node-gyp`, no Node.js) already sets `PNPM_HOME=/pnpm` and PATH; choose Node via `pnpm runtime set node <ver> -g` or `devEngines.runtime` (`pnpm install` downloads it).
>
> **Always copy `pnpm-workspace.yaml`** (when present) before `pnpm install`/`pnpm fetch`: it holds project settings and workspace patterns, and omitting it can change installs or fail frozen installs. Also copy patches and files named by `include`.
>
> **BuildKit cache mounts:** if the image has a runtime from `pnpm runtime set node -g`, don't mount over `/pnpm/store`; mount `/var/cache/pnpm` and pass `--store-dir /var/cache/pnpm`.
>
> **`pnpm fetch` recipe:** `COPY pnpm-lock.yaml pnpm-workspace.yaml /app/`, `RUN pnpm fetch`, then after `COPY . /app` run `pnpm install --offline --frozen-lockfile` (fetch only fills the virtual store); use a separate stage with `pnpm fetch --prod` + `install --offline --frozen-lockfile --prod` for production deps.
### Multi-Stage Build
```dockerfile
# Build stage
FROM node:24-slim AS builder
# Enable corepack for pnpm
RUN corepack enable
WORKDIR /app
# Copy package files first for layer caching
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages/*/package.json ./packages/
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy source and build
COPY . .
RUN pnpm build
# Production stage
FROM node:20-slim AS runner
RUN corepack enable
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
COPY --from=builder /app/pnpm-lock.yaml /app/pnpm-workspace.yaml ./
# Production install
RUN pnpm install --frozen-lockfile --prod
CMD ["node", "dist/index.js"]
```
### Optimized for Monorepos
```dockerfile
FROM node:20-slim AS builder
RUN corepack enable
WORKDIR /app
# Copy workspace config
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
# Copy all package.json files maintaining structure
COPY packages/core/package.json ./packages/core/
COPY packages/api/package.json ./packages/api/
# Install all dependencies
RUN pnpm install --frozen-lockfile
# Copy source
COPY . .
# Build specific package
RUN pnpm --filter @myorg/api build
```
## Key CI Flags
### --frozen-lockfile
**Always use in CI.** Fails if `pnpm-lock.yaml` needs updates:
```bash
pnpm install --frozen-lockfile
```
### --prefer-offline
Use cached packages when available:
```bash
pnpm install --frozen-lockfile --prefer-offline
```
### --ignore-scripts
Skip lifecycle scripts for faster installs (use cautiously):
```bash
pnpm install --frozen-lockfile --ignore-scripts
```
## Pinning the pnpm version
Pin with `packageManager` (exact) or `devEngines.packageManager` (range, resolved version stored in the lockfile):
```json
// package.json
{ "packageManager": "pnpm@10.0.0" }
```
The standalone install script and `pnpm/action-setup` both honor this pin. To skip the pin check when version management is external (asdf/mise/Volta), set `pmOnFail: ignore` in `pnpm-workspace.yaml`, or run a one-off with `pnpm with current <cmd>`.
> Corepack still works but is no longer the recommended CI path (it adds a Node.js shim to every `pnpm` call). Prefer the standalone script above.
## Monorepo CI Strategies
### Build Changed Packages Only
```yaml
- name: Build changed packages
run: |
pnpm --filter "...[origin/main]" build
```
### Parallel Jobs per Package
```yaml
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.changes.outputs.packages }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: changes
run: |
echo "packages=$(pnpm --filter '...[origin/main]' list --json | jq -c '[.[].name]')" >> $GITHUB_OUTPUT
test:
needs: detect-changes
if: needs.detect-changes.outputs.packages != '[]'
runs-on: ubuntu-latest
strategy:
matrix:
package: ${{ fromJson(needs.detect-changes.outputs.packages) }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- run: pnpm install --frozen-lockfile
- run: pnpm --filter ${{ matrix.package }} test
```
## Best Practices Summary
1. **Use `pnpm ci` or `--frozen-lockfile`** in CI
2. **Install via the standalone script** (`get.pnpm.io/install.sh`) rather than Corepack
3. **Cache the pnpm store and metadata cache** (`pnpm cache path`), only across trusted jobs
4. **Match the CI pnpm major** to the one that wrote the lockfile (CI fails on incompatible lockfiles)
5. **Pin `packageManager`** (or `devEngines.packageManager`) in package.json
6. **Use `--filter`** in monorepos to build only what changed
7. **Multi-stage Docker builds**; set `PATH=$PNPM_HOME/bin:$PATH`
<!--
Source references:
- https://pnpm.io/continuous-integration
- https://pnpm.io/docker
- https://pnpm.io/cli/ci
- https://pnpm.io/settings/other#ci
- https://pnpm.io/cli/cache-path
- https://github.com/pnpm/action-setup
-->
references/best-practices-migration.md---
name: migration-to-pnpm
description: Migrating from npm or Yarn to pnpm with minimal friction
---
# Migration to pnpm
Guide for migrating existing projects from npm or Yarn to pnpm, plus upgrading pnpm v10 → v11 and v11 → v12.
## Upgrading pnpm v11 → v12
pnpm 12 (native executable, no Node.js needed to run) reads the same `pnpm-lock.yaml`, `pnpm-workspace.yaml`, and store as v11 - upgrading is not a migration and there is no codemod. If still on v10, do v10 → v11 first (its codemod applies).
```sh
pnpm self-update 12 # also updates the packageManager/devEngines.packageManager pin
```
If you install pnpm another way, update the pin too, otherwise pnpm 12 runs the pinned pnpm 11. Then run `pnpm install`, fix reported settings, commit `package.json` and the lockfile. Behavior changes to check:
- **Unrecognized `pnpm-workspace.yaml` settings fail** (`ERR_PNPM_UNRECOGNIZED_WORKSPACE_SETTINGS`) when the project pins a pnpm version the running one satisfies (else still a warning). `confirmModulesPurge` is rejected. `pnpm config` never fails on it.
- **Boolean flag + separate value is rejected**: use `--frozen-lockfile=false` / `--no-frozen-lockfile`, not `--frozen-lockfile false`.
- **Git dependency resolution:** GitHub/GitLab/Bitbucket specifiers resolve via the host's HTTPS URL and pnpm never records an SSH URL (no network probing). Old `git@...` lockfile entries change only on re-resolve (`pnpm update <pkg>`). Private SSH: `git config --global url."git@github.com:".insteadOf https://github.com/`.
- **pnpmfile hooks:** `importPackage` is not run (use `packageImportMethod`); `filterLog` is ignored with a warning (use `loglevel`).
- **`engineStrict`** fails when an incompatible package is reached via regular `dependencies` even under an optional subtree; with `--no-runtime`/`runtime: false` it checks the root `engines.node` against the active Node.js.
- **Naming a package manager/runtime installs the tool**, not the npm package: `pnpm add -g yarn`, `pnx node@22`. `yarn@npm:yarn@1.22.22` still installs the package.
- **Global `node`/`deno`/`bun` follow the project's pin** (`devEngines.runtime`, `engines.runtime`, `.node-version`, `.nvmrc`). Opt out with `globalShims: false` (global config) or `PNPM_SHIM_BYPASS=1`.
- **Global commands under `sudo` fail** (`ERR_PNPM_SUDO_NOT_SUPPORTED`): `setup`, `self-update`, `add --global`.
- **TLS:** uses the OS certificate store (fallback: bundled Mozilla CAs); `ca`/`cafile` still override. **`maxsockets`** no longer defaults to 50; only `networkConcurrency` bounds direct connections (proxied: 50 per origin).
- **`shellEmulator`** uses pnpm's built-in shell (some `${NAME:=word}` forms are literal; unparsable scripts fail `ERR_PNPM_EXECUTOR_SHELL_EMULATOR_PARSE`). **`scriptsPrependNodePath`** is ignored.
- **`packageImportMethod: auto` hardlinks first on Linux** (was clone-first). Set `clone` on btrfs/xfs to keep copy-on-write.
- **Cyclic dependency graphs** give deterministic lockfiles; the first re-resolving install may rewrite cyclic peer variants once. Frozen installs are unaffected.
- **`pnpm dlx`/`pnx` do not use the global virtual store.**
- **`pnpm pack-app`** embeds Node.js 25.5.0 unless `--runtime`/`pnpm.app.runtime` is set. `--frozen-store` has no Node.js version requirement.
## Upgrading pnpm v10 → v11
v11 changes how configuration is read. Most of it is mechanical — run the codemod:
```bash
cd /path/to/project
pnx codemod run pnpm-v10-to-v11 # pnpx on pnpm v10
```
The codemod automatically:
- **Moves `package.json#pnpm` settings into `pnpm-workspace.yaml`** (the `pnpm` field is no longer read).
- **Splits `.npmrc`**: only auth/registry settings stay in `.npmrc`; every other key moves to `pnpm-workspace.yaml` as **camelCase** (e.g. `node-linker` → `nodeLinker`). Per-subproject `.npmrc` files become `packageConfigs["<name>"]`.
- **Consolidates build settings** (`onlyBuiltDependencies`, `neverBuiltDependencies`, `ignoredBuiltDependencies`, `onlyBuiltDependenciesFile`) into one `allowBuilds: { name: true|false }` map.
- **Replaces** `managePackageManagerVersions`/`packageManagerStrict`/`packageManagerStrictVersion` with `pmOnFail: download|ignore|warn|error`.
- **Renames** `allowNonAppliedPatches` → `allowUnusedPatches`, `auditConfig.ignoreCves` → `auditConfig.ignoreGhsas`.
- **Converts** `useNodeVersion` → `devEngines.runtime`, and bumps `packageManager`.
Manual follow-ups (not automatable):
- Convert `CVE-…` IDs to `GHSA-…` in `auditConfig.ignoreGhsas`.
- `ignorePatchFailures` removed — failed patches now always throw.
- `npm_config_*` env vars → `pnpm_config_*` (CI, shell profiles, Docker).
- `pnpm link <name>` → use a path (`pnpm link ./foo`); `pnpm link --global` → `pnpm add -g .`.
- `pnpm install -g` (no args) and `pnpm server` removed.
- A `package.json` script named like a built-in (e.g. `clean`, `deploy`) now shadows the built-in `pnpm <name>`; see the built-in/script name conflicts list in the scripts docs.
- Docker images must copy `pnpm-workspace.yaml` (if present) before `pnpm install`.
## Migrating from npm / Yarn
## Quick Migration
### From npm
```bash
# Remove npm lockfile and node_modules
rm -rf node_modules package-lock.json
# Install with pnpm
pnpm install
```
### From Yarn
```bash
# Remove yarn lockfile and node_modules
rm -rf node_modules yarn.lock
# Install with pnpm
pnpm install
```
### Import Existing Lockfile
pnpm can import existing lockfiles:
```bash
# Import from npm or yarn lockfile
pnpm import
# This creates pnpm-lock.yaml from:
# - package-lock.json (npm)
# - yarn.lock (yarn)
# - npm-shrinkwrap.json (npm)
```
## Handling Common Issues
### Phantom Dependencies
pnpm is strict about dependencies. If code imports a package not in `package.json`, it will fail.
**Problem:**
```js
// Works with npm (hoisted), fails with pnpm
import lodash from 'lodash' // Not in dependencies, installed by another package
```
**Solution:** Add missing dependencies explicitly:
```bash
pnpm add lodash
```
### Missing Peer Dependencies
pnpm reports peer dependency issues by default.
**Option 1:** Let pnpm auto-install (default in v8+):
```yaml title="pnpm-workspace.yaml"
autoInstallPeers: true
```
**Option 2:** Install manually:
```bash
pnpm add react react-dom
```
**Option 3:** Suppress warnings if acceptable:
```yaml title="pnpm-workspace.yaml"
peerDependencyRules:
ignoreMissing:
- react
```
### Symlink Issues
Some tools don't work with symlinks. Use hoisted mode:
```yaml title="pnpm-workspace.yaml"
nodeLinker: hoisted
```
Or hoist specific packages:
```yaml title="pnpm-workspace.yaml"
publicHoistPattern:
- '*eslint*'
- '*babel*'
```
### Native Module Rebuilds
If native modules fail, try:
```bash
# Rebuild all native modules
pnpm rebuild
# Or reinstall
rm -rf node_modules
pnpm install
```
## Monorepo Migration
### From npm Workspaces
1. Create `pnpm-workspace.yaml`:
```yaml
packages:
- 'packages/*'
```
2. Update internal dependencies to use workspace protocol:
```json
{
"dependencies": {
"@myorg/utils": "workspace:^"
}
}
```
3. Install:
```bash
rm -rf node_modules packages/*/node_modules package-lock.json
pnpm install
```
### From Yarn Workspaces
1. Remove Yarn-specific files:
```bash
rm yarn.lock .yarnrc.yml
rm -rf .yarn
```
2. Create `pnpm-workspace.yaml` matching `workspaces` in package.json:
```yaml
packages:
- 'packages/*'
```
3. Update `package.json` - remove Yarn workspace config if not needed:
```json
{
// Remove "workspaces" field (optional, pnpm uses pnpm-workspace.yaml)
}
```
4. Convert workspace references:
```json
// From Yarn
"@myorg/utils": "*"
// To pnpm
"@myorg/utils": "workspace:*"
```
### From Lerna
pnpm can replace Lerna for most use cases:
```bash
# Lerna: run script in all packages
lerna run build
# pnpm equivalent
pnpm -r run build
# Lerna: run in specific package
lerna run build --scope=@myorg/app
# pnpm equivalent
pnpm --filter @myorg/app run build
# Lerna: publish
lerna publish
# pnpm: use changesets instead
pnpm add -Dw @changesets/cli
pnpm changeset
pnpm changeset version
pnpm publish -r
```
## Configuration Migration
Keep only **auth/registry** in `.npmrc`; put everything else in `pnpm-workspace.yaml` (camelCase).
```ini title=".npmrc (auth only, gitignored)"
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
//npm.myorg.com/:_authToken=${MYORG_TOKEN}
```
```yaml title="pnpm-workspace.yaml"
registries:
default: https://registry.npmjs.org/
'@myorg': https://npm.myorg.com/
autoInstallPeers: true
strictPeerDependencies: false
```
### Scripts Migration
Most scripts work unchanged. Update pnpm-specific patterns:
```json
{
"scripts": {
// npm: recursive scripts
"build:all": "npm run build --workspaces",
// pnpm: use -r flag
"build:all": "pnpm -r run build",
// npm: run in specific workspace
"dev:app": "npm run dev -w packages/app",
// pnpm: use --filter
"dev:app": "pnpm --filter @myorg/app run dev"
}
}
```
## CI/CD Migration
Update CI configuration:
```yaml
# Before (npm)
- run: npm ci
# After (pnpm)
- uses: pnpm/action-setup@v4
- run: pnpm install --frozen-lockfile # or: pnpm ci
```
Add to `package.json` for Corepack:
```json
{
"packageManager": "pnpm@10.0.0"
}
```
## Gradual Migration
For large projects, migrate gradually:
1. **Start with CI**: Use pnpm in CI, keep npm/yarn locally
2. **Add pnpm-lock.yaml**: Run `pnpm import` to create lockfile
3. **Test thoroughly**: Ensure builds work with pnpm
4. **Update documentation**: Update README, CONTRIBUTING
5. **Remove old files**: Delete old lockfiles after team adoption
## Rollback Plan
If migration causes issues:
```bash
# Remove pnpm files
rm -rf node_modules pnpm-lock.yaml pnpm-workspace.yaml
# Restore npm
npm install
# Or restore Yarn
yarn install
```
Keep old lockfile in git history for easy rollback.
<!--
Source references:
- https://pnpm.io/migration
- https://pnpm.io/migration-v11
- https://pnpm.io/cli/import
- https://pnpm.io/configuring
-->
references/best-practices-performance.md--- name: pnpm-performance-optimization description: Tips and tricks for faster installs and better performance --- # pnpm Performance Optimization pnpm is fast by default, but these optimizations can make it even faster. ## Install Optimizations ### Use Frozen Lockfile Skip resolution when lockfile exists: ```bash pnpm install --frozen-lockfile ``` This is faster because pnpm skips the resolution phase entirely. ### Prefer Offline Mode Use cached packages when available: ```bash pnpm install --prefer-offline ``` ### Skip Optional Dependencies If you don't need optional deps: ```bash pnpm install --no-optional # or `optional: false` in pnpm-workspace.yaml ``` On pnpm 12, `--force` still skips optional deps built for other platforms unless `forceIgnoresPlatform: true`; prefer `supportedArchitectures` to install other platforms on purpose. ### Skip Scripts For CI or when scripts aren't needed: ```bash pnpm install --ignore-scripts ``` **Caution:** Some packages require postinstall scripts to work correctly. ### Only Build Specific Dependencies Build-script approval is a single `allowBuilds` map (replaces `onlyBuiltDependencies`/`neverBuiltDependencies`). Only allowed packages run install scripts: ```yaml title="pnpm-workspace.yaml" allowBuilds: esbuild: true '@swc/core': true core-js: false # explicitly skip ``` Packages not listed are treated as unreviewed (blocked by default). See `features-supply-chain-security` for the full build-approval workflow. ## Store Optimizations ### Side Effects Cache Cache native module build results (enabled by default): ```yaml title="pnpm-workspace.yaml" sideEffectsCache: true ``` This caches the results of postinstall scripts, speeding up subsequent installs. Object form: `read`, `write`, `remote`, and `exclude` (v12.14.0) for packages whose build depends on things pnpm cannot key on, such as system libraries: ```yaml title="pnpm-workspace.yaml" sideEffectsCache: exclude: ['sharp@1.0.0 || 2.0.0', '@native/*'] # always built locally, never cached ``` Builds can also be shared across machines with `sideEffectsCache.remote` and the `remoteCache` setting (v12.12.0; a pnpr server or a Turborepo Remote Cache API server such as Vercel). Publishing and signing keys may only be set in global config or env. ### Global Virtual Store For many checkouts of the same repo (e.g. git worktrees / multiple agents), enable the global virtual store so each project's `node_modules` is just symlinks into one shared store — near-zero per-checkout cost. Auto-disabled in CI. Only applies with `nodeLinker: isolated`; `pnpm dlx`/`pnx` do not use it. ```yaml title="pnpm-workspace.yaml" virtualStoreType: global ``` ### Shared Store A single content-addressable store is used for all projects by default: ```yaml title="pnpm-workspace.yaml" storeDir: ~/.local/share/pnpm/store ``` Benefits: packages downloaded once, hard links save disk space, faster cached installs. ### Store Maintenance Periodically clean unused packages: ```bash # Remove unreferenced packages pnpm store prune # Check store integrity pnpm store status ``` ## Workspace Optimizations ### Parallel Execution Run workspace scripts in parallel: ```bash pnpm -r --parallel run build ``` Control concurrency: ```yaml title="pnpm-workspace.yaml" workspaceConcurrency: 8 ``` ### Stream Output See output in real-time: ```bash pnpm -r --stream run build ``` ### Filter to Changed Packages Only build what changed: ```bash # Build packages changed since main branch pnpm --filter "...[origin/main]" run build ``` ### Topological Order Build dependencies before dependents: ```bash pnpm -r run build # Automatically runs in topological order ``` For explicit sequential builds: ```bash pnpm -r --workspace-concurrency=1 run build ``` ## Network Optimizations Network/registry settings are camelCase in `pnpm-workspace.yaml` (registry URLs may also go in `registries`): ```yaml title="pnpm-workspace.yaml" registries: default: https://registry.npmmirror.com/ fetchRetries: 3 fetchRetryMintimeout: 10000 fetchRetryMaxtimeout: 60000 networkConcurrency: 64 # auto: clamp(workers x 3, 64, 96) httpProxy: http://proxy.company.com:8080 httpsProxy: http://proxy.company.com:8080 ``` ## Lockfile Optimization ### Single Lockfile (Monorepos) Use shared lockfile for all packages (default): ```yaml title="pnpm-workspace.yaml" sharedWorkspaceLockfile: true ``` Benefits: - Single source of truth - Faster resolution - Consistent versions across workspace ### Lockfile-only Mode Only update lockfile without installing: ```bash pnpm install --lockfile-only ``` ## Benchmarking ### Compare Install Times ```bash # Clean install rm -rf node_modules pnpm-lock.yaml time pnpm install # Cached install (with lockfile) rm -rf node_modules time pnpm install --frozen-lockfile # With store cache time pnpm install --frozen-lockfile --prefer-offline ``` ### Profile Resolution Debug slow installs: ```bash # Verbose logging pnpm install --reporter=append-only # Debug mode DEBUG=pnpm:* pnpm install ``` ## Configuration Summary Optimized `pnpm-workspace.yaml` for performance: ```yaml title="pnpm-workspace.yaml" # Install behavior autoInstallPeers: true sideEffectsCache: true optimisticRepeatInstall: true # Build approval (only what's necessary) allowBuilds: esbuild: true '@swc/core': true # Network fetchRetries: 3 networkConcurrency: 64 # Workspace workspaceConcurrency: 4 # Many checkouts of the same repo virtualStoreType: global ``` ## Quick Reference | Scenario | Command/Setting | |----------|-----------------| | CI installs | `pnpm ci` / `pnpm install --frozen-lockfile` | | Offline development | `--prefer-offline` | | Control native builds | `allowBuilds` map | | Parallel workspace | `pnpm -r --parallel run build` | | Build changed only | `pnpm --filter "...[origin/main]" build` | | Clean store | `pnpm store prune` | | Many worktrees/agents | `virtualStoreType: global` | <!-- Source references: - https://pnpm.io/settings - https://pnpm.io/cli/install - https://pnpm.io/filtering - https://pnpm.io/global-virtual-store -->
references/core-cli.md--- name: pnpm-cli-commands description: Essential pnpm commands for package management, running scripts, workspaces, publishing, and runtimes --- # pnpm CLI Commands pnpm provides a comprehensive CLI. Commands resemble npm/yarn but with unique features. > **pnpm v12 is a Rust rewrite** of v11, stable, keeping v11's commands, flags, settings, and lockfile format. A few behaviors differ (see best-practices-migration). ## Installation Commands ```bash pnpm install # install all deps (alias: pnpm i) pnpm add <pkg> # production dependency pnpm add -D <pkg> # devDependency (also -d) pnpm add -O <pkg> # optionalDependency (also -o) pnpm add -E <pkg> # exact version (also -e) pnpm add <pkg>@<version> pnpm add <alias>@npm:<pkg>[@<version>] # install under another local name pnpm remove <pkg> # aliases: rm, uninstall, un pnpm update # alias: up pnpm update --latest # ignore semver ranges (-L) pnpm update -i # interactive pnpm update --depth 0 # direct deps only ``` ### Clean / reproducible installs ```bash pnpm install --frozen-lockfile # fail if lockfile would change (auto in CI) pnpm ci # clean install = pnpm clean + install --frozen-lockfile pnpm clean # remove node_modules in all workspace projects (alias: purge) pnpm clean --lockfile # also delete pnpm-lock.yaml pnpm install --no-frozen-lockfile # allow lockfile updates (e.g. in CI) pnpm install --lockfile-only # update lockfile without node_modules (also on add) pnpm install --allow-build=esbuild --allow-build='!core-js' # (v12.7+) allow/deny build scripts, recorded in allowBuilds ``` > Since v11, an integrity mismatch against the lockfile is a hard error (`ERR_PNPM_TARBALL_INTEGRITY`). Use `pnpm install --update-checksums` only after verifying the new bytes. Since v12.7, `--force` no longer installs optional deps whose os/cpu/libc mismatch the host (`forceIgnoresPlatform: true` restores it). In CI, pnpm also fails on lockfiles written by a newer pnpm major. ## Script Commands ```bash pnpm run <script> # or just: pnpm <script> pnpm run build -- --watch pnpm run --if-present build pnpm set-script test "vitest run" # add/update a scripts entry (alias: ss) pnpm exec <cmd> # run a local binary, e.g. pnpm exec eslint . pnpm exec -r <cmd> # in every workspace project (pnpm options may precede the command name) pnpm run "/^watch:/" # regex selector: run all matching scripts pnpm run -s <script> # -s = --sequential for run (and `pnpm -s <script>`), unlike elsewhere ``` - **Hidden scripts:** names starting with `.` (e.g. `.helper`) can't be run directly, only called from other scripts. - **Built-in vs script conflict:** `clean`, `setup`, `deploy`, `rebuild`, `tasks` prefer a same-named `package.json` script. Force the built-in with `pnpm pm <name>` (e.g. `pnpm pm clean`). ### dlx / pnx — run without installing ```bash pnx create-vite my-app # pnx == pnpm dlx == pnpx pnpm dlx degit user/repo dest pnx shx@catalog: # catalog: protocol supported pnx --package=@scope/tool tool --help ``` > `dlx`/`pnx` honor supply-chain settings (`minimumReleaseAge`, `trustPolicy`) and use the global virtual store by default. In an interactive terminal they prompt to approve a dependency's skipped build scripts (or use `--allow-build`). ### Run another package manager / runtime with pnx (v12) Naming a package manager (`pnpm`, `npm`, `yarn`, `bun`) or runtime (`node`, `deno`, `bun`) provisions the real thing, not the npm package of that name: ```bash pnx yarn@4 install pnx npm@11 ci pnx node@22 --version pnx --cpu arm64,x64 --os linux some-cli # pick platform-tagged optional deps (also --libc) pnx yarn@npm:yarn@1.22.22 # a specifier that locates a package installs it unchanged ``` ## Workspace Commands ```bash pnpm -r run <script> # run in all packages (alias: --recursive) pnpm --filter <pattern> run <script> pnpm --filter "./packages/**" run build pnpm --filter "@myorg/*" run lint pnpm -r --parallel run dev ``` ### Filter patterns ```bash pnpm --filter <pkg-name> <cmd> # by name (-F shorthand) pnpm --filter "./packages/core" test pnpm --filter "...@scope/app" build # package + its dependencies pnpm --filter "@scope/core..." test # package + its dependents pnpm --filter "...[origin/main]" build # changed since merge base with ref (v12.7+) pnpm --filter "!./packages/**" --filter @scope/core build # filters apply in order (v12.7+) ``` ## Patches ```bash pnpm patch <pkg>@<version> # opens an editable copy, prints a path pnpm patch-commit <path> # writes patches/*.patch and records it pnpm patch-remove <pkg>@<version> ``` ## Linking local packages ```bash pnpm link <dir>... # link paths into node_modules (path only!); also writes a link: dep and a root `overrides` entry pnpm add -g . # register the current package's bins globally ``` > Breaking in v11: `pnpm link` accepts **only relative/absolute paths** (no global store resolution, no `--global`, no bare `pnpm link`). Use `pnpm add -g .` to expose bins system-wide. `pnpm unlink` removes the overrides. ## Global packages (v11 isolated installs) ```bash pnpm add -g typescript prettier # each gets its own isolated install dir pnpm add -g eslint,prettier # comma = ONE shared install group pnpm add -g --allow-build=esbuild esbuild pnpm remove -g <pkg> pnpm list -g pnpm bin -g # show global bin dir ($PNPM_HOME/bin) ``` > `pnpm install -g` (no args) is not supported. After upgrading run `pnpm setup` so `$PNPM_HOME/bin` is on PATH. ## Project-aware command shims (v12) Global `node`/`deno`/`bun` (and shimmed tools) run the version the current project pins. Create shims for tools with no global install behind them: ```bash pnpm shim add yarn # `yarn` runs whatever version the current project pins pnpm shim ls pnpm shim rm yarn ``` > Shims are never written as a side effect of `pnpm setup`/install (a shim shadows PATH). Governed by the `globalShims` setting. See features-global-virtual-store. ## Runtimes (Node/Deno/Bun) ```bash pnpm runtime set node 22 -g # install & expose node (alias: rt) pnpm runtime set node lts -g pnpm runtime set deno 2 -g pnpm runtime set node 22 # no -g: add to project devEngines.runtime (-P: engines.runtime) pnpm install --no-runtime # skip installing devEngines.runtime entries ``` The Node runtime does not bundle `npm`/`npx`/`corepack`; install npm with `pnpm add -g npm`. ### Rust toolchain (v12.11+) ```bash pnpm add rust@1.95 # pin in rust-toolchain.toml (channel: version, stable, beta, nightly, nightly-YYYY-MM-DD) pnpm add -g rust@stable # global toolchain; bare cargo/rustc use project-pinned one pnx --package=rust@nightly-2026-01-01 cargo build # one-off release (v12.11.1) ``` See features-multi-ecosystem. ## Store management ```bash pnpm store path # store location (prints removed size after prune) pnpm store prune # GC unreferenced packages (+ global virtual store links) pnpm store status ``` ## Inspection / registry ```bash pnpm list # alias: ls pnpm why <pkg> # reverse-dependency tree (dedupes subtrees) pnpm why --find-by=<finder> # custom finder from .pnpmfile.mjs pnpm outdated pnpm audit pnpm peers check # report unmet/missing peers from the lockfile pnpm view <pkg> [field] # registry metadata (aliases: info, show) pnpm whoami pnpm rebuild pnpm import # create pnpm-lock.yaml from npm/yarn lockfile pnpm dedupe ``` ## Publishing ```bash pnpm pack pnpm publish -r --no-git-checks pnpm version patch|minor|major|2.0.0 # bump version, commit + tag pnpm version prerelease --preid beta pnpm version patch --dry-run # preview (also works with -r) pnpm deprecate <pkg>@<range> "message" pnpm dist-tag add <pkg>@<version> <tag> pnpm unpublish <pkg>@<version> # discouraged; prefer deprecate pnpm sbom --sbom-format cyclonedx # SBOM: cyclonedx (1.7) | spdx (2.3); needs lockfile, manifest optional pnpm stage publish ... # staged publishing (defer 2FA) pnpm token list|create|revoke <id> # registry auth tokens (v12.13+; create: --read-only, --cidr) pnpm profile get|set|enable-2fa|disable-2fa # registry profile (set password prompts; never pass on CLI) ``` ## Release management (native, v11.13+) ```bash pnpm change # record a change intent in .changeset/ pnpm change status # pending intents + release plan pnpm change check # validate committed versions vs epics/fixed groups (CI) pnpm version -r [--dry-run] # consume intents: bump, changelog, ledger (no git tag) pnpm lane <name> --filter <p> # move package(s) onto a release lane ``` See features-versioning for the full workflow. ## Task orchestration & pipelines ```bash pnpm -r run <script> # runs the tasks graph (see features-task-orchestration) pnpm -r run --dry-run build # inspect the graph pnpm tasks status # running/waiting tasks per concurrency group (v12.6) pnpm pipeline [name] # cached CI-style run (v12.4, experimental; `remoteCache` shares results) ``` ## Cache (registry metadata) ```bash pnpm cache path pnpm cache prune pnpm cache view <pkg> ``` ## Maintenance & version management ```bash pnpm self-update [<version>] # updates the packageManager pin, or installs globally pnpm with current install # run a specific pnpm version for one command pnpm with 11.0.0 install pnpm approve-builds [--all] # review dependency build scripts (writes allowBuilds) pnpm approve [--all] [pkg] [!pkg] # = pnpm permissions approve (v12.11+): build scripts AND agent skills pnpm permissions [list] # granted/denied permissions + packages awaiting approval ``` ## Useful Flags ```bash pnpm install --ignore-scripts pnpm install --prefer-offline pnpm install --prod # -P, omit devDependencies pnpm install --no-optional pnpm install --strict-peer-dependencies ``` ## Key Points - `pnpm ci` = clean + frozen install; CI auto-enables frozen-lockfile. - `dlx`/`pnpx` are aliases of `pnx`; global installs are now isolated per package (comma-list to share). - `pnpm link` only takes paths; use `pnpm add -g .` for global bins. - Manage Node/Deno/Bun with `pnpm runtime set`; skip them at install with `--no-runtime`. - Publishing/registry commands: `version`, `view`, `whoami`, `deprecate`, `dist-tag`, `unpublish`, `sbom`, `stage`. - v12 adds native releases (`change`, `version -r`, `lane`), task inspection (`tasks status`), `pipeline`, `shim`, and `cache` commands; `pnx` can run other package managers/runtimes. v12.11+ adds `pnpm permissions`/`approve` (also agent skills), `rust` toolchain support; v12.13+ adds `pnpm token`. <!-- Source references: - https://pnpm.io/cli/install - https://pnpm.io/cli/add - https://pnpm.io/cli/run - https://pnpm.io/filtering - https://pnpm.io/cli/link - https://pnpm.io/global-packages - https://pnpm.io/cli/runtime - https://pnpm.io/cli/version - https://pnpm.io/cli/with - https://pnpm.io/cli/sbom - https://pnpm.io/cli/change - https://pnpm.io/cli/lane - https://pnpm.io/cli/tasks - https://pnpm.io/cli/pipeline - https://pnpm.io/cli/shim - https://pnpm.io/cli/pnx - https://pnpm.io/cli/permissions - https://pnpm.io/cli/profile - https://pnpm.io/cli/token - https://pnpm.io/cli/exec -->
references/core-config.md---
name: pnpm-configuration
description: Configuring pnpm via pnpm-workspace.yaml (settings), the global config.yaml, and .npmrc (auth only)
---
# pnpm Configuration
pnpm settings are split into **two** categories. Knowing where each goes is the single most important config concept in current pnpm:
| Category | Stored in | Format |
|----------|-----------|--------|
| **All pnpm/install settings** (`nodeLinker`, `hoistPattern`, `autoInstallPeers`, `overrides`, `catalog`, …) | `pnpm-workspace.yaml` (project) and `config.yaml` (global) | YAML, **camelCase** keys |
| **Auth, registry credentials, TLS, proxy, `local-address`** (`_authToken`, `cert`, `key`, …) | `.npmrc` (project, gitignored) and global `auth.ini` | INI |
> **Important changes:** pnpm no longer reads settings from the `pnpm` field of `package.json` (a setting found there is ignored with a warning), and `.npmrc` is now used **only** for authentication/registry credentials. Everything else belongs in `pnpm-workspace.yaml`. Keys in YAML are **camelCase** (e.g. `nodeLinker`), not the kebab-case used by old `.npmrc` files.
> The upstream settings reference is now split into pages by area — `settings/build`, `settings/cli`, `settings/dependency-resolution`, `settings/network`, `settings/node-modules`, `settings/store`, `settings/peer-dependencies`, `settings/versioning`, `settings/other` — but they are all the same camelCase keys in `pnpm-workspace.yaml`.
## pnpm-workspace.yaml (primary config)
Place at the workspace/project root. Even a single-package project uses this file for pnpm settings.
```yaml title="pnpm-workspace.yaml"
# Workspace packages (omit for a single-package repo)
packages:
- 'packages/*'
- 'apps/*'
- '!**/test/**'
# Common install settings (camelCase)
nodeLinker: isolated # isolated (default) | hoisted | pnp
autoInstallPeers: true
strictPeerDependencies: false
savePrefix: '^'
saveExact: false
hoistPattern:
- '*eslint*'
- '*babel*'
publicHoistPattern: []
shamefullyHoist: false
dedupeDirectDeps: false
resolutionMode: highest # highest | time-based | lowest-direct
# Centralized version management
catalog:
react: ^18.2.0
# Force dependency versions (root only)
overrides:
lodash: ^4.17.21
'foo@^1.0.0>bar': ^2.0.0
# Extend/patch broken package manifests
packageExtensions:
react-redux:
peerDependencies:
react-dom: '*'
# Peer dependency rules
peerDependencyRules:
ignoreMissing:
- '@babel/*'
allowedVersions:
react: '17 || 18'
```
## Global configuration (config.yaml)
User-level non-auth settings live in a global YAML `config.yaml`:
- `$XDG_CONFIG_HOME/pnpm/config.yaml` (if set)
- Linux: `~/.config/pnpm/config.yaml`
- macOS: `~/Library/Preferences/pnpm/config.yaml`
- Windows: `%LOCALAPPDATA%\pnpm\config\config.yaml`
Auth INI settings go in `auth.ini` in the same directory (written by `pnpm login`/`pnpm config set` of auth keys).
Project-only settings (structure of `node_modules`/workspace: `nodeLinker`, `hoistPattern`, `publicHoistPattern`, `shamefullyHoist`, `linkWorkspacePackages`, ...) must be in `pnpm-workspace.yaml`; in the global `config.yaml` they are ignored with a warning, and `pnpm config set --global` fails with `ERR_PNPM_CONFIG_SET_UNSUPPORTED_YAML_CONFIG_KEY`. Machine-level settings (`storeDir`, `cacheDir`, `packageImportMethod`, `minimumReleaseAge`, network) may be global. Share project-only settings across repos via a config dependency. Conversely, machine-state settings (`bin`, `configDir`, `dir`, `globalBinDir`, `globalDir`, `macosBackup`, `npmrcAuthFile`, `pnpmHomeDir`, `scope`, `stateDir`, `userconfig`, `workspaceDir`, `ci`, `symlinkedLockfileTargetDirs`, `tools`, ...) are ignored in a project's `pnpm-workspace.yaml` (`pnpm config set --location=project` fails with `ERR_PNPM_CONFIG_SET_NOT_A_PROJECT_SETTING`). `cacheDir`/`storeDir` are fine.
## Splitting settings across files (config includes, v12.15+)
The root `pnpm-workspace.yaml` can delegate settings to other YAML files and to config dependencies via `include`, so protected files (CODEOWNERS, read-only mounts, agent sandboxes) hold security settings while routine ones (catalogs) live in freely editable files.
```yaml title="pnpm-workspace.yaml"
configDependencies:
'@acme/pnpm-policy': 3.1.0
pnpmfile: [] # no pnpmfile: stop auto-picking up a new .pnpmfile.mjs
include:
./config/security.yaml: '*' # any setting no other source owns
./config/catalog.yaml: [catalog] # list = permits AND reserves these settings
./patches/patches.yaml: [patchedDependencies]
'@acme/pnpm-policy': [minimumReleaseAge, trustPolicy] # config dep: list required
```
- Sources: local `./` or `../` `.yaml/.yml` files (must exist; no globs/absolute/URLs) or a name declared in `configDependencies`. Only the root file may define `include`; `include` cannot be delegated. Bad sources: `ERR_PNPM_INVALID_WORKSPACE_INCLUDE`.
- One owner per setting: a duplicate claim fails with `ERR_PNPM_DUPLICATE_SETTING`; defining a non-delegated setting in a file fails with `ERR_PNPM_SETTING_NOT_DELEGATED`. A delegated setting its owner omits is simply unset by the project.
- Catalogs: `catalog`/`catalogs.default`, `catalogs.<name>`, or `catalogs` (all). Entries of one catalog are never split.
- Relative paths inside an included file (`patchedDependencies`, `patchesDir`, `packages`, `file:`/`link:` entries, ...) are relative to that file.
- Writes (`pnpm add` with catalog, `approve-builds`, `patch-commit`, `audit --fix`, `config set --location project`) go to the owning file; unowned settings go to `pnpm-workspace.yaml`; config-dep-owned settings fail (`ERR_PNPM_SETTING_OWNED_BY_CONFIG_DEPENDENCY`).
- A config dependency supplies settings declaratively through `"pnpm": { "config": "./config.yaml" }` in its `package.json` (file must stay inside the package, else `ERR_PNPM_INVALID_CONFIG_DEPENDENCY_ENTRY`). Not delegable to config deps: `configDependencies`, `pnpmfile`, `globalPnpmfile`, `bootConfig`, `packages`, `scriptShell`, path settings (`patchedDependencies`, `storeDir`, `lockfileDir`).
- `bootConfig` sets the policy used when installing config dependencies / the pnpm version (before their settings load): accepts `minimumReleaseAge`, `minimumReleaseAgeExclude`, `minimumReleaseAgeStrict`, `minimumReleaseAgeIgnoreMissingTime`, `trustPolicy`, `trustPolicyExclude`, `trustPolicyIgnoreAfter`. Unset falls back to the regular value.
- An `updateConfig` hook runs after merge and can still change anything.
## Per-project settings in a workspace (packageConfigs)
There are no per-subproject `.npmrc` files anymore. Set per-package config via `packageConfigs` in the root `pnpm-workspace.yaml`:
```yaml title="pnpm-workspace.yaml"
packageConfigs:
# Map form: keyed by package name
project-1:
saveExact: true
project-2:
savePrefix: '~'
# Array form: pattern-matched rules
# - match: ['project-1', 'project-2']
# modulesDir: node_modules
# saveExact: true
```
## .npmrc — authentication only
Keep auth tokens out of the repo (gitignore the project `.npmrc`). Auth INI files, highest priority first (override the project-trusted file with `npmrcAuthFile`):
1. `<workspace root>/.npmrc` (project, gitignored)
2. `<pnpm config>/auth.ini` (written by `pnpm login` since v12.16, and by `pnpm config set` of auth keys)
3. `~/.npmrc` (fallback for npm compatibility)
```ini title=".npmrc"
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
@myorg:registry=https://npm.myorg.com/
//npm.myorg.com/:_authToken=${MYORG_TOKEN}
```
### Structured `_auth` (v11.10+, for CI)
An alternative to many `//host/:_authToken=…` lines, keyed by registry URL. Honored **only** from the global `config.yaml` and the `pnpm_config__auth` (or `PNPM_CONFIG__AUTH`) env var - **ignored** in a project `.npmrc`/`pnpm-workspace.yaml`. `authToken` is the only field and may use `${NAME}`. Each entry also routes packages: `"@"` sets the default registry, `"@org"` routes that scope; priority is CLI flags, then `pnpm_config__auth` (overrides `registry`/`registries`), then global `config.yaml` (only where no `registry`/`registries` chooses). `pnpm login` wrote here in v12.1-12.15; since v12.16 it saves to `auth.ini`.
```yaml title="config.yaml (global only)"
_auth:
https://registry.npmjs.org:
"@": { authToken: npm-token } # @ = registry-wide/default
"@org": { authToken: org-token } # scope-bound on the same host
```
```sh
export pnpm_config__auth='{"https://registry.npmjs.org":{"@":{"authToken":"npm-token"}}}'
```
Configure registries themselves (non-secret) in `pnpm-workspace.yaml`:
```yaml title="pnpm-workspace.yaml"
registries:
default: https://registry.npmjs.org/
'@my-org': https://private.example.com/
# Newer shape: keyed by URL, with scopes and a bare-specifier prefix (`pnpm add work:@corp/lib`)
# registries:
# https://npm.work.example.com/:
# scopes: ['@corp']
# prefix: work
```
(`namedRegistries` is deprecated in favor of `prefix` in `registries`.)
Per-scope tokens: `//npm.pkg.github.com/:_authToken` plus scoped keys; `tokenHelper=/abs/path` (user-level auth files only, absolute path, no args) prints a fresh token for expiring credentials.
> Security: env-variable expansion is disabled for registry/proxy URLs, `//` keys, and credentials (`_authToken`, `_auth`, `_password`, `username`, `tokenHelper`, `cert`, `key`) in the **project** `.npmrc` (such settings are ignored with a warning). Put `${NPM_TOKEN}` lines in `auth.ini`/`~/.npmrc`, use `_auth`, or pass env var `pnpm_config_//registry.npmjs.org/:_authToken=...` via `env` (shells cannot `export` that name). Last resort for trusted CI: `PNPM_CONFIG_NPMRC_AUTH_FILE=.npmrc`. Likewise env vars are not expanded in `registry`/`registries`/`userAgent` values in `pnpm-workspace.yaml`.
## The `pnpm config` command
```bash
# Writes to global config.yaml / rc by default
pnpm config set nodeVersion 22.0.0
pnpm config set --location=project nodeVersion 22.0.0 # writes pnpm-workspace.yaml
# JSON values create arrays/objects
pnpm config set --location=project --json allowBuilds '{"react": true}'
# get prints strings/numbers/booleans as plain text, objects/arrays as JSON (`--json` forces JSON); list prints JSON
pnpm config get nodeLinker
pnpm config get 'allowBuilds.react'
pnpm config list
```
## Environment variables
Use `pnpm_config_*` (or `PNPM_CONFIG_*`). pnpm **no longer reads `npm_config_*`**, except `NPM_CONFIG_WORKSPACE_DIR`, `npm_config_userconfig`, and URL-scoped credentials (`npm_config_//host/:_authToken`). In config files, `${NAME}`, `${NAME-fallback}`, `${NAME:-fallback}` expand; `${NAME?}` expands to empty silently (v12.7+).
```bash
pnpm_config_save_exact=true pnpm add foo
```
## Notable settings that changed names
| Old (removed) | Replacement | Notes |
|---------------|-------------|-------|
| `onlyBuiltDependencies`, `neverBuiltDependencies`, `ignoredBuiltDependencies`, `onlyBuiltDependenciesFile` | `allowBuilds: { name: true\|false }` | Single map controlling build-script approval. See supply-chain-security. |
| `managePackageManagerVersions`, `packageManagerStrict`, `packageManagerStrictVersion`, `COREPACK_ENABLE_STRICT` | `pmOnFail: download\|ignore\|warn\|error` | Behavior when running pnpm version ≠ declared one. |
| `useNodeVersion` | `devEngines.runtime` (in `package.json`) | Runtime pinning. |
| `auditConfig.ignoreCves` | `auditConfig.ignoreGhsas` | Use GHSA IDs. |
| `allowNonAppliedPatches` | `allowUnusedPatches` | `ignorePatchFailures` removed (patches now always throw). |
| `package.json#pnpm` field | `pnpm-workspace.yaml` | No longer read at all. |
## Package Manager / Runtime pinning (package.json)
```json
{
"packageManager": "pnpm@10.0.0",
"devEngines": {
"packageManager": { "name": "pnpm", "version": ">=11.0.0 <12.0.0", "onFail": "download" },
"runtime": { "name": "node", "version": "22.x", "onFail": "download" }
}
}
```
`devEngines.packageManager` supports ranges (resolved version stored in lockfile); `packageManager` requires an exact version. Declaring `packageManager` with another manager (`npm`, `yarn`, `bun`) is allowed. With no lockfile pin yet, pnpm records the running version if it satisfies the range and is older than `minimumReleaseAge`, else the newest old-enough version. Override `onFail` without editing the manifest via `pmOnFail` / `runtimeOnFail` settings.
## Key Points
- All pnpm settings go in `pnpm-workspace.yaml` (camelCase) or global `config.yaml`; `.npmrc` is auth/registry only.
- `package.json#pnpm` and `npm_config_*` env vars are no longer read.
- Use `packageConfigs` for per-package settings inside a workspace.
- Build-script approval is now one `allowBuilds` map; package-manager strictness is one `pmOnFail` setting.
- `pnpm config get`/`list` output JSON, and `--location=project` writes to `pnpm-workspace.yaml`.
<!--
Source references:
- https://pnpm.io/settings
- https://pnpm.io/configuring
- https://pnpm.io/npmrc
- https://pnpm.io/pnpm-workspace_yaml
- https://pnpm.io/package_json
- https://pnpm.io/cli/config
- https://pnpm.io/config-includes
-->
references/core-store.md---
name: pnpm-store
description: Content-addressable storage system that makes pnpm fast and disk-efficient
---
# pnpm Store
pnpm uses a content-addressable store to save disk space and speed up installations. All packages are stored once globally and hard-linked to project `node_modules`.
## How It Works
1. **Global Store**: Packages are downloaded once to a central store
2. **Hard Links**: Projects link to store instead of copying files
3. **Content-Addressable**: Files are stored by content hash, deduplicating identical files
### Storage Layout
```
<store-dir>/ # Global content-addressable store (pnpm store path)
└── files/
└── <hash>/ # Files stored by content hash
project/
└── node_modules/
├── .pnpm/ # Virtual store (hard links to global store)
│ ├── lodash@4.17.21/
│ │ └── node_modules/
│ │ └── lodash/
│ └── express@4.18.2/
│ └── node_modules/
│ ├── express/
│ └── <deps>/ # Flat structure for dependencies
├── lodash -> .pnpm/lodash@4.17.21/node_modules/lodash
└── express -> .pnpm/express@4.18.2/node_modules/express
```
## Store Commands
```bash
# Show store location
pnpm store path
# Remove unreferenced packages
pnpm store prune
# Check lockfile packages for modifications (ERR_PNPM_MODIFIED_DEPENDENCY)
pnpm store status
# Add package (or tarballs) to store without installing
pnpm store add <pkg>
pnpm store add ./tarballs/*.tgz && pnpm install --offline --frozen-lockfile # offline/sandboxed builds
```
- `store add` of tarballs: install uses one when the lockfile entry names the same package/version and pins that tarball (`integrity` may be sha512/sha1 of the tgz or the uncompressed tar hash from `lockfile.integrityTarget: tar`). For named-registry packages, run it in the declaring project.
- `store prune` also deletes expired `pnx` cache entries first, removes the package metadata cache (`metadata*` dirs; next install re-downloads), keeps undecodable index entries (reported in summary), and (v12.7+) removes private pnpm/runtime copies left by killed processes. With a global virtual store it mark-and-sweeps `links/` using registered projects (symlinks in `$(pnpm store path)/projects`; written by every `pnpm install` except `--frozen-store`/`enableModulesDir: false` without GVS, `--lockfile-only`, `--dry-run`); if no registered project links into `links/`, it is left unchanged.
## Configuration
Store/linker settings live in `pnpm-workspace.yaml` (camelCase), not `.npmrc`.
### Store Location
```yaml title="pnpm-workspace.yaml"
storeDir: ~/.local/share/pnpm/store
```
If no directory above the project accepts hard links (agent sandbox, writable bind mount), the store falls back to `node_modules/.pnpm-store` (workspace root in a workspace). Cross-process locks live in a per-user dir (`$XDG_RUNTIME_DIR` if usable, else `/tmp`; `~/AppData/Local` on Windows); a sandbox blocking `/tmp` can set `XDG_RUNTIME_DIR` (processes sharing a store should see the same value).
The default store path is OS-specific (e.g. `~/.local/share/pnpm/store` on Linux, `~/Library/pnpm/store` on macOS). Find it with `pnpm store path`.
### Virtual Store
The virtual store (`.pnpm` in `node_modules`) contains hard links to the global store:
```yaml title="pnpm-workspace.yaml"
virtualStoreDir: node_modules/.pnpm
virtualStoreDirMaxLength: 60 # lower this for long-path issues on Windows
nodeLinker: hoisted # alternative flat layout
```
### Store index format (v12.13+)
```yaml title="~/.config/pnpm/config.yaml"
storeIndexFormat: sqlite # default: one index.db, local disk only
# storeIndexFormat: files # one file per package in <store>/index; lock-free, for network stores shared by CI jobs
```
The format belongs to the store: once an `index` dir exists, every pnpm uses it; existing `index.db` entries are not carried over (re-download). Switch back by deleting `index`. pnpm v11 cannot read `files` stores.
## Disk Space Benefits
pnpm saves significant disk space:
- **Deduplication**: Same package version stored once across all projects
- **Content deduplication**: Identical files across different packages stored once
- **Hard links**: No copying, just linking
### Check disk usage
```bash
# Compare actual vs apparent size
du -sh node_modules # Apparent size
du -sh --apparent-size node_modules # With hard links counted
```
## Global Virtual Store
With `virtualStoreType: global`, projects skip the per-project `node_modules/.pnpm` directory entirely; their `node_modules` contains only symlinks into one shared virtual store at `<store-path>/links/`, keyed by dependency-graph hash. It is the default for global installs (`pnpm add -g`) only; `pnpm dlx`/`pnx` does NOT use it (each dlx cache entry keeps its own virtual store); for project installs it is still opt-in/experimental. Each project's `node_modules/.pnpm` still holds its current lockfile and hidden hoisted deps. Location: `<store-path>/links`, or set `globalVirtualStoreDir` (wins) or `virtualStoreDir`. Only applies with `nodeLinker: isolated`. Spawned processes get `NODE_PATH` plus a `NODE_OPTIONS --import` hook so hoisted lookups work in ESM too. It is no longer auto-disabled in CI. See `features-global-virtual-store` for details and the git-worktrees multi-agent workflow.
```yaml title="pnpm-workspace.yaml"
virtualStoreType: global # canonical spelling
# enableGlobalVirtualStore: true # equivalent, still works (virtualStoreType wins if both set)
```
## Node Linker Modes
Configure how `node_modules` is structured (`nodeLinker` in `pnpm-workspace.yaml`):
```yaml title="pnpm-workspace.yaml"
nodeLinker: isolated # default: symlinked virtual store (strict, no phantom deps)
# nodeLinker: hoisted # flat node_modules (npm-like) for tools that dislike symlinks
# nodeLinker: pnp # Plug'n'Play, no node_modules (set `symlink: false` too)
# nodeLinker: { type: loaded, excluded: [vitest] } # experimental store loader, see below
```
### Isolated Mode (Default)
- Strict dependency resolution
- No phantom dependencies
- Packages can only access declared dependencies
### Hoisted Mode
- Flat `node_modules` like npm
- For compatibility with tools that don't support symlinks
- Loses strictness benefits
### Loaded linker (experimental store loader)
`nodeLinker: { type: loaded }` lets Node load JS/JSON directly from the content-addressable store with no packages in `node_modules` (also enables the global virtual store). Setting `nodeLinker` replaces the whole value across config sources; env/CLI accept JSON (`PNPM_CONFIG_NODE_LINKER`, `--config.node-linker`).
```yaml title="pnpm-workspace.yaml"
nodeLinker:
type: loaded
excluded: # exact names; all versions + their full dep trees are materialized as normal GVS packages
- vitest
```
- Run `pnpm install`, then `pnpm run/test/exec` as usual: pnpm writes `node_modules/.pnpm/.store-manifest.json` and `.store-loader.mjs` and preloads the loader (child Node processes inherit via `NODE_OPTIONS`). Outside pnpm: `node --import ./node_modules/.pnpm/.store-loader.mjs app.mjs`.
- Requires Node 24.18+ (24.x) or 26.2+, else `ERR_PNPM_LOADER_UNSUPPORTED_NODE`.
- Patched packages, runtimes (`node@runtime:`), and anything with build scripts must be materialized (scripts packages need explicit `excluded` selection unless scripts are disabled; build approval still applies).
- Not combinable with `symlink: false` or `nodePackageMap.mode` other than `off`. Remove old `node_modules` when switching modes. Files are machine-local: reinstall after moving the project.
- Limits: no real filesystem (`__dirname`, `import.meta.url` are virtual; `fs` reads of adjacent assets fail), no native addons, WASM, TypeScript, or package-internal symlinks. Packages needing these must be put in `excluded`.
## Package map (v12.13+)
```yaml title="pnpm-workspace.yaml"
nodePackageMap:
mode: off # off (default) | write-only (write node_modules/.package-map.json) | load (also add --experimental-package-map to NODE_OPTIONS)
# type: loose
```
Replaces deprecated `nodeExperimentalPackageMap` (`true` = `mode: load`) and `nodePackageMapType`.
## Side Effects Cache
Cache build outputs for native modules (enabled by default):
```yaml title="pnpm-workspace.yaml"
sideEffectsCache: true
sideEffectsCacheReadonly: false # only read the cache, don't create it
```
## Read-only / Frozen Store
`frozenStore: true` lets `pnpm install` run against a read-only store (Nix store, read-only bind mount, OCI layer). Pair with `--offline --frozen-lockfile`; the store must already contain everything, including approved build outputs.
```bash
pnpm install --frozen-store --offline --frozen-lockfile
```
No other process may modify the store during a frozen-store install (immutable reads bypass SQLite locks and can report a malformed database). Missing builds fail with `ERR_PNPM_FROZEN_STORE_NEEDS_BUILD`, a missing content dir with `ERR_PNPM_FROZEN_STORE_INCOMPLETE`. Leave it off for ordinary parallel installs sharing a local store.
## Shared Store Across Machines
For CI/CD, you can share the store:
```yaml
# GitHub Actions example
- uses: pnpm/action-setup@v4
with:
run_install: false
- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
```
## Troubleshooting
### Store corruption
```bash
# Verify and fix store
pnpm store status
pnpm store prune
```
### Hard link issues (network drives, Docker)
```yaml title="pnpm-workspace.yaml"
# auto (default): on Linux (v12) tries hardlink -> clone -> copy; macOS is clone-first (APFS)
packageImportMethod: copy # copy | clone | clone-or-copy | hardlink
```
Cloning shares data with the store until written (safe to edit). A package a lifecycle script or patch will write is imported with `clone-or-copy` under `auto`/`hardlink`/`clone`. On macOS pnpm writes with 4 threads; on machines with Endpoint Security software try `PNPM_MACOS_LINK_THREADS=0`.
> v12 change: on Linux `packageImportMethod: auto` hardlinks before reflinking (roughly halves materialize time on btrfs). Use `clone` (or `clone-or-copy`) if you edit files inside `node_modules`, since a hardlinked file *is* the store's file.
### Permission issues
```bash
# Fix store permissions (find the path with `pnpm store path`)
chmod -R u+w "$(pnpm store path)"
```
<!--
Source references:
- https://pnpm.io/symlinked-node-modules-structure
- https://pnpm.io/cli/store
- https://pnpm.io/settings/store
- https://pnpm.io/settings/node-modules
- https://pnpm.io/global-virtual-store
- https://pnpm.io/store-loader
- https://pnpm.io/settings/store#storeindexformat
-->
references/core-workspaces.md---
name: pnpm-workspaces
description: Monorepo support with workspaces for managing multiple packages
---
# pnpm Workspaces
pnpm has built-in support for monorepos (multi-package repositories) through workspaces.
## Setting Up Workspaces
Create `pnpm-workspace.yaml` at the repository root:
```yaml
packages:
# Include all packages in packages/ directory
- 'packages/*'
# Include all apps
- 'apps/*'
# Include nested packages
- 'tools/*/packages/*'
# Exclude test directories
- '!**/test/**'
```
### Converting from `workspaces`
If the root `package.json` has a `workspaces` field (npm/Yarn/Bun) and there's no `pnpm-workspace.yaml`, `pnpm install` creates one with the same patterns. An existing file is never changed (pnpm warns on mismatch); `--ignore-workspace` skips creation.
## Workspace Protocol
Use `workspace:` protocol to reference local packages:
```json
{
"dependencies": {
"@myorg/utils": "workspace:*",
"@myorg/core": "workspace:^",
"@myorg/types": "workspace:~"
}
}
```
### Protocol Variants
| Protocol | Behavior | Published As |
|----------|----------|--------------|
| `workspace:*` | Any version | Actual version (e.g., `1.2.3`) |
| `workspace:^` | Compatible version | `^1.2.3` |
| `workspace:~` | Patch version | `~1.2.3` |
| `workspace:^1.0.0` | Semver range | `^1.0.0` |
## Filtering Packages
Run commands on specific packages using `--filter`:
```bash
# By package name
pnpm --filter @myorg/app build
pnpm -F @myorg/app build
# By directory path
pnpm --filter "./packages/core" test
# Glob patterns
pnpm --filter "@myorg/*" lint
pnpm --filter "!@myorg/internal-*" publish
# All packages
pnpm -r build
pnpm --recursive build
```
### Dependency-based Filtering
```bash
# Package and all its dependencies
pnpm --filter "...@myorg/app" build
# Package and all its dependents
pnpm --filter "@myorg/core..." test
# Both directions
pnpm --filter "...@myorg/shared..." build
# Changed since git ref
pnpm --filter "...[origin/main]" test
pnpm --filter "[HEAD~5]" lint
```
Since v12.7.0, `[<since>]` compares against the merge base of the current branch and `<since>` (uncommitted changes still count; needs Git 2.24+), and a catalog version change in `pnpm-workspace.yaml` marks affected projects changed. Selectors are evaluated in order, so a later inclusion can restore a project an earlier exclusion removed; if the first selector is an exclusion, filtering starts from all projects. `--no-fail-if-no-match` overrides `failIfNoMatch: true`. Defaults can be set via `testPattern` and `changedFilesIgnorePattern` (string arrays).
## Workspace Commands
### Install dependencies
```bash
# Install all workspace packages
pnpm install
# Add dependency to specific package
pnpm --filter @myorg/app add lodash
# Add workspace dependency
pnpm --filter @myorg/app add @myorg/utils
```
### Run scripts
```bash
# Run in all packages with that script
pnpm -r run build
# Recursive run/exec/test are topologically ordered by default (--no-sort disables)
pnpm -r run build
pnpm -r --workspace-concurrency=1 run build # one at a time
# Run in parallel
pnpm -r --parallel run test
# Stream output
pnpm -r --stream run dev
```
### Execute commands
```bash
# Run command in all packages
pnpm -r exec pwd
# Run in specific packages
pnpm --filter "./packages/**" exec rm -rf dist
```
## Workspace Settings
Configure in `pnpm-workspace.yaml` using **camelCase** keys (these settings no longer belong in `.npmrc`):
```yaml title="pnpm-workspace.yaml"
packages:
- 'packages/*'
# Link workspace packages automatically
linkWorkspacePackages: true
# Prefer workspace packages over registry
preferWorkspacePackages: true
# Single lockfile for the whole workspace (recommended)
sharedWorkspaceLockfile: true
# Workspace protocol handling on publish
saveWorkspaceProtocol: rolling
# Concurrent workspace scripts (default: min(4, CPU cores), container CPU quota aware; <=0 = cores minus abs value)
workspaceConcurrency: 4
# Use root deps to resolve peers of all projects
resolvePeersFromWorkspaceRoot: true
# Scripts required in every project (else `pnpm -r run <name>` fails)
requiredScripts:
- build
# Allow adding deps to the workspace root without -w
ignoreWorkspaceRootCheck: false
# Downgrade dependency-cycle warnings; also turns ERR_PNPM_TASK_CYCLE into a warning
ignoreWorkspaceCycles: false
```
> `linkWorkspacePackages: true` links a workspace project only where a project declares it directly; a transitive plain-range dep still comes from the registry. Use `deep` to link workspace projects into subdependencies too.
### Cross-project task graphs
`pnpm -r run <script>` schedules a dependency-aware task graph. Declare relationships under `tasks` (with `dependsOn`, `concurrencyGroups`, `priority`) and run cached CI-style `pnpm pipeline`. See `features-task-orchestration`.
```yaml title="pnpm-workspace.yaml"
tasks:
build:
dependsOn: ['^build'] # build each workspace dependency first
test:
dependsOn: ['build']
```
### Per-package configuration (packageConfigs)
There are no per-subproject `.npmrc` files. Set package-specific settings from the root file:
```yaml title="pnpm-workspace.yaml"
packageConfigs:
project-1:
saveExact: true
project-2:
savePrefix: '~'
```
## Versioning & releases
pnpm versions workspaces natively: `pnpm change` + `pnpm version -r` (see features-versioning).
## Publishing Workspaces
When publishing, `workspace:` protocols are converted:
```json
// Before publish
{
"dependencies": {
"@myorg/utils": "workspace:^"
}
}
// After publish
{
"dependencies": {
"@myorg/utils": "^1.2.3"
}
}
```
Use `--no-git-checks` for publishing from CI:
```bash
pnpm publish -r --no-git-checks
```
## Best Practices
1. **Use workspace protocol** for internal dependencies
2. **Enable `linkWorkspacePackages`** for automatic linking
3. **Use shared lockfile** for consistency
4. **Filter by dependencies** when building to ensure correct order
5. **Use catalogs** for shared external dependency versions (defined in this same file)
6. **Keep all pnpm settings in `pnpm-workspace.yaml`** (camelCase), not `.npmrc`
## Example Project Structure
```
my-monorepo/
├── pnpm-workspace.yaml
├── package.json
├── pnpm-lock.yaml
├── packages/
│ ├── core/
│ │ └── package.json
│ ├── utils/
│ │ └── package.json
│ └── types/
│ └── package.json
└── apps/
├── web/
│ └── package.json
└── api/
└── package.json
```
<!--
Source references:
- https://pnpm.io/workspaces
- https://pnpm.io/filtering
- https://pnpm.io/workspace-task-orchestration
- https://pnpm.io/cli/recursive
-->
references/features-aliases.md---
name: pnpm-aliases
description: Install packages under custom names for versioning, forks, or alternatives
---
# pnpm Aliases
pnpm supports package aliases using the `npm:` protocol. This lets you install packages under different names, use multiple versions of the same package, or substitute packages.
## Basic Syntax
```bash
pnpm add <alias>@npm:<package>@<version>
```
In `package.json`:
```json
{
"dependencies": {
"<alias>": "npm:<package>@<version>"
}
}
```
Selector form: `<alias>@npm:<package>[@<version-or-tag>]`, scoped targets allowed (`pnpm add babel-core@npm:@babel/core@8.0.6`). The alias is the name written to `package.json`; `npm:` here is the package-alias protocol, not a named registry. To replace a package everywhere in the graph, use `overrides`.
## Use Cases
### Multiple Versions of Same Package
Install different versions side by side:
```json
{
"dependencies": {
"lodash3": "npm:lodash@3",
"lodash4": "npm:lodash@4"
}
}
```
Usage:
```js
import lodash3 from 'lodash3'
import lodash4 from 'lodash4'
```
### Replace Package with Fork
Substitute a package with a fork or alternative:
```json
{
"dependencies": {
"original-pkg": "npm:my-fork@^1.0.0"
}
}
```
All imports of `original-pkg` will resolve to `my-fork`.
### Replace Deprecated Package
```json
{
"dependencies": {
"request": "npm:@cypress/request@^3.0.0"
}
}
```
### Scoped to Unscoped (or vice versa)
```json
{
"dependencies": {
"vue": "npm:@anthropic/vue@^3.0.0",
"@myorg/utils": "npm:lodash@^4.17.21"
}
}
```
## CLI Usage
### Add with alias
```bash
# Add lodash under alias
pnpm add lodash4@npm:lodash@4
# Add fork as original name
pnpm add request@npm:@cypress/request
```
### Add multiple versions
```bash
pnpm add react17@npm:react@17 react18@npm:react@18
```
## With TypeScript
For type resolution with aliases, you may need to configure TypeScript:
```json
// tsconfig.json
{
"compilerOptions": {
"paths": {
"lodash3": ["node_modules/lodash3"],
"lodash4": ["node_modules/lodash4"]
}
}
}
```
Or use `@types` packages with aliases:
```json
{
"devDependencies": {
"@types/lodash3": "npm:@types/lodash@3",
"@types/lodash4": "npm:@types/lodash@4"
}
}
```
## Combined with Overrides
Force all transitive dependencies to use an alias:
```yaml
# pnpm-workspace.yaml
overrides:
"underscore": "npm:lodash@^4.17.21"
```
This replaces all `underscore` imports (including in dependencies) with lodash.
## Git and Local Aliases
Aliases work with any valid pnpm specifier:
```json
{
"dependencies": {
"my-fork": "npm:user/repo#commit",
"local-pkg": "file:../local-package"
}
}
```
## Registry Aliases (registries `prefix`)
Distinct from package aliases: a registry `prefix` selects *which registry* a package is fetched from. `namedRegistries` is deprecated (still read, only for prefixes `registries` does not declare); declare `prefix` in `registries` instead.
```yaml title="pnpm-workspace.yaml"
registries:
https://npm.work.example.com/:
prefix: work
scopes: ["@corp"] # optional: also route the scope here
```
```bash
pnpm add work:@corp/lib@^2.0.0 # resolves @corp/lib against the work registry
```
Built-in prefixes: `gh:` (GitHub Packages) and `npmjs:` (public npm, pins a dep to the public registry even when `registry` points at a proxy; `npm:` cannot do that). Both can be overridden. Auth is reused from per-URL `.npmrc` entries. Reserved prefix names (`file`, `link`, `workspace`, `npm`, `jsr`, `git`, `catalog`, `http`, `pkg`, `tool`, ...) fail with `ERR_PNPM_RESERVED_NAMED_REGISTRY_NAME`.
Packages from a prefixed registry get registry-qualified lockfile keys (`foo@work:1.0.0`) so the same name@version from two registries never collapses. Every non-built-in prefix referenced by the lockfile must stay declared (`ERR_PNPM_MISSING_NAMED_REGISTRY`).
## Best Practices
1. **Clear naming**: Use descriptive alias names that indicate purpose
```json
"lodash-legacy": "npm:lodash@3"
"lodash-modern": "npm:lodash@4"
```
2. **Document aliases**: explain why aliases exist
3. **Prefer overrides for global replacement**: to replace a package everywhere, use `overrides` (in `pnpm-workspace.yaml`) instead of aliases
4. **Test thoroughly**: Aliased packages may have subtle differences in behavior
<!--
Source references:
- https://pnpm.io/aliases
- https://pnpm.io/settings#namedregistries
- https://pnpm.io/registries
-->
references/features-catalogs.md---
name: pnpm-catalogs
description: Centralized dependency version management for workspaces
---
# pnpm Catalogs
Catalogs provide a centralized way to manage dependency versions across a workspace. Define versions once, use everywhere.
## Basic Usage
Define a catalog in `pnpm-workspace.yaml`:
```yaml
packages:
- 'packages/*'
catalog:
react: ^18.2.0
react-dom: ^18.2.0
typescript: ~5.3.0
vite: ^5.0.0
```
Reference in `package.json` with `catalog:`:
```json
{
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:"
},
"devDependencies": {
"typescript": "catalog:",
"vite": "catalog:"
}
}
```
`catalog:` is shorthand for `catalog:default`. The `catalog:` protocol is valid in `package.json` `dependencies`, `devDependencies`, `peerDependencies`, and `optionalDependencies`, plus in `overrides` inside `pnpm-workspace.yaml`. It also works on the CLI: `pnpm add react@catalog:` and `pnx shx@catalog:`.
The default catalog can also be written as `catalogs.default`; defining both `catalog` and `catalogs.default` fails ("The 'default' catalog was defined multiple times").
## Named Catalogs
Create multiple catalogs for different scenarios:
```yaml
packages:
- 'packages/*'
# Default catalog
catalog:
lodash: ^4.17.21
# Named catalogs
catalogs:
react17:
react: ^17.0.2
react-dom: ^17.0.2
react18:
react: ^18.2.0
react-dom: ^18.2.0
testing:
vitest: ^1.0.0
"@testing-library/react": ^14.0.0
```
Reference named catalogs:
```json
{
"dependencies": {
"react": "catalog:react18",
"react-dom": "catalog:react18"
},
"devDependencies": {
"vitest": "catalog:testing"
}
}
```
## Adding to a catalog from the CLI
`pnpm add --save-catalog <dep>` writes the range to the default catalog and `catalog:` to `package.json`; `--save-catalog-name <name>` picks a named catalog. `catalogMode` controls whether `pnpm add` does this without the flag.
Errors: `ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC` (no entry for the dep), `ERR_PNPM_CATALOG_ENTRY_INVALID_RECURSIVE_DEFINITION` (an entry is itself a `catalog:` reference).
## workspace:, file:, and link: entries
A catalog entry may hold a `workspace:` range (v12.2.0), so the version a workspace dependency links by is defined once too. It expands to the range and links the project; on publish both protocols are replaced.
```yaml title="pnpm-workspace.yaml"
catalog:
'@example/utils': workspace:^
my-lib: link:./packages/my-lib # v12.6.0; path measured from pnpm-workspace.yaml
my-tarball: file:./tarballs/foo.tgz
```
## Keeping overrides in sync with a catalog
Reference a catalog from `overrides` so the version lives in exactly one place:
```yaml title="pnpm-workspace.yaml"
catalog:
foo: ^1.0.0
overrides:
foo: 'catalog:' # or catalog:<name>
```
## Settings
```yaml title="pnpm-workspace.yaml"
# How `pnpm add` interacts with the default catalog (v10.12+)
catalogMode: manual # manual (default) | prefer | strict
# strict: only catalog versions allowed; prefer: fall back if no match
catalogPrune: true # remove unused catalog entries on install (v11.22+; was cleanupUnusedCatalogs)
```
## Benefits
1. **Single source of truth**: Update version in one place
2. **Consistency**: All packages use the same version
3. **Easy upgrades**: Change version once, affects entire workspace
4. **Fewer merge conflicts**: package.json files stay untouched on upgrades
## Catalog vs Overrides
| Feature | Catalogs | Overrides |
|---------|----------|-----------|
| Purpose | Define versions for direct dependencies | Force versions for any dependency |
| Scope | Direct dependencies only | All dependencies (including transitive) |
| Usage | `"pkg": "catalog:"` | Applied automatically |
| Opt-in | Explicit per package.json | Global to workspace |
## Publishing with Catalogs
When publishing, `catalog:` references are replaced with actual versions:
```json
// Before publish (source)
{
"dependencies": {
"react": "catalog:"
}
}
// After publish (published package)
{
"dependencies": {
"react": "^18.2.0"
}
}
```
## Migration from Overrides
If you're using overrides for version consistency:
```yaml
# Before (using overrides)
overrides:
react: ^18.2.0
react-dom: ^18.2.0
```
Migrate to catalogs for cleaner dependency management:
```yaml
# After (using catalogs)
catalog:
react: ^18.2.0
react-dom: ^18.2.0
```
Then update package.json files to use `catalog:`. To migrate an existing workspace automatically:
```bash
pnpx codemod pnpm/catalog
```
## Best Practices
1. **Use default catalog** for commonly shared dependencies
2. **Use named catalogs** for version variants (e.g., different React versions)
3. **Keep catalog minimal** - only include shared dependencies
4. **Combine with workspace protocol** for internal packages
```yaml
catalog:
# External shared dependencies
lodash: ^4.17.21
zod: ^3.22.0
# Internal packages use workspace: protocol instead
# "dependencies": { "@myorg/utils": "workspace:^" }
```
<!--
Source references:
- https://pnpm.io/catalogs
-->
references/features-config-dependencies.md---
name: pnpm-config-dependencies
description: Share and centralize pnpm hooks, settings, patches, catalogs, and overrides across repos via config dependencies
---
# pnpm Config Dependencies
Config dependencies are npm packages that pnpm installs **before** all regular dependencies, so they can supply hooks, settings, patches, catalogs, and overrides that are reused across many repositories. They let you keep one shared "pnpm config" package and consume it everywhere.
## Declaring config dependencies
They live in `pnpm-workspace.yaml`; their integrity is recorded in a dedicated boot document inside `pnpm-lock.yaml`. They install to `node_modules/.pnpm-config/<name>`.
```yaml title="pnpm-workspace.yaml"
configDependencies:
my-configs: "^1.0.0"
```
Add one with the `--config` flag:
```bash
pnpm add --config my-configs
pnpm add --config @myorg/pnpm-plugin-my-catalogs
```
Since v12.12 the saved range follows `pnpm add` rules (`savePrefix`, `--save-exact`); a dist-tag is saved as a range on the resolved version. Remove one by deleting it from `configDependencies` and running `pnpm install` (without `--frozen-lockfile`).
## Updating
Since v12.12, `pnpm update` updates config dependencies like regular ones: `pnpm update` (newest within range, rewrites range in `pnpm-workspace.yaml`), `--latest`, or by name/pattern (`pnpm update my-configs`). Dist-tag declarations keep tracking the tag (only the lockfile moves). Skipped with `--interactive`, `--patches`, `--no-save`, `--filter`, dep-group flags (`--prod`), and when pinned with `version+integrity`.
## Registry and security
Must come from an npm registry and are verified against it before install (offline install of new/changed ones needs cached metadata). `minimumReleaseAge` applies unless pinned with `version+integrity`; they install before `updateConfig` runs, so `minimumReleaseAgeExclude` added by a hook does not cover them. Since v12.15, `bootConfig` sets a separate policy for installing them. A config dependency can also supply settings declaratively without a pnpmfile (see config-includes in `core-config`).
## Constraints
- **No regular `dependencies`.** They may declare `optionalDependencies`, but only one level deep.
- **No lifecycle scripts** (`preinstall`, `postinstall`, …).
- `optionalDependencies` (used for platform-specific binaries, esbuild-style) must use **exact** versions — ranges/tags fail with `ERR_PNPM_CONFIG_DEP_OPTIONAL_NOT_EXACT`, keeping installs reproducible.
## Auto-loaded plugins
A config dependency named `pnpm-plugin-*`, `@*/pnpm-plugin-*`, or `@pnpm/plugin-*` has its `pnpmfile.mjs` (or `.cjs`) loaded automatically from the package root.
## Use cases
### Import hook logic from a shared package
Because config deps install before the pnpmfile loads, you can import from them:
```js title=".pnpmfile.mjs"
import { readPackage } from './node_modules/.pnpm-config/my-hooks/index.mjs'
export const hooks = { readPackage }
```
### Share settings & catalogs via updateConfig
A plugin can inject settings/catalog entries through the `updateConfig` hook:
```js title="@myorg/pnpm-plugin-my-catalogs/pnpmfile.mjs"
export const hooks = {
updateConfig(config) {
config.catalogs.default ??= {}
config.catalogs.default['is-odd'] = '1.0.0'
return config
}
}
```
After installing it as a config dependency, consumers can use the catalog:
```bash
pnpm add is-odd@catalog: # installs is-odd@1.0.0, writes "is-odd": "catalog:"
```
### Share patch files
Reference patches stored inside a config dependency:
```yaml title="pnpm-workspace.yaml"
configDependencies:
my-patches: "1.0.0"
patchedDependencies:
react: "node_modules/.pnpm-config/my-patches/react.patch"
```
## Key Points
- Centralize hooks, settings, catalogs, overrides, and patches in one package, consumed across repos.
- Declared via `configDependencies` in `pnpm-workspace.yaml`; installed before regular deps.
- No regular dependencies and no lifecycle scripts; `optionalDependencies` need exact versions.
- `pnpm-plugin-*` / `@pnpm/plugin-*` packages auto-load their pnpmfile.
- Pair with the `updateConfig` hook to push settings/catalogs into consuming projects.
<!--
Source references:
- https://pnpm.io/config-dependencies
- https://pnpm.io/pnpmfile#hooksupdateconfigconfig-config--promiseconfig
-->
references/features-global-virtual-store.md---
name: pnpm-global-virtual-store
description: Global virtual store for shared node_modules, git-worktree multi-agent setups, isolated global packages, project-aware global bins, shims, and other package managers
---
# Global Virtual Store, Git Worktrees, Global Packages & Shims
## Global virtual store
By default each project has its own `node_modules/.pnpm` virtual store containing hard links to the content-addressable store. With the **global virtual store** enabled, pnpm keeps one shared virtual store at `<store-path>/links/` (find it via `pnpm store path`, or at `virtualStoreDir` when set), and each project's `node_modules` contains only **symlinks** into it.
```yaml title="pnpm-workspace.yaml"
virtualStoreType: global # enableGlobalVirtualStore: true is equivalent
```
```
# Default (per-project .pnpm with hard links)
project-a/node_modules/lodash -> .pnpm/lodash@4.17.21/node_modules/lodash
# Global virtual store (symlink to shared location)
project-a/node_modules/lodash -> <store>/links/@/lodash/4.17.21/<hash>/node_modules/lodash
project-b/node_modules/lodash -> <store>/links/@/lodash/4.17.21/<hash>/node_modules/lodash # same target
```
- **Package identity = hash of the dependency graph.** Two projects with the same `lodash@4.17.21` and the same transitive tree point at the exact same directory (NixOS-style). Different peers ⇒ separate entries.
- No packages are linked into a per-project `.pnpm`; it holds only the project's current lockfile and hidden hoisted deps.
- **Near-zero per-project cost** and **instant installs** once a version is in the store.
- It is the default for global installs (`pnx` does not use it); for **project** installs it is still **opt-in/experimental**.
### Limitations
- **CI:** auto-disabled (no warm cache to benefit from).
- **Trust:** the store is shared writable state — only for mutually trusting projects/users/jobs; protect the path with filesystem permissions.
- **ESM hoisting:** relies on `NODE_PATH`, which Node ignores for ESM imports. pnpm-spawned processes (`run`, `exec`, lifecycle scripts) also get a `NODE_OPTIONS --import` resolve hook restoring those lookups under ESM. A `node` started outside pnpm doesn't get it, and `extendNodePath: false` disables the whole mechanism — declare missing deps with `packageExtensions` (or `@pnpm/plugin-esm-node-path`) if they must resolve either way.
## Git worktrees for multi-agent development
Git worktrees let you check out many branches simultaneously, each in its own directory, sharing one `.git` object store. Combined with the global virtual store, every worktree gets a fully functional `node_modules` that is almost free on disk — ideal for running multiple AI agents in parallel.
```sh
# Bare repo as the hub, one worktree per branch/agent
git clone --bare https://github.com/your-org/your-monorepo.git your-monorepo
cd your-monorepo
git worktree add ./main main
git worktree add ./feature-auth feat/auth
git worktree add ./fix-api fix/api-error
```
```yaml title="pnpm-workspace.yaml"
packages:
- 'packages/*'
virtualStoreType: global
```
```sh
cd main && pnpm install # first install fills the global store
cd ../feature-auth && pnpm install # subsequent worktrees: nearly instant, just symlinks
```
Each worktree has its own `node_modules` tree (so agents can install different versions on different branches without conflict), but all package contents come from the one shared store. Remove a worktree with `git worktree remove ./feature-auth`.
> Assumes all worktrees/agents share the same trust boundary.
## Global packages (v11 isolated installs)
`pnpm add -g` was redesigned in v11 for isolation. Each globally installed package (or group) gets its own install directory with its own `package.json`, `node_modules/`, and lockfile, so global tools can't break each other via peer/hoisting conflicts. Installs are stored at `{pnpmHomeDir}/global/v11/{hash}/` and share the global virtual store.
```sh
pnpm add -g typescript prettier # space-separated = separate isolated installs each
pnpm add -g eslint,prettier # comma-separated = ONE shared install group
pnpm remove -g eslint # removes only eslint's group
pnpm add -g --allow-build=esbuild esbuild # pre-approve build scripts
pnpm list -g # always works at depth 0
pnpm bin -g # global bin dir = $PNPM_HOME/bin
```
- `pnpm install -g` (no args) is **not** supported — use `pnpm add -g <pkg>`.
- `pnpm add -g <pkg>` **replaces every existing group containing `<pkg>`**: after `pnpm add -g eslint`, the `eslint,prettier` group is gone (prettier and its bins too). Re-add the whole group (`pnpm add -g eslint,prettier`) or use `pnpm update -g`.
- Cannot be installed globally: pnpm itself (`ERR_PNPM_GLOBAL_PNPM_INSTALL`, use `pnpm self-update`), a package whose bin a `pnpm shim` already provides (`ERR_PNPM_GLOBAL_BIN_CONFLICT`), `crate:`/Python deps. `pnpm add -g rust` installs a Rust toolchain, not the npm `rust` package.
- `pnpm outdated -g` and `pnpm approve-builds -g` work. Orphaned install dirs older than 5 min are cleaned at the next global add/update/`self-update`. With `virtualStoreType: project`, each group keeps its own `node_modules/.pnpm`.
- Binaries live in `$PNPM_HOME/bin` (not `$PNPM_HOME` directly). Run `pnpm setup` after upgrading to put it on PATH.
- Register a local package's bins globally with `pnpm add -g .` (replaces `pnpm link --global`).
- `pnpm list -g --depth=<n>` (n>0) only works for a single install group.
## Project-aware global bins (v12)
A globally installed `node`/`deno`/`bun` (and any shimmed tool) runs the version the **current project** pins — no version-manager, no shell hooks. pnpm walks up to the nearest project that provides the command:
- **Runtimes** (`node`/`deno`/`bun`): only a runtime pin counts (`devEngines.runtime`, then `engines.runtime`; for `node` since v12.7.0 also `.node-version`/`.nvmrc`, see below); the version is downloaded into the global virtual store on demand. A dependency cannot supply the `node` you run.
- **Package managers** (`npm`/`yarn`/`bun`, v12.0.0-rc.6): the project's `packageManager`/`devEngines.packageManager` pin counts and pnpm provisions it.
- **Any other package:** the project's `node_modules/.bin/<name>`.
`.node-version`/`.nvmrc` (v12.7.0): the nearest directory declaring a Node.js runtime wins; within one directory `package.json` > `.node-version` > `.nvmrc`. Accepts a version/range (optional `v`), `node`, `stable`, `lts/*`, `lts/<codename>`; comments ignored; nvm-only values (`system`, `default`, custom aliases) are ignored and count as unpinned.
If the project provides nothing (or the lookup is declined), the global version runs — dispatch never makes a command fail.
**Trust:** under the default `auto` policy a signed stable Node.js and package-manager pins resolved from the npm registry (signature-verified, e.g. `yarn@1`, `npm`) switch silently; everything else (Deno, Bun, prereleases, ordinary packages) asks once per project + per binary, remembered machine-locally. CI (non-interactive) always uses the global version. Set the `globalShims` policy to `prompt` or `always` to change this; `PNPM_SHIM_BYPASS=1` bypasses for one command.
Runtimes, Rust toolchain shims (`cargo`, `rustc`, ... following the project's toolchain file) and globally installed package managers participate by default; enable others via `globalShims` (global config only, keyed by package name), then reinstall the package:
```yaml title="~/.config/pnpm/config.yaml"
globalShims:
typescript: true
```
### Shims for tools not installed globally (`pnpm shim`)
```bash
pnpm shim add yarn # links a `yarn` that runs whatever the current project pins
pnpm shim ls
pnpm shim rm yarn
```
A shim has no global install behind it; it exists purely to let the project decide. Never created as a side effect of `pnpm setup`/install (it shadows PATH). Since v12.3.0 every project-aware global command pnpm writes is a native executable on every platform (`.exe` on Windows, not `.cmd`/`.ps1`).
To add `bun` as a runtime (not package manager) use `pnpm add bun@tool:1.3.0`; the explicit runtime spelling is `node@tool:22` (not `@runtime:`).
## Other package managers (v12)
pnpm v12 can install **npm, Yarn (Classic/Berry/6), and Bun** — not just itself. Registry-published ones are verified against npm's signature; Yarn 6 and Bun arrive as checksum-pinned platform archives. A JS package manager runs on a managed LTS runtime, so no separate Node.js is needed.
```bash
pnpm add yarn@4 # records the project's package manager (NOT the npm "yarn" package)
pnpm add -g yarn # installs the current Yarn line
pnpm add -g node@22 # installs that Node.js release, not a wrapper
pnpm add yarn@npm:yarn@1.22.22 # a package specifier still installs that package
```
- Yarn is recorded in `packageManager` (exact, resolved); every other PM in `devEngines.packageManager` (range). Only one of the two fields is kept.
- A globally installed PM defers to a project's pin (adds a `globalShims` entry). `pnpm add <pm> --filter …` is refused — run it in the project.
- A git-hosted dependency is built with the package manager its own repo asks for, so a Yarn repo installs on a pnpm-only machine.
## Key Points
- `virtualStoreType: global` ⇒ `node_modules` is symlinks into one shared, hash-addressed store.
- Best for many checkouts of the same repo (git worktrees, parallel agents); auto-disabled in CI.
- Watch out for ESM packages importing undeclared deps (NODE_PATH limitation; the resolve hook covers pnpm-spawned processes).
- Global installs are isolated per package; comma-list to share a group; bins live in `$PNPM_HOME/bin`.
- v12: global `node`/`deno`/`bun` and shimmed tools follow the project's pin; pnpm can install other package managers (npm/yarn/bun).
<!--
Source references:
- https://pnpm.io/global-virtual-store
- https://pnpm.io/git-worktrees
- https://pnpm.io/global-packages
- https://pnpm.io/package-managers
- https://pnpm.io/cli/shim
- https://pnpm.io/settings/node-modules#virtualstoretype
- https://pnpm.io/settings/other#globalshims
- https://pnpm.io/settings/node-modules#virtualstoredir
-->
references/features-hooks.md---
name: pnpm-hooks
description: Customize resolution, config, packing, and fetching with .pnpmfile.mjs hooks, finders, and custom resolvers/fetchers
---
# pnpm Hooks (.pnpmfile.mjs)
pnpm hooks customize installation. Declare them in `.pnpmfile.mjs` (ESM, preferred) or `.pnpmfile.cjs` (CommonJS), located next to the lockfile (workspace root for a monorepo).
> The modern format uses ESM `export const hooks = { ... }`. The old CommonJS `module.exports = { hooks }` still works in `.pnpmfile.cjs`.
## Setup
```js title=".pnpmfile.mjs"
export const hooks = {
readPackage,
afterAllResolved,
updateConfig,
beforePacking,
}
```
## Hook reference
| Hook | When | Use |
|------|------|-----|
| `readPackage(pkg, ctx)` | after a dependency manifest is parsed | mutate a dependency's `package.json` (affects resolution) |
| `afterAllResolved(lockfile, ctx)` | after resolution | mutate the lockfile before it's written |
| `updateConfig(config)` | before install | mutate pnpm's settings (great with config dependencies) |
| `beforePacking(pkg, dir, context)` | before `pnpm pack`/`publish` tarball | customize the **published** manifest only |
| `preResolution(opts, logger)` | after reading lockfiles, before resolution | mutate `opts.wantedLockfile`/`currentLockfile` in place |
| `importPackage(dir, opts)` | when writing to node_modules | **removed (v12.0.0)** - ignored with a warning; use `packageImportMethod` |
| `filterLog(log)` | per log entry | **deprecated/ignored (v12.0.0)** — use `loglevel` instead |
## readPackage
Called for every package before resolution. Common uses:
```js title=".pnpmfile.mjs"
function readPackage(pkg, context) {
// Add a missing peer dependency
if (pkg.name === 'some-broken-package') {
pkg.peerDependencies = { ...pkg.peerDependencies, react: '*' }
}
// Pin a transitive version
if (pkg.dependencies?.lodash) pkg.dependencies.lodash = '^4.17.21'
// Drop a problematic optional dep
delete pkg.optionalDependencies?.fsevents
// Replace a deprecated dep
if (pkg.dependencies?.['old-pkg']) {
pkg.dependencies['new-pkg'] = pkg.dependencies['old-pkg']
delete pkg.dependencies['old-pkg']
}
return pkg
}
export const hooks = { readPackage }
```
> Mutations are not written to disk; they only affect resolution. pnpm records a checksum of the pnpmfile in the lockfile and re-resolves affected dependencies when it changes. Guard fields with `?.` (e.g. `pkg.version?.startsWith`). Removing `scripts` here does **not** stop a build — use the `allowBuilds` setting instead. To persist a change to a dependency's files, use `pnpm patch`.
## updateConfig
Modify pnpm's own settings programmatically — most powerful when shipped in a config dependency so settings are shared across repos.
```js title=".pnpmfile.mjs"
export const hooks = {
updateConfig(config) {
return Object.assign(config, {
enablePrePostScripts: false,
optimisticRepeatInstall: true,
resolutionMode: 'lowest-direct',
verifyDepsBeforeRun: 'install',
})
}
}
```
```js
// Add a catalog entry from a plugin
export const hooks = {
updateConfig(config) {
config.catalogs.default ??= {}
config.catalogs.default['is-odd'] = '1.0.0'
return config
}
}
```
> Since v12.4.1, `config` is the **resolved** configuration (every setting pnpm will act on, from `.npmrc`, CLI, and defaults; unset keys are absent, not `null`). It also carries `registriesByScope` (scope → registry URL; rewrite to redirect fetches) and `configByUri` (registry URI → credentials). Since v12.3.0 the pnpmfile is loaded by many more commands (`run`, `exec`, `rebuild`, script shortcuts, `link`, `outdated`, `import`, `pack`, `publish`, `stage publish`), so `updateConfig` settings like `extraEnv`/`extraBinPaths` reach spawned processes and hook-provided catalogs resolve at pack time.
#### Registry routing and command-line precedence
Returning `registriesByScope` replaces the routing map: omitted scopes use its `default` (or the previous main registry); the built-in `@jsr` route stays unless overridden. Spread `...config.registriesByScope` to keep existing routes. A returned `registries` key is the `registries` setting (merges per key), not the routing map. Since v12.8, settings given on the command line (`--registry`, `--store-dir`, `--config.<key>`, `--no-optional`, ...) win: the hook sees them but pnpm restores them after it returns. Rewriting `registriesByScope` does not affect routes given on the command line.
## beforePacking
Customize the manifest that ends up in the published tarball without touching your local `package.json`.
```js title=".pnpmfile.mjs"
export const hooks = {
beforePacking(pkg, dir, context) { // dir: project being packed; context.log(msg) for debug log
delete pkg.devDependencies
pkg.main = './dist/index.js'
return pkg
}
}
```
## preResolution
Only in-place changes to `options.wantedLockfile`/`currentLockfile` count; the return value is ignored, and a hook that throws or leaves an invalid lockfile fails the install. With `--frozen-lockfile` the changed wanted lockfile is checked against manifests. `logger.info(msg)`/`logger.warn(msg)` print messages.
```js title=".pnpmfile.mjs"
export const hooks = {
preResolution({ wantedLockfile }, logger) {
delete wantedLockfile.importers?.['.'] // re-resolve root deps
logger.info('Dropped locked root dependencies')
},
}
```
## afterAllResolved
```js title=".pnpmfile.mjs"
export const hooks = {
afterAllResolved(lockfile, context) {
context.log(`Resolved ${Object.keys(lockfile.packages || {}).length} packages`)
return lockfile
}
}
```
## Finders (pnpm list / why)
Custom predicates used via `--find-by`:
```js title=".pnpmfile.mjs"
export const finders = {
react17: (ctx) => ctx.readManifest().peerDependencies?.react === '^17.0.0'
}
```
```bash
pnpm why --find-by=react17
pnpm list --find-by=react17 --find-by=other # repeatable; matches if any finder matches
```
## Custom resolvers & fetchers (advanced)
Register top-level `resolvers`/`fetchers` to support new package schemes (e.g. `my-protocol:pkg`). Each is an object with cheap `canResolve`/`canFetch` guards plus `resolve`/`fetch`. Custom resolvers run before built-ins; custom resolution `type` fields must use the `custom:` prefix.
```js title=".pnpmfile.cjs"
const resolver = {
canResolve: (dep) => dep.alias.startsWith('@company/'),
resolve: async (dep) => ({
id: `${dep.alias}@${dep.bareSpecifier}`,
resolution: { type: 'custom:cdn', cdnUrl: '...' },
}),
}
const fetcher = {
canFetch: (id, res) => res.type === 'custom:cdn',
fetch: (cafs, res, opts, fetchers) =>
fetchers.remoteTarball(cafs, { tarball: res.cdnUrl, integrity: res.integrity }, opts),
}
module.exports = { resolvers: [resolver], fetchers: [fetcher] }
```
Built-in fetchers: `localTarball`, `remoteTarball`, `gitHostedTarball`, `git`, `directory`, `binary` (runtime archives like Node.js). A result from a built-in fetcher must be returned unchanged (changed `filesMap` is rejected; changed `manifest`/`requiresBuild` ignored) - return a `{ delegate }` envelope to install different files.
> `hooks.fetchers` was removed in v11 — use the top-level `fetchers` export instead.
### Delegating to built-in fetchers
Instead of fetching itself, a custom fetcher can return a `{ delegate }` envelope naming a complete, fetchable resolution for pnpm to fetch with its built-in path (single-step; a custom-typed delegate is rejected):
```js
fetch: (cafs, resolution) => ({
delegate: { tarball: resolution.customUrl, integrity: resolution.integrity },
})
```
> The envelope works for every resolution in every pnpm version. In pnpm 12, `cafs` is a handle (with `storeDir` and `tempDir()`) to pass to the built-in `fetchers`.
## Related settings
```yaml title="pnpm-workspace.yaml"
ignorePnpmfile: false # ignore the pnpmfile entirely
pnpmfile: ['.pnpmfile.mjs'] # local pnpmfile location(s); default falls back to .pnpmfile.cjs if no .mjs
globalPnpmfile: ~/.pnpm/global_pnpmfile.mjs
```
## Hooks vs Overrides
| | Hooks (.pnpmfile) | Overrides (pnpm-workspace.yaml) |
|--|-------------------|---------------------------------|
| Logic | JavaScript | declarative |
| Scope | any manifest field, config, lockfile, packing | versions |
| Use when | conditional/complex fixes | simple version pins |
Prefer `overrides`/`packageExtensions` for simple cases; use hooks for conditional logic, config sharing, or packing tweaks.
## Key Points
- Prefer `.pnpmfile.mjs` with `export const hooks`/`finders`/`resolvers`/`fetchers`.
- Hooks: `updateConfig` (mutate settings), `beforePacking` (published manifest), `preResolution`. `importPackage` was removed in v12.
- With `sharedWorkspaceLockfile: false`, each project runs its own pnpmfile hooks, then the root's.
- Pair `updateConfig` with config dependencies to share settings/catalogs across repos.
- `--ignore-scripts` does **not** disable the pnpmfile; use `ignorePnpmfile`.
<!--
Source references:
- https://pnpm.io/pnpmfile
- https://pnpm.io/finders
- https://pnpm.io/config-dependencies
-->
references/features-multi-ecosystem.md---
name: pnpm-multi-ecosystem
description: Install Python (pypi:) and Cargo (crate:) dependencies alongside npm packages in one pnpm install
---
# Multi-Ecosystem: Python & Cargo (v12.4.0+, experimental)
pnpm v12 can resolve and install Python and Rust/Cargo dependencies in the same `pnpm install` as npm packages. Both graphs share pnpm's connection budget, artifact verification, and content-addressable store; each keeps its own lockfile and requirement semantics. Settings and on-disk layout may still change.
## Python
Enable in `pnpm-workspace.yaml`:
```yaml title="pnpm-workspace.yaml"
python:
enabled: true
```
pnpm reads `pyproject.toml`, writes `pylock.toml`, and builds a `.venv` per project (a symlink into `python-envs` in the store; swapped atomically). It never touches a hand-made `.venv`.
```sh
pnpm add pypi:httpx # -> httpx==<latest> in pyproject.toml
pnpm add pypi:httpx@0.28.1 # exact pin
pnpm add pypi:'httpx>=0.28' # PEP 508 spec kept as written
pnpm add -D pypi:pytest
```
- `pnpm outdated`/`pnpm update` also cover Python deps (selectors `pypi:<name>`); requirements keep their operator and precision on update.
- Resolution reads each release's metadata from the index's PEP 658 metadata file, downloading the wheel only once selected. Indexes must support the Simple JSON API (HTML-only unsupported).
- `pnpm run`/`pnpm exec` put `.venv/bin` (`.venv/Scripts` on Windows) on PATH, so scripts call `pytest`/`ruff` without activation.
- `--lockfile-only`, `--frozen-lockfile`, `--offline` apply.
- **Local/workspace deps:** declare in `[tool.uv.sources]` (`{ workspace = true }` or `{ path, editable }`); a project with `[build-system]` is installed editable.
- **Build backends need approval** under `allowBuilds` with Package URL keys: `'pkg:pypi/hatchling': true`.
- **Indexes** (v12.5.0+): declare via `registries` with `ecosystem: pypi` and route package names with `packages` patterns; `python.indexUrl` is no longer supported. Credentials go in `.npmrc`, matched by origin.
- **Multi-env locking:** `supportedArchitectures` + `python.versions: ['3.12','3.13']` locks wheels for each platform×interpreter.
- **Interpreter:** auto-selected to satisfy each `requires-python` (prefers `.python-version`); downloads a python-build-standalone build if none fits (`runtimeOnFail` controls). Set `python.executable` to force one.
- Key settings: `python.enabled`, `python.executable`, `python.extras`, `python.groups` (default `['dev']`), `python.versions`, `python.overrides`, `python.constraints`. Per-project overrides in `[tool.pnpm.python]`. `shared-environment = true` under `[tool.uv.workspace]` shares one `.venv` across uv-workspace members.
## Cargo
Enable in `pnpm-workspace.yaml`:
```yaml title="pnpm-workspace.yaml"
cargo:
enabled: true
```
```sh
pnpm add crate:serde
pnpm add crate:serde@^1.0.200
pnpm add --save-build crate:cc # as a Cargo build dependency
```
`pnpm add crate:`/`rust@` fail with `--recursive`/`--filter`; run them in the project. `pnpm outdated`/`update` also cover crates (`crate:<name>` selectors).
- pnpm reads the workspace `Cargo.toml`, writes a deterministic `Cargo.lock`, verifies each `.crate` against its checksum, unpacks into the store, and links a Cargo [directory source] under `.pnpm/crates/crates-io`. It writes the source replacement to `.pnpm/crates/config.toml` and adds an optional `[[include]]` of it to `.cargo/config.toml` between `# >>> pnpm-managed cargo sources >>>` markers (your content outside is untouched; write your own includes as `[[include]]` tables; Cargo 1.95+ reads it). In a fresh clone without `.pnpm`, Cargo skips the include and fetches normally. Already-unpacked crates are linked without re-verifying; `pnpm install --force` unpacks again.
- `cargo build` then compiles **offline** against vendored sources; pnpm compiles nothing.
- Crates are recorded only in `Cargo.lock`, never `pnpm-lock.yaml`. `--lockfile-only`/`--frozen-lockfile`/`--offline` apply.
- **Registry:** declare a sparse index via `registries` with `ecosystem: cargo` (default crates.io); only one index per workspace. `cargo.indexUrl` is no longer supported. Auth reuses URL-scoped credentials plus `CARGO_REGISTRY_TOKEN`/`$CARGO_HOME/credentials.toml`.
- Git deps and `[patch]`/`[replace]` overrides are honored and vendored for offline builds (git packages in `.pnpm/crates/git`). Such workspaces resolve via the `cargo` of the workspace's Rust toolchain and require the default crates.io index; otherwise commit a `Cargo.lock` generated by Cargo. Only `unstable.bindeps` and `resolver.incompatible-rust-versions` are read from the checkout's `.cargo/config.toml`.
- A dependency with its own `registry = "..."` is rejected (one index per workspace). With `unstable.build-std`, the toolchain needs the `rust-src` component.
- Reuse Cargo build state across runs/worktrees with `pnpm pipeline` and `tasks.<name>.cargoTargetDir`.
## Rust toolchain (v12.11.0+)
`pnpm install` installs the toolchain named by the nearest `rust-toolchain.toml` (or `rust-toolchain`), read like rustup. pnpm verifies it against Rust's signing key, stores it once per machine, and links it into `.pnpm/rust`; `pnpm run`/`exec` and pnpm's own cargo calls use it.
```toml title="rust-toolchain.toml"
[toolchain]
channel = "1.95.0" # version, stable, beta, nightly, nightly-YYYY-MM-DD
components = ["rust-src"]
targets = ["wasm32-unknown-unknown"]
```
- `pnpm add rust@1.95` writes the file; `pnpm add -g rust@stable` installs globally; `pnpm shim add rust` delegates to rustup instead; `pnx --package=rust@<channel> cargo ...` runs one release.
- Moving channels (`stable`) are checked daily; offline uses the newest cached one. Files with `path`, no `channel`, linked toolchains, or exotic profiles are left to rustup.
- `runtimeOnFail: error|warn|ignore` turns it off; `tools.rust.mirror` sets a mirror.
## pnpr acceleration
With `pnprServer` set, the server resolves the Python/Cargo graph so pnpm needn't download wheels/index files to discover requirements; it falls back to local resolution when the server doesn't answer for that ecosystem.
<!--
Source references:
- https://pnpm.io/python
- https://pnpm.io/cargo
- https://pnpm.io/registries
- https://pnpm.io/cli/add
- https://pnpm.io/cli/pnx
-->
references/features-overrides.md---
name: pnpm-overrides
description: Force specific versions of dependencies including transitive dependencies
---
# pnpm Overrides
Overrides let you force specific versions of packages, including transitive dependencies. Useful for fixing security vulnerabilities or compatibility issues.
## Basic Syntax
Define overrides in `pnpm-workspace.yaml`. They can only be set at the **root** of the project.
> The `pnpm.overrides` field in `package.json` is **no longer read** (pnpm no longer reads any settings from `package.json#pnpm`). Move overrides to `pnpm-workspace.yaml`.
```yaml title="pnpm-workspace.yaml"
packages:
- 'packages/*'
overrides:
# Override all versions of a package
lodash: ^4.17.21
# Override specific version range
"foo@^1.0.0": ^1.2.3
# Override nested dependency (only zoo inside qar@1)
"qar@1>zoo": "2"
# Override to different package
"underscore": "npm:lodash@^4.17.21"
# Reference a catalog so the version stays in sync
"react": "catalog:"
```
## Override Patterns
### Override all instances
```yaml
overrides:
lodash: ^4.17.21
```
Forces all lodash installations to use ^4.17.21.
### Override specific parent version
```yaml
overrides:
"foo@^1.0.0": ^1.2.3
```
Only override foo when the requested version matches ^1.0.0.
### Override nested dependency
```yaml
overrides:
"express>cookie": ^0.6.0
"foo@1.x>bar@^2.0.0>qux": ^1.0.0
```
Override cookie only when it's a dependency of express.
### Replace with different package
```yaml
overrides:
# Replace underscore with lodash
"underscore": "npm:lodash@^4.17.21"
# Use local file
"some-pkg": "file:./local-pkg"
# Use git
"some-pkg": "github:user/repo#commit"
```
### Remove a dependency
```yaml
overrides:
"unwanted-pkg": "-"
"foo@1.0.0>bar": "-" # great for skipping unused optionalDependencies
```
The `-` removes the package entirely.
### Override peer dependencies
Overrides also apply to `peerDependencies`:
```yaml title="pnpm-workspace.yaml"
overrides:
"react-dom>react": "18.1.0"
```
Without a selector, an override applies to `dependencies`, `devDependencies`, `optionalDependencies`, and `peerDependencies`. Use the `:peer` suffix to target only `peerDependencies`:
```yaml title="pnpm-workspace.yaml"
overrides:
"react-dom>react:peer": "^18.0.0 || ^19.0.0"
"@babel/*:peer": "*"
```
- Ranges, `workspace:`, and `catalog:` replace the peer range.
- Non-range specifiers (`link:`, `file:`, `npm:`) leave `peerDependencies` unchanged (an untyped `"foo": "file:./foo.tgz"` replaces `foo` in `dependencies` only).
- `-` removes the peer dependency and its `peerDependenciesMeta` entry.
- To turn a peer into a direct dep: add it under `packageExtensions.<pkg>.dependencies` and override `"<pkg>>dep:peer": "-"`.
### Dependency-type selectors and wildcards
Append `:prod` (only `dependencies`), `:optional`, or `:peer` to the target to restrict the group. Both parent and target selectors support `*` on package names:
```yaml title="pnpm-workspace.yaml"
overrides:
"foo:prod": "^2.0.0"
"@esbuild/*:optional": "-" # also removes duplicate declaration from dependencies
```
### Convergence overrides
An empty range selector `"pkg@"` rewrites an edge only when the pinned version satisfies the range that edge declares, so compatible consumers converge without forcing an incompatible one:
```yaml title="pnpm-workspace.yaml"
overrides:
"form-data@": 4.0.6 # ^4.0.5 edges -> 4.0.6; ^3.0.0 edges untouched
```
Value must be an exact version (or a `catalog:` entry resolving to one), else `ERR_PNPM_INVALID_CONVERGENCE_OVERRIDE`. Only plain semver edges participate. Cannot combine with a parent selector (`"parent>pkg@"` rejected). A regular override wins for the same edge. pnpm warns when the override is stale.
## Common Use Cases
### Security Fix
Force patched version of vulnerable package:
```yaml
overrides:
# Fix CVE in transitive dependency
"minimist": "^1.2.6"
"json5": "^2.2.3"
```
### Deduplicate Dependencies
Force single version when multiple are installed:
```yaml
overrides:
"react": "^18.2.0"
"react-dom": "^18.2.0"
```
### Fix Peer Dependency Issues
```yaml
overrides:
"@types/react": "^18.2.0"
```
### Replace Deprecated Package
```yaml
overrides:
"request": "npm:@cypress/request@^3.0.0"
```
## Hooks Alternative
For more complex scenarios, use `.pnpmfile.mjs`:
```js title=".pnpmfile.mjs"
function readPackage(pkg, context) {
// Override dependency version
if (pkg.dependencies?.lodash) {
pkg.dependencies.lodash = '^4.17.21'
}
// Add missing peer dependency
if (pkg.name === 'some-package') {
pkg.peerDependencies = {
...pkg.peerDependencies,
react: '*'
}
}
return pkg
}
export const hooks = {
readPackage
}
```
Or extend a manifest declaratively with `packageExtensions` (no JS needed):
```yaml title="pnpm-workspace.yaml"
packageExtensions:
react-redux:
peerDependencies:
react-dom: '*'
```
## Overrides vs Catalogs
| Feature | Overrides | Catalogs |
|---------|-----------|----------|
| Affects | All dependencies (including transitive) | Direct dependencies only |
| Usage | Automatic | Explicit `catalog:` reference |
| Purpose | Force versions, fix issues | Version management |
| Granularity | Can target specific parents | Package-wide only |
## Debugging
Check which version is resolved:
```bash
# See resolved versions
pnpm why lodash
# List all versions
pnpm list lodash --depth=Infinity
```
<!--
Source references:
- https://pnpm.io/settings#overrides
- https://pnpm.io/settings#packageextensions
- https://pnpm.io/pnpmfile
-->
references/features-patches.md---
name: pnpm-patches
description: Patch third-party packages directly with customized fixes
---
# pnpm Patches
pnpm's patching feature lets you modify third-party packages directly. Useful for applying fixes before upstream releases or customizing package behavior.
## Creating a Patch
### Step 1: Initialize Patch
```bash
pnpm patch <pkg>@<version>
# Example
pnpm patch express@4.18.2
```
This creates a temporary directory with the package source and outputs the path:
```
You can now edit the following folder: /tmp/abc123...
```
### Step 2: Edit Files
Navigate to the temporary directory and make your changes:
```bash
cd /tmp/abc123...
# Edit files as needed
```
### Step 3: Commit Patch
```bash
pnpm patch-commit <path-from-step-1>
# Example
pnpm patch-commit /tmp/abc123...
```
`pnpm patch` extracts to `node_modules/.pnpm_patches/<pkg>` (or `--edit-dir`/`-d <dir>`; fails if non-empty: `ERR_PNPM_PATCH_EDIT_DIR_EXISTS`/`ERR_PNPM_EDIT_DIR_NOT_EMPTY`). Run `pnpm install` first (`ERR_PNPM_PATCH_NO_LOCKFILE` otherwise). If several versions are installed, pnpm asks which to patch and whether to apply to all (then the key is the bare package name). `patch-commit` accepts a path, or a package name/specifier (`ERR_PNPM_AMBIGUOUS_PATCH_TARGET` if several edit dirs match), and runs an install afterwards to apply the patch. To change a package's dependencies, use `overrides` or a hook, not a patched `package.json`.
This creates a `.patch` file in `patches/` and records it in `pnpm-workspace.yaml`:
```
patches/
└── express@4.18.2.patch
```
```yaml title="pnpm-workspace.yaml"
patchedDependencies:
express@4.18.2: patches/express@4.18.2.patch
```
> `patchedDependencies` (like all pnpm settings) now lives in `pnpm-workspace.yaml`, not the `package.json#pnpm` field.
## Patch File Format
Patches use standard unified diff format:
```diff
diff --git a/lib/router/index.js b/lib/router/index.js
index abc123..def456 100644
--- a/lib/router/index.js
+++ b/lib/router/index.js
@@ -100,6 +100,7 @@ function createRouter() {
// Original code
- const timeout = 30000;
+ const timeout = 60000; // Extended timeout
return router;
}
```
## Managing Patches
### List Patched Packages
```bash
pnpm list --depth=0
# Shows (patched) marker for patched packages
```
### Update a Patch
```bash
# Edit existing patch
pnpm patch express@4.18.2
# After editing
pnpm patch-commit <path>
```
### Remove a Patch
```bash
pnpm patch-remove <pkg>@<version>
# Example
pnpm patch-remove express@4.18.2
```
`patch-remove` with no args lists recorded patches and prompts. It deletes the patch files, removes the keys, and deletes an empty patches dir. Errors: `ERR_PNPM_NO_PATCHES_TO_REMOVE`, `ERR_PNPM_PATCH_NOT_FOUND`.
Or manually:
1. Delete the patch file from `patches/`
2. Remove the entry from `patchedDependencies` in `pnpm-workspace.yaml`
3. Run `pnpm install`
## Patch Configuration
### Multiple Packages / Workspaces
Patches are shared across the whole workspace from the root `pnpm-workspace.yaml`:
```yaml title="pnpm-workspace.yaml"
patchedDependencies:
express@4.18.2: patches/express@4.18.2.patch
lodash@4.17.21: patches/lodash@4.17.21.patch
'@types/node@20.10.0': patches/@types__node@20.10.0.patch
```
A version-less key (`express:`) patches every installed version. All workspace packages using a matching version get the patch.
### Patches from a config dependency
Patch files can live inside a shared config dependency and be referenced by path:
```yaml title="pnpm-workspace.yaml"
configDependencies:
my-patches: '1.0.0'
patchedDependencies:
react: node_modules/.pnpm-config/my-patches/react.patch
```
### allowUnusedPatches
```yaml title="pnpm-workspace.yaml"
allowUnusedPatches: true # don't fail when a listed patch wasn't applied
```
> `ignorePatchFailures` was **removed** in v11. A patch that fails to apply now always throws. When several patches are grouped, all errors are reported together at the end.
## Best Practices
1. **Version specificity**: Patches are tied to exact versions. Update patches when upgrading dependencies.
2. **Document patches**: Add comments explaining why the patch exists:
```bash
# In patches/README.md
## express@4.18.2.patch
Fixes timeout issue. PR pending: https://github.com/expressjs/express/pull/1234
```
3. **Minimize patches**: Keep patches small and focused. Large patches are hard to maintain.
4. **Track upstream**: Note upstream issues/PRs so you can remove patches when fixed.
5. **Test patches**: Ensure patched code works correctly in your use case.
## Troubleshooting
### Patch fails to apply
```
ERR_PNPM_PATCH_FAILED Cannot apply patch
```
The package version changed. Recreate the patch:
```bash
pnpm patch-remove express@4.18.2
pnpm patch express@4.18.2
# Reapply changes
pnpm patch-commit <path>
```
### Patch not applied
Ensure:
1. Version in `patchedDependencies` matches installed version exactly
2. Run `pnpm install` after adding patch configuration
<!--
Source references:
- https://pnpm.io/cli/patch
- https://pnpm.io/cli/patch-commit
- https://pnpm.io/config-dependencies
-->
references/features-peer-deps.md---
name: pnpm-peer-dependencies
description: Handling peer dependencies with auto-install and resolution rules
---
# pnpm Peer Dependencies
pnpm has strict peer dependency handling by default. It provides configuration options to control how peer dependencies are resolved and reported.
All peer-dependency settings live in `pnpm-workspace.yaml` (camelCase). The `package.json#pnpm` field is no longer read.
## Auto-Install Peer Dependencies
By default (since v8), pnpm automatically installs missing non-optional peer dependencies:
```yaml title="pnpm-workspace.yaml"
autoInstallPeers: true
```
On conflicting requirements (e.g. one dep needs `react@^16`, another `react@^17`), pnpm installs nothing and prints a warning - resolve it manually, or opt in to joining the ranges:
```yaml title="pnpm-workspace.yaml"
autoInstallPeersFromHighestMatch: true # default false; joins ranges with ||, installs the highest match (react@17)
```
This also lets pnpm resolve missing required/optional peers from compatible versions elsewhere in the workspace (even with `autoInstallPeers: false`). A missing optional peer is only resolved if the workspace already has a satisfying version (e.g. from another project's devDependencies). To disable workspace-wide peer reuse, set both `autoInstallPeers` and `dedupePeerDependents` to `false`. Resolved peers are not added to your declared deps: if scripts run a peer's binary, declare it directly.
## Strict Peer Dependencies
```yaml title="pnpm-workspace.yaml"
strictPeerDependencies: true # default false
```
When strict, commands fail on a missing or invalid peer dependency in the tree.
## Resolve from workspace root
```yaml title="pnpm-workspace.yaml"
resolvePeersFromWorkspaceRoot: true # default; install shared peers once at the root
```
## Deduplicate peers
```yaml title="pnpm-workspace.yaml"
dedupePeerDependents: true # default; share package instances across projects when peers match
dedupePeers: false # version-only peer suffixes (name@version), fewer instances
addMissingPeerTypes: false # v12.12+: every peer `foo` also gets optional peer `@types/foo` (@babel/core -> @types/babel__core)
```
`addMissingPeerTypes` helps with `enableGlobalVirtualStore`, where types installed in your project are otherwise not found next to the package. It changes the lockfile.
## Peer Dependency Rules
`peerDependencyRules` is deprecated (still works): it only silences warnings after resolution. To change declared peer ranges so resolution changes, use `overrides` with `:peer`:
```yaml title="pnpm-workspace.yaml"
overrides:
'react:peer': '-' # remove the peer declaration (and its peerDependenciesMeta)
'button@2>react:peer': '16 || 17' # widen the declared peer range
'@babel/*:peer': '*'
```
```yaml title="pnpm-workspace.yaml"
peerDependencyRules:
ignoreMissing:
- '@babel/*'
- eslint
allowedVersions:
react: '17 || 18'
allowAny:
- '@types/*'
```
### ignoreMissing
Suppress warnings for missing peer dependencies. Patterns: exact name (`react`), scope (`@babel/*`), or `*` (not recommended). Does not support `parent>peer` selectors.
```yaml title="pnpm-workspace.yaml"
peerDependencyRules:
ignoreMissing:
- '@babel/*'
- eslint
- webpack
```
### allowedVersions
Allow specific versions that would otherwise warn. Target a specific parent with `parent>peer`.
```yaml title="pnpm-workspace.yaml"
peerDependencyRules:
allowedVersions:
react: '17'
'button@2>react': '17' # only when react is a peer of button@2
```
### allowAny
Resolve matching peers from any version, ignoring the declared range. Name or glob only (no `parent>peer`).
```yaml title="pnpm-workspace.yaml"
peerDependencyRules:
allowAny:
- '@types/*'
- eslint
```
## Adding Peer Dependencies via packageExtensions
Declaratively add a missing peer dependency without JS:
```yaml title="pnpm-workspace.yaml"
packageExtensions:
problematic-package:
peerDependencies:
react: '*'
```
For conditional logic, use a `readPackage` hook in `.pnpmfile.mjs` instead.
## Peer Dependencies in Workspaces
Workspace packages can satisfy peer dependencies:
```json
// packages/app/package.json
{
"dependencies": {
"react": "^18.2.0",
"@myorg/components": "workspace:^"
}
}
// packages/components/package.json
{
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0"
}
}
```
The workspace `app` provides `react` which satisfies `components`' peer dependency.
## Common Scenarios
### Monorepo with Shared React
```yaml
# pnpm-workspace.yaml
catalog:
react: ^18.2.0
react-dom: ^18.2.0
```
```json
// packages/ui/package.json
{
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
// apps/web/package.json
{
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:",
"@myorg/ui": "workspace:^"
}
}
```
### Suppress ESLint Plugin Warnings
```yaml title="pnpm-workspace.yaml"
peerDependencyRules:
ignoreMissing:
- eslint
- '@typescript-eslint/parser'
```
### Allow Multiple Major Versions
```yaml title="pnpm-workspace.yaml"
peerDependencyRules:
allowedVersions:
webpack: '4 || 5'
postcss: '7 || 8'
```
## Debugging Peer Dependencies
```bash
# Report unmet/missing peers from the lockfile (exit code 1 on issues; bare `pnpm peers` = check)
pnpm peers check # also: --json, -r (all workspace projects), -g, --lockfile-only
# See why a package is installed
pnpm why <package>
# Check dependency tree
pnpm list --depth=Infinity
```
## Best Practices
1. **Keep `autoInstallPeers` on** for convenience (default in v8+)
2. **Use `peerDependencyRules`** instead of blanket-ignoring warnings
3. **Document suppressed warnings** explaining why they're safe
4. **Keep peer ranges wide** in libraries (e.g. `"react": "^17 || ^18"`)
5. **Run `pnpm peers check`** in CI to catch peer regressions
<!--
Source references:
- https://pnpm.io/settings#peerdependencyrules
- https://pnpm.io/settings#autoinstallpeers
- https://pnpm.io/cli/peers
-->
references/features-supply-chain-security.md---
name: pnpm-supply-chain-security
description: Build-script approval (allowBuilds), minimum release age, trust policy, and exotic-subdep blocking for safer installs
---
# pnpm Supply-Chain Security
pnpm blocks several attack vectors by default. Agents installing dependencies must understand these, since installs can fail or prompt on them.
## Build-script approval (allowBuilds)
By default pnpm does **not** run dependency lifecycle scripts (`preinstall`/`install`/`postinstall`). Packages must be explicitly approved. Approval lives in one `allowBuilds` map in `pnpm-workspace.yaml`.
```yaml title="pnpm-workspace.yaml"
allowBuilds:
esbuild: true
core-js: false
# version selectors are supported
nx@21.6.4 || 21.6.5: true
```
- Packages **not listed** are unreviewed and blocked by default.
- `strictDepBuilds: true` (default) ⇒ unreviewed builds make install exit non-zero (`ERR_PNPM_IGNORED_BUILDS`). Set `false` to warn instead.
- During install, unreviewed packages with build scripts are auto-added to `pnpm-workspace.yaml` with the placeholder `set this to true or false` so you can set `true`/`false`.
- Git/tarball deps are never approved by bare name: use the exact resolved path (with commit) or the repo URL (`foo@git+https://github.com/org/foo.git: true`, also covers tarball downloads from GitHub/GitLab/Bitbucket).
> `allowBuilds` replaces the removed `onlyBuiltDependencies`, `neverBuiltDependencies`, `ignoredBuiltDependencies`, `onlyBuiltDependenciesFile`, and `ignoreDepScripts`.
### Approving builds
```bash
pnpm approve-builds # interactive prompt
pnpm approve-builds --all # approve all pending (cannot combine with names)
pnpm approve-builds esbuild fsevents !core-js # ! = deny
pnpm add --allow-build=esbuild my-bundler # approve while adding
pnpm add -g --allow-build=esbuild esbuild # global (replaces approve-builds -g)
```
Naming one package both approved and denied fails (`ERR_PNPM_APPROVE_BUILDS_CONTRADICTING_ARGS`). Approved pending packages are rebuilt immediately. Writing `allowBuilds` also removes the ignored legacy settings from the file.
### Permissions and agent skills (v12.11.0)
`pnpm approve` (= `pnpm permissions approve`; `pnpm permissions ls` lists) reviews what each dependency may do: `build` (run build scripts) and `skills` (pnpm links the agent skills the package ships, from its `skills/<name>/SKILL.md`, into the project's agent skill dirs). Decisions go in a `permissions` map; a package's `permissions` entry outranks its `allowBuilds` entry. Build decisions are written to `permissions` only if the file already has it, otherwise to `allowBuilds`.
```yaml title="pnpm-workspace.yaml"
permissions:
drizzle-kit:
build: true
skills: [guide, migrate] # true = all, false = none, list = exactly these
some-pkg:
skills: false
skills:
dirs: [.claude/skills] # default: existing .<agent>/skills dirs + the running agent's; [] disables skills
```
- Install never fails on pending skills; it prints "Agent skills awaiting approval" for direct deps (only dep groups the install includes). Run `pnpm install` before `pnpm approve`.
- `pnpm approve drizzle-kit !esbuild` (`!` denies), `--all` approves everything.
- Links are named after the skill dir (fallback `pnpm-<package>-<skill>`, `/` in scope becomes `+`), listed in a `# pnpm` block of a `.gitignore` in each skills dir. Not linked by global installs, `dlx`, or `deploy`.
### Escape hatch (dangerous)
```yaml title="pnpm-workspace.yaml"
dangerouslyAllowAllBuilds: true # runs ALL build scripts now and in the future — avoid
```
Explicitly denied packages (`false` in `allowBuilds`, `build: false` in `permissions`) stay blocked.
## Minimum release age
Delay installing freshly published versions so malicious releases (usually pulled within an hour) are avoided. Applies to **all** deps, including transitive.
```yaml title="pnpm-workspace.yaml"
minimumReleaseAge: 1440 # minutes; default 1440 (1 day)
minimumReleaseAgeExclude: # always install newest of these immediately
- webpack
- '@myorg/*'
- nx@21.6.5 # exempt a specific version
```
- `minimumReleaseAgeStrict` — when no in-range version satisfies the age, fail (default when you set `minimumReleaseAge` yourself) vs. fall back.
- `minimumReleaseAgeIgnoreMissingTime` — skip the check for registries that omit the `time` field (default `true`); also governs `trustPolicy` (undatable packages are skipped with a warning instead of `ERR_PNPM_MISSING_TIME`).
- Does not apply to config dependencies pinned with `version+integrity`. `minimumReleaseAgeExcludePrune: true` (and `trustPolicyExcludePrune`, v12.4.0) drop stale exclude entries on install.
## Trust policy
Fail if a package's trust level **decreased** vs earlier releases (e.g. was published by a trusted publisher, now only has provenance or nothing).
```yaml title="pnpm-workspace.yaml"
trustPolicy: no-downgrade # off (default) | no-downgrade
trustPolicyExclude:
- 'chokidar@4.0.3'
trustPolicyIgnoreAfter: 525600 # ignore the check for pkgs published > N minutes ago
```
The check also covers the pnpm version pinned in `packageManager`/`devEngines.packageManager`.
## Block exotic transitive sources
```yaml title="pnpm-workspace.yaml"
blockExoticSubdeps: true # default (on)
```
When `true`, only **direct** dependencies may use exotic sources (git repos, direct tarball URLs); all transitive deps must come from a trusted source (registry, local path, workspace link, or trusted GitHub repos).
## Lockfile integrity
Since v11, a downloaded tarball whose hash doesn't match `pnpm-lock.yaml` is a hard error (`ERR_PNPM_TARBALL_INTEGRITY`) — protecting committed lockfiles from a compromised registry/proxy. `--force` and `pnpm update` do **not** bypass it.
```bash
pnpm install --update-checksums # narrow opt-in after verifying the new bytes
```
## Pin packages to their registry
If you install from more than one registry, use `namedRegistries` aliases for packages that must come from a specific one. pnpm records these under registry-qualified lockfile keys (`<name>@<registryName>:<version>`), so a package can't be silently substituted by another registry publishing the same name/version.
## Lockfile scanners: two-document lockfile
`pnpm-lock.yaml` may be a two-document file. A vulnerability scanner or SBOM generator that reads only the first document reports "no dependencies" (and no vulnerabilities) without failing — verify your tooling handles both documents.
## Trusted store/cache
The content-addressable store, global virtual store, and metadata cache are part of pnpm's trust domain. Share them only between mutually trusting users/jobs and protect with filesystem permissions. `verifyStoreIntegrity` (default `true`) detects accidental corruption but does not make a writable-by-untrusted store safe.
## Key Points
- Dependency build scripts are blocked until approved via `allowBuilds` / `pnpm approve-builds`; unreviewed builds fail by default (`strictDepBuilds`).
- `minimumReleaseAge` (default 1 day in v11) delays new releases; `trustPolicy: no-downgrade` blocks trust regressions; `blockExoticSubdeps` limits transitive git/tarball sources.
- Tarball integrity mismatches are fatal; use `--update-checksums` only after verification.
- Treat the store/cache as trusted shared state.
<!--
Source references:
- https://pnpm.io/settings/build#allowbuilds
- https://pnpm.io/cli/approve-builds
- https://pnpm.io/settings/dependency-resolution#minimumreleaseage
- https://pnpm.io/settings/dependency-resolution#trustpolicy
- https://pnpm.io/settings/dependency-resolution#blockexoticsubdeps
- https://pnpm.io/supply-chain-security
- https://pnpm.io/agent-skills
- https://pnpm.io/cli/permissions
- https://pnpm.io/settings/build#permissions
-->
references/features-task-orchestration.md---
name: pnpm-task-orchestration
description: Declare cross-project task graphs (tasks/dependsOn), concurrency groups, priorities, and run cached CI pipelines
---
# Workspace Task Orchestration
`pnpm -r run <script>` schedules a graph of workspace tasks. A task is `<project>#<script>`; it becomes ready once every task it depends on succeeds, and ready tasks run under `--workspace-concurrency`. Independent tasks run in unpredictable order.
## Declaring task dependencies
Configure under `tasks` in `pnpm-workspace.yaml`:
```yaml title="pnpm-workspace.yaml"
tasks:
build:
dependsOn:
- ^build # build in each workspace dependency
test:
dependsOn:
- build # build in the same project
```
- `build` → the `build` task in the same project.
- `^build` → the `build` task in each selected workspace dependency.
- A task with **no** `tasks` entry defaults to depending on the same task in its workspace deps (an unconfigured `build` behaves as `dependsOn: ['^build']`), preserving deps-before-dependents order.
- Once a task has an entry, an omitted `dependsOn` means `dependsOn: []`. If you set another field (e.g. `concurrency`) and still want topological order, declare `dependsOn: ['^build']` explicitly.
- Task deps stay within the `--filter`/`includeWorkspaceRoot` selection. A project missing the named script is a pass-through (reported skipped, doesn't break the chain).
## Per-task concurrency
```yaml title="pnpm-workspace.yaml"
tasks:
build:
concurrency: 2 # max 2 instances of build across projects
dependsOn: ['^build']
```
Separate from `--workspace-concurrency`; a task waiting for its slot doesn't occupy a workspace slot.
## Concurrency groups (v12.5.0)
Machine-wide limits shared across pnpm processes that use the same `stateDir` (including `pnpm pipeline`):
```yaml title="pnpm-workspace.yaml"
tasks:
test:rust:
concurrencyGroup: cargo
dependsOn: []
concurrencyGroups:
cargo: 2 # at most 2 cargo tasks at once, across processes
```
- A nested `pnpm run` in the same group reuses its parent's slot. Slots release on process exit/crash.
- A missing/zero group limit doesn't restrict. Changing `stateDir` creates a separate slot pool.
### Task priority (v12.6.0)
`priority` (integer, default `0`) orders waiting tasks for available slots — higher runs first, ties broken by arrival:
```yaml title="pnpm-workspace.yaml"
tasks:
build:critical: { concurrencyGroup: build, priority: 10 }
build:cleanup: { concurrencyGroup: build, priority: -1 }
```
### Inspecting groups (v12.6.0)
```sh
pnpm tasks status [groups...] # running + waiting tasks per group
pnpm pm tasks status # force built-in if a "tasks" script shadows it
```
A subcommand is required. A waiting task shows its priority only when non-zero.
```sh
```
## Inspecting the graph
```sh
pnpm -r run --dry-run build # stable topological ordering, no scripts run
pnpm -r run --dry-run --json test # nodes + edges as JSON
```
## Recursive run options
- `--resume-from <pkg>` — resume at a package's task, skipping tasks a prior run of the same invocation recorded as passed.
- `--reverse` — reverse every edge (dependents run first).
- `--no-bail` — keep running independent ready tasks after a failure; tasks depending on the failed one are skipped (default `--bail` stops dispatching and kills running tasks: SIGTERM, then SIGKILL after 2s on Unix). Accepted only by run/exec/test/start/stop/rebuild/install-test.
- `--workspace-concurrency` defaults to min(4, available CPU cores), respecting container CPU quotas; `<= 0` means cores minus abs value (min 1).
- `--report-summary` statuses: `passed|queued|running|skipped|failure` (failures carry `prefix`/`message`); non-requested tasks are keyed `<dir>#<task>`.
- Output is inherited when only one script can run at a time; otherwise piped. Use `--stream` for immediate prefixed output or `--aggregate-output`.
## Cycles
A cycle fails before any script with `ERR_PNPM_TASK_CYCLE`. Set `ignoreWorkspaceCycles: true` only for deliberate cycles (pnpm warns and drops ordering among members).
## Commands that ignore `tasks`
`--no-sort` and `--parallel` (implies `--no-sort`) ignore `tasks` declarations. Recursive `exec` has no script name so it doesn't join `dependsOn`, but still uses dependency-aware scheduling and `--resume-from`.
## pnpm pipeline (v12.4.0, experimental)
Runs a named set of tasks the way a CI job would: frozen install, then the affected projects' task graph, with cached results restored.
```yaml title="pnpm-workspace.yaml"
tasks:
build: { dependsOn: ['^build'], outputs: ['dist/**'], inputs: ['src/**'], env: ['NODE_ENV'] }
test: { dependsOn: ['build'], outputs: [] }
lint: { outputs: [] }
pipelines:
default: [build, test, lint]
release: [build]
```
```sh
pnpm pipeline # runs the "default" pipeline
pnpm pipeline release
pnpm pipeline --dry-run --json
pnpm pipeline --full # every project, not just affected-since-base
pnpm pipeline --base <ref> # affected diff base (default origin/main, or pipelineBase)
pnpm pipeline --no-cache
```
- **Caching:** a task is cacheable only when it declares `outputs` (`outputs: []` = "produces no files", makes a linter/test cacheable). `inputs` narrows the cache key (`+glob` adds to the default); `env` names hashed vars; `cache: false` opts out. The key also covers script text, dependency task keys, the lockfile, and the runtime. A hit restores files and replays logs.
- A plain recursive `pnpm run` **never** restores from the pipeline cache; `outputs`/`inputs`/`env`/`cache`/`cargoTargetDir` are read by `pnpm pipeline` only.
- **Remote cache:** a `remoteCache` setting shares signed results through a Turborepo-API server (e.g. Vercel Remote Cache) or pnpr. Machines restore only results signed by their `trustedKeys`; CI publishes with `PNPM_REMOTE_CACHE_URL/_TEAM/_TRUSTED_KEYS/_PRIVATE_KEY/_KEY_ID/_BUILDER_ID/_PUBLISH=true`. Published results include output files and terminal logs. A missing, untrusted, or unreachable cache is just a miss.
- Accepts every `pnpm install` option (for its frozen install). Errors: `ERR_PNPM_NO_PIPELINES`, `ERR_PNPM_UNKNOWN_PIPELINE`, `ERR_PNPM_PIPELINE_FAIL`. `--report`/`--report-to <url>` submit run data; `--repo` with `--watch`/`--once`/`--interval` watches a repo.
- **Cargo build state:** `cargoTargetDir: target` keeps a task's Cargo target dir between runs/worktrees via immutable snapshots.
## Other dependency-aware commands
Workspace install, rebuild, pack, publish, stage, and lifecycle work start a project's work as soon as its workspace deps finish — following the package graph (not `tasks`), no longer waiting for unrelated topological groups.
<!--
Source references:
- https://pnpm.io/workspace-task-orchestration
- https://pnpm.io/cli/tasks
- https://pnpm.io/cli/pipeline
-->
references/features-versioning.md---
name: pnpm-release-management
description: Native workspace versioning and releases with pnpm change, pnpm version -r, lanes, epics, and fixed groups
---
# Release Management (v11.13.0+)
pnpm versions and releases a workspace without a separate release tool. Two halves:
1. As you work, `pnpm change` records **change intents** — markdown files in `.changeset/` (changesets format) naming affected packages, bump types, and a changelog summary. Commit them with the change.
2. At release, bare `pnpm version -r` consumes pending intents: bumps versions, propagates to dependents, writes changelogs, and records what it consumed in a committed ledger.
An existing `.changeset/` keeps working; you can still use the Changesets CLI instead.
## Recording a change
```sh
pnpm change # interactive prompt
pnpm change --bump patch --summary "Fix crash" @ex/core # non-interactive (scripts)
pnpm change status # pending intents + release plan
```
Writes e.g. `.changeset/calm-cats-resolve.md`:
```markdown
---
"@example/core": minor
---
Added a `--watch` flag to the build command.
```
`--bump` accepts `none|patch|minor|major` (`none` = explicit "no release"). When two projects share a name, reference one by `./`-prefixed directory (`"./packages/cli": minor`).
## Releasing
```sh
pnpm version -r # consume intents, bump, changelog, ledger
pnpm version -r --dry-run # preview
pnpm version -r --filter … # narrow (selection expands to settle deps/fixed groups)
```
No git commit/tag is created (many packages, many versions) — commit yourself, then `pnpm publish -r`. Working tree must be clean unless `--dry-run`/`--no-git-checks`. Every package bumped through a `workspace:` range to a bumped dependency is bumped too. First release of a package publishes the manifest version verbatim (v11.16.0+); private packages skip that registry check and always use committed `CHANGELOG.md` (`repository` storage). Bumps run `preversion`/`version`/`postversion` scripts (skipped by `--dry-run`/`ignoreScripts`); `gitTagVersion: false` and `tagVersionPrefix` can be set in `pnpm-workspace.yaml`. `pnpm version -r` outside a workspace fails with `ERR_PNPM_WORKSPACE_ONLY`.
## Configuration (`versioning` in pnpm-workspace.yaml)
```yaml title="pnpm-workspace.yaml"
versioning:
fixed:
- ['@example/cli', '@example/napi'] # always release at one shared version
ignore:
- '@example/internal' # excluded from versioning + propagation
maxBump: minor # cap the bump this checkout may apply
lanes:
'@example/cli': alpha # parallel release track
epics:
- lead: '@example/app'
packages: ['./packages/**', '!./packages/private-*']
changelog:
storage: repository # commit CHANGELOG.md (default: registry)
```
- **fixed groups** release together at the highest current version bumped by the largest needed bump; must move lanes together and sit wholly in/out of an epic.
- **changelog.storage:** `registry` (default) composes each section at publish time into the tarball, no committed `CHANGELOG.md`; `repository` commits one per package.
## Lanes
A lane is a parallel release track. A package on lane `alpha` releases `X.Y.Z-alpha.N` prereleases from the same runs that ship stable versions of everything on `main`.
```sh
pnpm lane alpha --filter @example/cli # move onto alpha (--filter required)
pnpm lane main --filter @example/cli # graduate to stable on next version -r
pnpm lane # show membership
```
`N` counts from 0 and restarts when the stable target changes. A package already on another lane can't move directly (`ERR_PNPM_VERSIONING_ALREADY_ON_LANE`): go through `main` first. An empty `--filter` match fails with `ERR_PNPM_VERSIONING_NO_PACKAGES`. Lane names: alphanumerics/hyphens, not purely numeric; `main` is reserved.
## Epics
An epic ties member packages to a lead, constraining each member's major to a band: while the lead is on major `M`, members live in `M*100`…`M*100+99` (lead on `11.x` ⇒ members in `1100`–`1199`). A bump past the ceiling is rejected until the lead advances; when the lead hits a new major, members re-base to the band floor in the same plan.
## Validation & the ledger
```sh
pnpm change check # (v12.4.0) validate pending intents (plans the release; fails on e.g. an intent naming a package outside the workspace) + committed versions against epic bands and fixed groups — run on every PR
```
`pnpm version -r` records consumed intents in `.changeset/ledger.yaml` (committed, append-only). Consumption is tracked per project — an intent file is deleted only once every project it names has released, making cherry-picks/merge-backs between release branches safe.
## Release notes on pnpr
For registries declared `serverType: pnpr`, `registry` storage sends each section as the version's release notes instead of packing it into the tarball. Intents naming one package go to its notes; intents naming several go once into the release notes (named `v<version>` after the epic lead or the one fixed group). `pnpm publish -r --batch` sends the whole release in one request; `pnpm version -r --release-notes <file>` adds a hand-written summary above the composed notes; `pnpm publish --publication-name` names the release.
<!--
Source references:
- https://pnpm.io/versioning
- https://pnpm.io/cli/change
- https://pnpm.io/cli/lane
- https://pnpm.io/cli/version
- https://pnpm.io/settings/versioning
-->
SKILL.md--- name: pnpm description: Node.js package manager with strict dependency resolution. Use when running pnpm specific commands, configuring workspaces via pnpm-workspace.yaml, or managing dependencies with catalogs, patches, overrides, config dependencies, or the global virtual store. metadata: author: Anthony Fu version: "2026.11.5" source: Generated from https://github.com/pnpm/pnpm, scripts located at https://github.com/antfu/skills --- pnpm is a fast, disk space efficient package manager. It uses a content-addressable store to deduplicate packages across all projects on a machine, and enforces strict dependency resolution by default, preventing phantom dependencies. **pnpm v12 is a Rust rewrite** of v11: stable, and keeps v11's commands, flags, settings, and lockfile format — so most guidance here applies to both. A handful of v12 behaviors differ (git deps resolve via HTTPS, project-aware global bins, other package managers, `packageImportMethod: auto` hardlinks first on Linux) — see best-practices-migration. **Configuration model (important):** pnpm settings live in `pnpm-workspace.yaml` (and the global `config.yaml`) using **camelCase** keys. `.npmrc` is used **only** for authentication/registry credentials, and the `pnpm` field of `package.json` is no longer read. When working in a pnpm project, check `pnpm-workspace.yaml` for settings/workspace structure and `.npmrc` only for auth. Always use `--frozen-lockfile` (or `pnpm ci`) in CI. > The skill is based on pnpm 12.18, generated at 2026-11-05. It covers v11+v12 behavior (config split, config includes, isolated global packages, `allowBuilds`, `pmOnFail`, global virtual store, native release management, workspace task orchestration, and experimental Python/Cargo support) where current docs describe them. ## Core | Topic | Description | Reference | |-------|-------------|-----------| | CLI Commands | install/add/remove/update, run, dlx/pnx, workspace, runtime, publishing (version, view, sbom, stage) | [core-cli](references/core-cli.md) | | Configuration | pnpm-workspace.yaml settings (camelCase), global config.yaml, config includes, packageConfigs, auth (.npmrc, auth.ini) | [core-config](references/core-config.md) | | Workspaces | Monorepo support: filtering, workspace protocol, shared lockfile, packageConfigs | [core-workspaces](references/core-workspaces.md) | | Store | Content-addressable store, virtual store, node linker modes (incl. loaded/store loader), frozen/read-only store | [core-store](references/core-store.md) | ## Features | Topic | Description | Reference | |-------|-------------|-----------| | Catalogs | Centralized dependency versions; catalogMode, catalog: in overrides | [features-catalogs](references/features-catalogs.md) | | Overrides | Force versions (incl. transitive & peer deps); packageExtensions | [features-overrides](references/features-overrides.md) | | Patches | Modify third-party packages; patchedDependencies in pnpm-workspace.yaml | [features-patches](references/features-patches.md) | | Aliases | Install under custom names (npm:) and registry aliases (namedRegistries) | [features-aliases](references/features-aliases.md) | | Hooks | .pnpmfile.mjs hooks (readPackage, updateConfig, beforePacking), finders, resolvers/fetchers | [features-hooks](references/features-hooks.md) | | Peer Dependencies | Auto-install, strict mode, rules, dedupePeers, peers check | [features-peer-deps](references/features-peer-deps.md) | | Config Dependencies | Share hooks/settings/catalogs/patches across repos via configDependencies | [features-config-dependencies](references/features-config-dependencies.md) | | Global Virtual Store & Shims | Shared node_modules, git-worktree multi-agent setups, isolated global packages, project-aware bins, other package managers | [features-global-virtual-store](references/features-global-virtual-store.md) | | Supply-Chain Security | Build approval (allowBuilds), permissions and agent skills, minimumReleaseAge, trustPolicy, lockfile integrity | [features-supply-chain-security](references/features-supply-chain-security.md) | | Task Orchestration | Cross-project task graphs (tasks/dependsOn), concurrency groups, priority, pnpm pipeline | [features-task-orchestration](references/features-task-orchestration.md) | | Release Management | Native versioning: pnpm change/version -r/lane, lanes, epics, fixed groups | [features-versioning](references/features-versioning.md) | | Multi-Ecosystem | Python (pypi:) and Cargo (crate:) dependencies alongside npm (experimental) | [features-multi-ecosystem](references/features-multi-ecosystem.md) | ## Best Practices | Topic | Description | Reference | |-------|-------------|-----------| | CI/CD Setup | GitHub Actions, GitLab, Docker, pnpm ci, store caching, frozen lockfiles | [best-practices-ci](references/best-practices-ci.md) | | Migration | npm/Yarn → pnpm, phantom deps, and pnpm v10 → v11 → v12 upgrade notes | [best-practices-migration](references/best-practices-migration.md) | | Performance | Install optimizations, allowBuilds, global virtual store, workspace parallelization | [best-practices-performance](references/best-practices-performance.md) |