CalcSnippets Search
JavaScript 3 min read

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.json

At 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 install

Do 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 0

If 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 0

Why local packages seem “not updated”

Usually one of these is true:

  1. the package folder is not included in pnpm-workspace.yaml
  2. the dependency is pointing to a registry version instead of workspace:*
  3. the consuming app needs a restart because its dev server cached module state
  4. 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 build

Run only in one package:

pnpm --filter @acme/ui build

Run a package and its dependencies:

pnpm --filter web... build

These 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.

Sources

Keep reading

Related guides