Yarn Berry Workspaces with PnP: Setup, Constraints, and Trade-offs
Yarn Berry's Plug'n'Play removes node_modules, but zero-installs isn't always worth the repo size. A minimal workspace setup shows how to decide.
11 Jul 2025, 12:08 UTC

The Problem: Slow Installs and Phantom Dependencies
In a JavaScript monorepo, node_modules often grows to gigabytes, and yarn install can block CI for minutes. Worse, Classic Yarn's hoisting can hide missing dependency declarations: code works locally because a transitive package is physically present, then fails in CI when the tree resolves differently. Yarn Berry (v2+) with Plug'n'Play (PnP) replaces the physical tree with a resolver file, .pnp.cjs, that maps imports directly to packages in Yarn's global cache.
What Workspaces and PnP Change
Workspaces let multiple packages share one repository and one lockfile. The workspace: protocol in package.json declares a dependency on a local sibling without publishing to npm. PnP stops creating node_modules entirely; instead, Node loads .pnp.cjs at startup and resolves packages from the cache. Each workspace keeps its own resolution graph, so an undeclared dependency throws an error instead of silently working.
Worked Example: Two-Workspace Monorepo
Run these commands in a terminal inside a new, empty directory. No special permissions are required. The commands create a .git directory and modify .yarnrc.yml; delete those files to undo the setup.
# 1. Initialize workspace root and switch to Yarn Berry
mkdir yarn-pnp-demo && cd yarn-pnp-demo
yarn init -w
yarn set version berry
printf 'nodeLinker: pnp\n' >> .yarnrc.yml
# 2. Add two workspaces
mkdir -p packages/api packages/ui
cat > packages/api/package.json <<'EOF'
{
"name": "api",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"ui": "workspace:*"
}
}
EOF
cat > packages/ui/package.json <<'EOF'
{
"name": "ui",
"version": "1.0.0",
"main": "index.js"
}
EOF
echo "module.exports = { color: 'blue' };" > packages/ui/index.js
echo "const ui = require('ui'); module.exports = { theme: ui.color };" > packages/api/index.js
# 3. Install and inspect
yarn install
ls -a | grep pnp
After yarn install, confirm that .pnp.cjs exists and that no node_modules folder was created at the root. To verify resolution through the PnP loader, run:
node -e "console.log(require('./packages/api').theme)"
If the resolver works, the command prints the value exported by packages/ui. If it fails with "Cannot find module", the PnP loader is not active—check that nodeLinker: pnp is set and that you are using Yarn Berry, not Yarn 1 Classic.
Adding a Dependency Constraint
Constraints let you declare rules that apply across every workspace, such as requiring a specific TypeScript version. Install the constraints plugin, then create .yarn/constraints.pro:
yarn plugin import constraints
cat > .yarn/constraints.pro <<'EOF'
gen_enforced_dependency(WorkspaceCwd, "typescript", "^5.0.0").
EOF
yarn constraints
The DSL is Prolog-like and runs at install time. Start with one rule; complex constraints can slow installs and are difficult to debug. A practical check is to run yarn install after adding a rule and confirm it succeeds without error.
Trade-offs: Zero-Installs, IDEs, and Native Modules
- Repo size: Committing
.yarn/cacheenables clone-and-run withoutyarn install, but the cache stores ZIP archives and can grow large. For large monorepos, consider skipping zero-installs and letting CI runyarn installinstead. - IDE integration: TypeScript and ESLint assume
node_modulesexists. The official Yarn VS Code extension teaches the editor to use.pnp.cjs; without it, go-to-definition and auto-imports break across workspace boundaries. - Native modules: Packages using
node-gypmay expectnode_modulesduring postinstall scripts. Test native dependencies early; Yarn 4 improves script isolation, but edge cases remain. - Tooling assumptions: Some Webpack loaders and older Jest versions assume physical
node_modules. You can switch tonodeLinker: node-modulesin.yarnrc.ymlfor those packages, but you lose PnP benefits.
How to Verify and Decide
- Run the worked example above and confirm
node -eresolves the workspace import. - Add one simple constraint and run
yarn constraintsto validate the DSL syntax. - Clone the repository to a temporary directory and run
yarn install. Measure whether the time saved justifies committing.yarn/cache. - Install the Yarn VS Code extension and open a workspace file to confirm go-to-definition works.
Yarn Berry Workspaces with PnP removes phantom dependencies and can cut install time, but it is not a free switch. Commit the cache only if your team can standardize on the IDE extension and accept the larger clone size; otherwise, run yarn install in CI and keep the repository lean.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.