Managing Monorepo Packages with pnpm workspaces and catalogs
node.jspnpm is a fast, disk-efficient, and feature-rich Node.js package manager. By default, postinstall scripts are disabled, and with minimumReleaseAge, you can prevent installation of versions that were just published. These make pnpm robust against supply chain attacks like the recent axios incident.
$ corepack enable pnpm
$ pnpm --version
10.33.0
pnpm also provides monorepo-friendly features: define packages in pnpm-workspace.yaml, run commands with –filter, and unify dependency versions using catalog.
$ vi pnpm-workspace.yaml
packages:
- "packages/*"
- "apps/*"
catalog:
react: "^19.2.4"
minimumReleaseAge: 1440
$ pnpm add --filter lib-a react@catalog:
Progress: resolved 2, reused 0, downloaded 0, added 0, done
Done in 312ms using pnpm v10.33.0
$ cat packages/lib-a/package.json
{
...
"dependencies": {
"react": "catalog:"
}
}
$ tree .
.
├── apps
│ └── app-x
│ └── package.json
├── node_modules
├── package.json
├── packages
│ └── lib-a
│ ├── node_modules
│ │ └── react -> ../../../node_modules/.pnpm/[email protected]/node_modules/react
│ └── package.json
├── pnpm-lock.yaml
└── pnpm-workspace.yaml
8 directories, 5 files
While npm -w installs packages as flat as possible in the root node_modules, pnpm creates links in each package.
This prevents access to packages not declared in dependencies without sacrificing speed or disk efficiency.
$ cat package.json
{
...
"workspaces": ["apps/*", "packages/*"]
}
$ npm install -w apps/app-x [email protected]
added 2 packages, and audited 4 packages in 755ms
found 0 vulnerabilities
$ tree -L 2
.
├── apps
│ └── app-x
├── node_modules
│ ├── app-x -> ../apps/app-x
│ └── react
├── package-lock.json
├── package.json
├── packages
│ └── lib-a
└── pnpm-workspace.yaml
8 directories, 3 files
When installing other packages within the monorepo, specify workspace:.
$ npm install -w apps/app-x lib-a
$ pnpm add --filter app-x lib-a@workspace:
pnpm deploy outputs a standalone package that bundles the actual files of all dependencies. Either enable injectWorkspacePackages or pass –legacy to run it.
$ cat pnpm-workspace.yaml
...
injectWorkspacePackages: true
$ pnpm deploy --filter app-x --prod ./out
$ tree -a -L 3 out/
out/
├── main.js
├── node_modules
│ ├── .modules.yaml
│ ├── .pnpm
│ │ ├── lib-a@file++++home+godgo+pnpmtest+packages+lib-a
│ │ ├── lock.yaml
│ │ └── [email protected]
│ ├── .pnpm-workspace-state-v1.json
│ ├── lib-a -> .pnpm/lib-a@file++++home+godgo+pnpmtest+packages+lib-a/node_modules/lib-a
│ └── react -> .pnpm/[email protected]/node_modules/react
├── package.json
└── pnpm-lock.yaml