pnpm 08 📦 pnpm Workspaces and Monorepo Architecture Management
A monorepo stores multiple packages in a single repository. pnpm Workspaces is the mechanism that makes this practical: it installs all packages’ dependencies together into a single virtual store, links packages to each other without publishing to a registry, and provides commands to run scripts across the entire project. The architecture is defined by a single configuration file—pnpm-workspace.yaml—which tells pnpm which directories contain packages. Unlike npm and Yarn, which use the workspaces field in package.json, pnpm keeps the workspace definition separate from the manifest. This separation is deliberate: the root package.json remains a package manifest, and the workspace configuration is an independent file that pnpm reads before installing anything.
Key point: A pnpm workspace is any directory matched by a glob in pnpm-workspace.yaml that contains a package.json. The root package.json is always part of the workspace, but it must be private: true if it is not intended for publication.
Why pnpm Workspaces exist
The duplication problem. Without workspaces, a monorepo with three packages that all depend on React installs React three times into three separate node_modules directories. pnpm’s global store eliminates the duplication at the file level—React is stored once and hard-linked—but the workspace adds a second layer: a single lockfile, a single resolution step, and a single set of dependency versions for the entire project.
The linking problem. Package A depends on Package B, both in the same repository. Without a workspace protocol, Package A must reference Package B by a version number that exists on the registry. The workspace: protocol tells pnpm to resolve the dependency to the local workspace regardless of the version, and replaces the protocol with the actual version only when the package is published.
The orchestration problem. Running a build across ten packages requires running ten commands in dependency order. pnpm -r run build executes the script in every workspace. The --filter flag narrows the scope to specific packages, and the --filter ... syntax includes dependencies. The topological sort ensures that a package’s dependencies build before the package itself.
The configuration problem. The root pnpm-workspace.yaml is also the place for shared settings: catalog versions (shared dependency versions across workspaces), patched dependencies, and package extensions. These are defined once and applied everywhere.
The trade-off. Workspaces reduce duplication and simplify cross-package development, but they introduce a single point of failure. A broken script in one workspace can affect the entire repository’s CI. Every workspace must declare its own dependencies; pnpm’s strictness enforces this, but the discipline is the responsibility of the developers.
a. Defining the workspace
The workspace is defined in pnpm-workspace.yaml at the repository root. It contains a list of glob patterns. Any directory matching a pattern and containing a package.json is a workspace package.
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
- '!**/test/**'
The ! prefix excludes a path from the workspace. The glob patterns are resolved relative to the root. The typical layout separates applications (apps/) from shared libraries (packages/).
The root package.json must have "private": true. This prevents accidental publication of the workspace root.
{
"name": "my-monorepo",
"private": true,
"devDependencies": {
"typescript": "^5.5.0"
}
}
After defining the workspace and creating the sub-packages, pnpm install at the root resolves and installs all workspace dependencies. The resulting lockfile is at the root, and the virtual store (.pnpm) is at the root. Each workspace has its own node_modules, but those directories contain only symlinks to the packages that workspace declared.
b. Linking workspaces with the workspace protocol
To depend on another workspace, the package.json of the consuming package uses the workspace: protocol. The range can be workspace:*, workspace:^, or an explicit version.
{
"name": "@myorg/app",
"dependencies": {
"@myorg/utils": "workspace:*"
}
}
The workspace:* protocol tells pnpm to resolve the dependency to the local workspace. During pnpm install, pnpm creates a symlink from @myorg/app‘s node_modules to the @myorg/utils workspace. When the package is published to a registry, the protocol is replaced with the actual version of @myorg/utils at the time of publication.
This is the same mechanism used by Yarn Berry, and it solves the “local development vs published package” problem without requiring a separate publishing step for internal dependencies.
c. Filtering and topological execution
The --filter (-F) flag selects specific workspaces for execution. The ... suffix includes dependencies, and the ^... prefix includes dependents.
# Run build in all workspaces
pnpm -r run build
# Run build in the app package and its dependencies
pnpm --filter @myorg/app... run build
# Run build in the app package and its dependents
pnpm --filter ...@myorg/utils run build
The ... suffix is the most useful filter for monorepos. pnpm --filter foo... run build builds foo and every package that foo depends on. The execution order is topological: dependencies are built before the packages that depend on them.
For workspace management, pnpm -r exec runs an arbitrary command in every workspace. This is useful for one-off operations like generating files or checking configuration.
Complete Example Session
# ============================================
# PART 1: CREATE THE WORKSPACE ROOT
# ============================================
mkdir my-monorepo && cd my-monorepo
pnpm init
# Edit package.json: add "private": true
# ============================================
# PART 2: DEFINE THE WORKSPACE
# ============================================
cat > pnpm-workspace.yaml <<'EOF'
packages:
- 'apps/*'
- 'packages/*'
EOF
# ============================================
# PART 3: CREATE A WORKSPACE PACKAGE
# ============================================
mkdir -p packages/utils
cd packages/utils
pnpm init
# Edit package.json: "name": "@myorg/utils"
# ============================================
# PART 4: LINK A WORKSPACE
# ============================================
# In apps/web/package.json:
# "dependencies": { "@myorg/utils": "workspace:*" }
# ============================================
# PART 5: INSTALL EVERYTHING
# ============================================
cd ../..
pnpm install
# The virtual store is at the root.
# Workspace packages are linked via symlinks.
# ============================================
# PART 6: RUN SCRIPTS ACROSS WORKSPACES
# ============================================
pnpm -r run build
# Runs "build" in every workspace that defines it.
# ============================================
# PART 7: FILTER BY DEPENDENCY
# ============================================
pnpm --filter @myorg/app... run build
# Builds @myorg/app and its dependencies.
# ============================================
# PART 8: SHARE DEPENDENCY VERSIONS (CATALOG)
# ============================================
# pnpm-workspace.yaml:
# catalog:
# react: ^19.0.0
# In workspace package.json:
# "dependencies": { "react": "catalog:" }
# ============================================
# PART 9: EXEC IN ALL WORKSPACES
# ============================================
pnpm -r exec pwd
# Prints the directory of every workspace.
The nine parts trace the workspace lifecycle: root creation, workspace definition, package creation, linking, installation, script execution, filtering, shared versions, and arbitrary execution.
Quick Reference
Workspace Configuration
| File | Purpose |
|---|---|
pnpm-workspace.yaml | Defines packages and shared settings |
Root package.json | "private": true required |
workspace:* | Links to local workspace |
Filtering Syntax
| Pattern | Meaning |
|---|---|
--filter <name> | Specific workspace |
--filter <name>... | Workspace and its dependencies |
--filter ...<name> | Workspace and its dependents |
--filter '!<name>' | Exclude workspace |
Recursive Commands
| Command | Purpose |
|---|---|
pnpm -r run <script> | Run script in all workspaces |
pnpm -r exec <cmd> | Execute arbitrary command in all workspaces |
pnpm -r test | Run tests in all workspaces |
Best Practices
✅ Do This:
packages:
- 'apps/*'
- 'packages/*'
- ✅ Use explicit glob patterns
{ "private": true }
- ✅ Root must be private
{ "dependencies": { "@myorg/utils": "workspace:*" } }
- ✅ Use
workspace:protocol for internal deps
❌ Don’t Do This:
{ "name": "root" }
- ❌ Missing
private: truebreaks workspace
# Rely on hoisted dependency without declaring it
- ❌ Breaks when package is published
pnpm -r run build
- ❌ Without topological sort, build order may be wrong
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Workspaces ignored | Root not private: true | Add "private": true |
| Internal dep not linked | No workspace: protocol | Use workspace:* |
| Wrong build order | -r without topological sort | Use --filter <name>... |
| Filter matches nothing | Wrong package name | Check name in package.json |
| Catalog not applied | Wrong syntax in pnpm-workspace.yaml | Verify catalog: syntax |
Real-World Examples
1. Define Workspace
packages:
- 'apps/*'
- 'packages/*'
2. Link Internal Package
{ "dependencies": { "@myorg/ui": "workspace:*" } }
3. Run in All Workspaces
pnpm -r run build
4. Build App and Dependencies
pnpm --filter @myorg/app... run build
5. Build Utilities and Dependents
pnpm --filter ...@myorg/utils run build
6. Catalog for Shared Version
catalog:
react: ^19.0.0
7. Reference Catalog
{ "dependencies": { "react": "catalog:" } }
8. Exec in All Workspaces
pnpm -r exec pwd
9. Filter by Directory
pnpm --filter "./apps/*" run test
10. Check Workspace List
pnpm m ls
Visual
Monorepo Structure
┌──────────────────────────────────────────────┐
│ my-monorepo/ │
│ ├── pnpm-workspace.yaml │
│ ├── package.json (private: true) │
│ ├── pnpm-lock.yaml │
│ ├── node_modules/ │
│ │ └── .pnpm/ (global virtual store) │
│ ├── apps/ │
│ │ ├── web/ │
│ │ │ └── package.json │
│ │ └── api/ │
│ │ └── package.json │
│ └── packages/ │
│ └── utils/ │
│ └── package.json │
└──────────────────┬───────────────────────────┘
│
│ pnpm install
▼
┌──────────────────────────────────────────────┐
│ All dependencies linked from the store. │
│ Workspace packages linked via symlinks. │
└──────────────────────────────────────────────┘
Filter Execution
┌──────────────────────────────────────────────┐
│ pnpm --filter @myorg/app... run build │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Include @myorg/app and its dependencies: │
│ 1. @myorg/utils (no deps) │
│ 2. @myorg/ui (depends on utils) │
│ 3. @myorg/app (depends on ui, utils) │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Build runs in topological order. │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Workspace definition | pnpm-workspace.yaml |
| Root requirement | "private": true |
| Internal dependency | workspace:* |
| Run all workspaces | pnpm -r run <script> |
| Filter with dependencies | pnpm --filter <name>... run |
| Filter with dependents | pnpm --filter ...<name> run |
| Shared versions | catalog: in pnpm-workspace.yaml |
| Arbitrary command | pnpm -r exec <cmd> |
Key takeaways:
- pnpm Workspaces are defined in
pnpm-workspace.yaml, notpackage.json. The glob patterns list the directories that contain packages. The root must beprivate: true. - The
workspace:protocol links internal packages. It resolves to the local workspace regardless of registry versions, and the range determines the published manifest. - The
--filterflag is the primary orchestration tool. The...suffix includes dependencies, and the...prefix includes dependents. The execution order is topological. pnpm -rruns across all workspaces. Therunsubcommand executes scripts;execruns arbitrary commands.- Catalogs share dependency versions. A
catalog:entry inpnpm-workspace.yamlreplaces duplicated version ranges across workspaces.
Remember: A pnpm workspace is a directory matched by pnpm-workspace.yaml that contains a package.json. The root owns the lockfile and the virtual store. The workspace: protocol links packages without a registry. The --filter command narrows the scope. The -r flag expands it. Together, these features turn a collection of packages into a single, coherent project.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!