Fix: pnpm Workspace Not Working — workspace:* Protocol, Catalog, Filters, and Hoisting Issues
Quick Answer
How to fix pnpm workspace errors — workspace:* not resolving, catalog versions out of sync, --filter not matching, peer deps unmet across packages, shamefully-hoist trade-offs, and publishConfig for releases.
The Error
You add a workspace dependency with workspace:* and pnpm install fails:
ERR_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE
In packages/app: "@my-org/shared@workspace:*" is in the dependencies but no package named "@my-org/shared" is present in the workspaceOr the package resolves locally but pnpm publish ships a wrong-looking dependency:
{
"dependencies": {
"@my-org/shared": "workspace:*"
}
}(It should be replaced with a real version on publish.)
Or your --filter pattern matches nothing:
$ pnpm --filter ./apps/web build
No projects matched the filtersOr shared peers like react install at different versions in different packages and tests fail with Invalid hook call:
Invalid hook call. Hooks can only be called inside of the body of a function component.
This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)Why This Happens
pnpm’s workspace model is stricter than npm’s:
- Strict node_modules. Each package only sees the dependencies it explicitly declares. This catches accidental imports of transitive deps — and breaks code that relied on them.
workspace:*is a protocol, not a version range. It tells pnpm “link to the local copy.” On publish, pnpm rewrites it to the real version of the local package. If that package isn’t inpnpm-workspace.yaml, the protocol can’t resolve.--filtermatches againstnamefrompackage.jsonand against path globs. Misuse (wrong glob, wrong name pattern) produces “matched nothing” silently.- Catalogs unify versions across packages. Without them, each package can pull a different React, leading to “two React copies” runtime bugs.
Fix 1: Declare All Packages in pnpm-workspace.yaml
Every package directory must be covered by the packages field:
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
- "tools/*"After editing, re-run pnpm install. Verify with:
pnpm list -r --depth -1
# Lists every workspace package the resolver knows about.If @my-org/shared isn’t listed, the glob didn’t match. Check the directory structure — the name in packages/shared/package.json must be @my-org/shared, and the directory must be under one of the globs.
Common Mistake: Adding a new package directory but forgetting to run pnpm install at the root. The lockfile is stale until you do.
Fix 2: Use workspace:* (or workspace:^) in Internal Dependencies
In apps/web/package.json:
{
"dependencies": {
"@my-org/shared": "workspace:*"
}
}Three forms:
workspace:*— always link to the local version, regardless of what it is.workspace:^— link to local, publish as^X.Y.Z(caret of the current local version).workspace:~— link to local, publish as~X.Y.Z.workspace:1.2.3— link to local, publish as1.2.3(exact).
For internal apps that don’t publish, workspace:* is simplest. For published libraries that depend on each other, prefer workspace:^ so consumers get semver-compatible releases.
Pro Tip: Use pnpm add @my-org/shared --workspace --filter @my-org/web to add the dependency with the correct workspace: protocol — avoids hand-editing package.json.
Fix 3: Configure publishConfig for Publishable Packages
When you pnpm publish a workspace package, pnpm rewrites workspace: protocols to real versions in the published package.json. But you also want to control which files ship:
{
"name": "@my-org/shared",
"version": "1.2.3",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist", "README.md"],
"publishConfig": {
"access": "public",
"main": "./dist/index.js",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
}
}
}publishConfig fields override the top-level on publish. Useful when:
- You want different
exportspaths in published vs source (src/vsdist/). - You need
access: "public"for scoped packages on the public npm registry. - You’re using
modulefor ESM and want a published consumer-friendlymain.
Fix 4: --filter Patterns
--filter accepts package names, paths, and special selectors:
# By package name
pnpm --filter @my-org/web build
pnpm --filter "@my-org/*" build # All packages in the @my-org scope
# By path
pnpm --filter "./apps/**" build # All apps
pnpm --filter "./packages/shared" build # One specific package by path
# By dependents — build a package and everything that depends on it
pnpm --filter ...@my-org/shared build
# By dependencies — build a package and what it depends on
pnpm --filter @my-org/web... build
# Changed since main branch
pnpm --filter "[main]" buildThe ... syntax is powerful for incremental rebuilds. To rebuild only what changed since main:
pnpm --filter "...[origin/main]" buildThis selects all packages with changes since origin/main plus every package that depends on them, transitively. Cuts build time dramatically in CI.
Common Mistake: Quoting filters wrong on Windows. PowerShell eats the * in --filter @my-org/*. Quote explicitly: pnpm --filter "@my-org/*" build.
Fix 5: Use Catalogs to Pin Shared Versions
Catalogs (added in pnpm 9) let you define versions once at the workspace level and reference them from each package:
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
catalog:
react: ^19.0.0
react-dom: ^19.0.0
typescript: ^5.6.0
catalogs:
testing:
vitest: ^2.0.0
"@testing-library/react": ^16.0.0In each package.json:
{
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:"
},
"devDependencies": {
"vitest": "catalog:testing",
"@testing-library/react": "catalog:testing"
}
}catalog: (no name) refers to the default catalog. catalog:testing refers to a named one. Upgrade React across the entire monorepo by changing one line in pnpm-workspace.yaml.
Note: Catalogs require pnpm 9.0+. On older versions you’ll see Unsupported version reference: catalog:.
Fix 6: Hoisting and Strict Peer Resolution
pnpm puts dependencies in node_modules/.pnpm/ and symlinks only the declared deps into each package’s node_modules/. This is strict-by-default — code that does require("lodash") without declaring lodash will fail.
If a tool you can’t modify needs hoisting:
# .npmrc (at workspace root)
public-hoist-pattern[]=*types*
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*This selectively hoists packages matching the patterns into the root node_modules. It’s the safe alternative to shamefully-hoist=true, which dumps everything at the root and defeats the point of pnpm’s strictness.
For React projects with hook-call errors caused by duplicate React installs:
# .npmrc
public-hoist-pattern[]=react
public-hoist-pattern[]=react-domCombined with a catalog entry, this guarantees exactly one React in the entire workspace.
Fix 7: Patching Dependencies
When a third-party package has a bug, patch it in place:
pnpm patch some-package
# pnpm creates a temporary copy. Edit files there.
pnpm patch-commit /tmp/abc123
# pnpm writes the patch to patches/ and updates package.json.{
"pnpm": {
"patchedDependencies": {
"[email protected]": "patches/[email protected]"
}
}
}Commit the patches/ directory. Every pnpm install re-applies the patch.
Pro Tip: Patches are tied to specific versions. When the package upgrades, the patch may not apply. Either pin the version in package.json or be ready to re-create the patch on each upgrade.
Fix 8: CI: Use Frozen Lockfile
In CI, always install with frozen lockfile to catch drift:
# .github/workflows/ci.yml
- run: pnpm install --frozen-lockfile--frozen-lockfile fails if pnpm-lock.yaml would be modified by the install. This catches:
- A
package.jsonchange not reflected in the lockfile. - A lockfile committed without running
pnpm installfirst. - A catalog version updated but
pnpm installnot re-run.
Pair with corepack to pin the pnpm version itself:
{
"packageManager": "[email protected]"
}Enable in CI:
- run: corepack enable
- run: pnpm install --frozen-lockfileStill Not Working?
A few less-obvious failures:
ENOENT: no such file or directoryfor a workspace package. The package was added topnpm-workspace.yamlbut its directory doesn’t exist yet. Create the directory with a minimalpackage.jsonbefore installing.workspace:*shipped to npm un-rewritten. You published withnpm publishinstead ofpnpm publish. Onlypnpm publishrewrites the protocol. Use it (or wire up Changesets / Release Please which use it internally).- Two copies of
reactafter install. Either the catalog isn’t applied (checkpnpm-workspace.yaml), or one package declaresreactas a peer dep and ships its own dev version. Runpnpm why reactto trace. pnpm installslow in CI even with cache. Cache the~/.local/share/pnpm/storedirectory.actions/setup-nodedoesn’t cache it by default — usepnpm/action-setupor set up caching manually.tsc -brebuilds everything despite Turborepo cache. TypeScript references invalidate onpackage.jsonchanges. Either narrow the deps that affect the cache key, or usetsc --build --incrementalwith cachedtsbuildinfofiles.pnpm installdeletes node_modules of a package. Apreparescript in that package fails, pnpm rolls back. Check the package’sprepare/postinstallscript for errors.--filterincludes the root package unexpectedly. The workspace root counts as a package with name from rootpackage.json. Filter it out with--filter !@root-nameif it’s interfering.- PowerShell on Windows:
pnpm execdoesn’t find local binaries. Usepnpm dlxfor one-off runs, or add the localnode_modules/.binto PATH inside the command.
For related package manager and monorepo issues, see pnpm peer dependency error, Turborepo not working, npm eresolve unable to resolve dependency tree, and npm err peer dep conflict.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Node.js fs.watch Not Working — Cross-Platform Quirks, chokidar Migration, Recursive Watch
How to fix Node.js file watching — fs.watch unreliable on Linux, missing events on save, recursive watch on Windows/macOS, chokidar polling fallback, ignoring patterns, debouncing, and EMFILE errors.
Fix: Node.js Stream pipeline() Not Working — Backpressure, Error Propagation, AbortSignal, and Web Streams Interop
How to fix Node.js stream/promises pipeline errors — uncaught stream errors, backpressure ignored, AbortSignal not propagating, async iterators in pipeline, Transform stream object mode, and converting between Node and Web Streams.
Fix: Node.js Test Runner Not Working — node --test, TypeScript, Mocks, Coverage, and Watch Mode
How to fix Node.js built-in test runner errors — node --test not finding files, ESM vs CJS imports, TypeScript with --experimental-strip-types, mock.method isolation, coverage reporting, and watch mode setup.
Fix: Nx Not Working — project.json Targets, Affected Commands, Caching, and Generators
How to fix Nx errors — nx.json plugin config, project.json target inputs/outputs, nx affected base branch, cache misses, generator schema, custom executors, and nx migrate failures.