pnpm Workspace on macOS: How to Set Up a Monorepo That Links Local Packages on Purpose
A practical pnpm workspace guide for macOS covering monorepo setup, local package linking, the workspace protocol, install behavior, and the mistakes that make developers think their packages are not resolving.
Why this keeps frustrating people: monorepos are not confusing because the concept is hard. They are confusing because package managers appear to “mostly work” even when the workspace is wired incorrectly.
What a pnpm workspace is really solving
If you have a web app, a shared UI package, and maybe a config package, you do not want to publish every internal change to npm just to test it locally. A workspace lets multiple packages live in one repository and resolve each other directly.
With pnpm, that setup is usually fast and clean, but only if you define the workspace structure explicitly.
A minimal monorepo layout
Example:
my-monorepo/
package.json
pnpm-workspace.yaml
apps/
web/
package.json
packages/
ui/
package.jsonAt the repo root, create:
packages:
- "apps/*"
- "packages/*"That file is what tells pnpm which directories belong to the workspace.
Root package setup
A simple root package.json can look like this:
{
"name": "my-monorepo",
"private": true,
"packageManager": "pnpm@10",
"scripts": {
"dev:web": "pnpm --filter web dev",
"build": "pnpm -r build"
}
}Marking the repo as private is a good default. It prevents accidental publication of the root package.
The mistake people make with local dependencies
Inside apps/web/package.json, developers often try this:
{
"dependencies": {
"@acme/ui": "^1.0.0"
}
}That looks normal, but it can silently drift away from your local package if the version numbers stop matching.
A better approach is the workspace protocol:
{
"dependencies": {
"@acme/ui": "workspace:*"
}
}That tells pnpm you expect the dependency to come from the local workspace, not from the public registry.
Install the workspace the right way
From the repo root:
pnpm installDo not jump into one subpackage and start with pnpm install there before the workspace is defined. That is one of the easiest ways to create confusing partial state.
A good first verification step
After install, check whether pnpm understands the repo structure:
pnpm -r list --depth 0If the workspace is wired correctly, you should see packages from both apps and packages.
You can also inspect one app specifically:
pnpm --filter web list --depth 0Why local packages seem “not updated”
Usually one of these is true:
- the package folder is not included in
pnpm-workspace.yaml - the dependency is pointing to a registry version instead of
workspace:* - the consuming app needs a restart because its dev server cached module state
- the package build output is stale if the shared package has its own build step
That fourth point matters a lot. If your UI package compiles src into dist, changing the source code may not help until the package is rebuilt.
Useful workspace commands
Run a command in all packages:
pnpm -r buildRun only in one package:
pnpm --filter @acme/ui buildRun a package and its dependencies:
pnpm --filter web... buildThese filter patterns are one reason pnpm becomes comfortable once the repo is set up correctly.
Final recommendation
If your monorepo feels unpredictable, do not blame the concept first. Check the boring parts: pnpm-workspace.yaml, workspace:*, and whether you are installing from the root. Most monorepo pain comes from small structural mismatches, not from pnpm being unreliable.