Using pnpm --filter for Targeted Builds
A targeted build runs only the packages a change actually affects, instead of rebuilding the whole repository. pnpm's --filter flag delivers that natively, but it fails in confusing ways when the selector does not resolve: either it errors out, or worse, it silently falls back to running everywhere. This page covers the exact symptoms, the root cause, a step-by-step recovery workflow, and the CI guardrails that keep targeted builds targeted. It builds directly on pnpm Workspace Filtering.
Exact Symptoms
ERR_PNPM_FILTER_NO_MATCH No projects matched the filters in "/repo"
Other signals that filtering has gone wrong:
- Builds execute on packages you never named, and CI duration creeps back up toward a full-workspace run.
- A
--filter '...[origin/main]'selection resolves to zero packages even though files changed. - Cascading "cannot find module" failures because a targeted build skipped a dependency it needed.
Root Cause
ERR_PNPM_FILTER_NO_MATCH and silent full-workspace fallback both trace to the same thing: the selector did not resolve against the workspace graph. The usual causes are misconfigured boundaries in pnpm-workspace.yaml, a package whose package.json name does not match the pattern, a missing traversal operator (...) so dependencies are excluded, or — for change-aware filters — a shallow clone that gives the git diff nothing to compare against. Understanding the underlying resolution model in pnpm Workspace Filtering is what keeps these failures from recurring.
Diagnostic & Recovery Workflow
1. Validate workspace boundaries
Confirm the manifest exists at the repository root and declares every package directory.
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'
pnpm list --recursive --depth=0
This lists every package pnpm recognizes. If a package you expected is absent, its directory is not covered by a glob, or its package.json has no name.
2. Test filter resolution with a dry run
Resolve the filter with pnpm list before you attach an expensive build to it. list is read-only, so it is safe to iterate on.
# Exact name
pnpm list --filter "my-package" --recursive
# Upstream: target + all dependencies
pnpm list --filter "...my-package" --recursive
# Downstream: target + all dependents
pnpm list --filter "my-package..." --recursive
3. Run the targeted build
Once the selector lists the packages you expect, attach the build. Use the upstream operator so dependencies compile first.
pnpm --filter "...my-package" run build
Read the terminal output and confirm only the intended package and its upstream dependencies executed.
Required Configuration Baseline
pnpm-workspace.yamlmust sit at the repository root and cover every package directory.package.jsonin every package needs a valid, uniquenamethat matches the filter pattern you intend to use..npmrcshould setauto-install-peers=trueso peer dependencies resolve consistently across filtered scopes.
CI/CD Safeguards
- Change-aware filtering. Replace bare
pnpm run buildwithpnpm --filter '...[origin/main]' run buildso only modified packages and their dependencies build. - Full history. Set
fetch-depth: 0onactions/checkout— a shallow clone makes the git range resolve to nothing. - Fail on empty match. Add
--fail-if-no-match(pnpm v8+) so a typo errors instead of silently building nothing. - Production-only installs. Use
pnpm install --prodin deploy steps to keepdevDependenciesout of the resolution graph. - Pre-commit consistency. Run
pnpm install --frozen-lockfileandpnpm list --recursive --depth=0in a hook to catch a broken workspace before it reaches CI.
Frequently Asked Questions
What is the difference between pnpm --filter my-package and pnpm --filter ...my-package?
The ... prefix turns on upstream dependency-aware filtering, so pnpm builds the target plus every workspace dependency it needs — which prevents missing-module errors during compilation. Without the prefix, only the exact named package runs.
Why does pnpm --filter build packages I did not specify?
You used a traversal operator. ...pkg pulls in the upstream dependency chain and pkg... pulls in the downstream dependent chain. Drop the operators and pass an exact name to restrict execution to a single package.
Why does --filter '...[origin/main]' build nothing in CI?
The runner cloned shallowly, so the diff against origin/main has no history to compare. Set fetch-depth: 0 on the checkout step, and make sure the base ref is actually fetched.
Can I combine pnpm --filter with Turborepo or Nx?
You can, but it is usually redundant — both provide their own task graph and caching. Reserve pnpm --filter for raw script execution or for bypassing the orchestrator on a single targeted install or audit.
Related
- pnpm Workspace Filtering — the full filter syntax, resolution model, and change-aware CI patterns this page applies.
- Running Scripts Across Workspaces with pnpm — orchestrating scripts across a filtered set in topological order.
- Cross-Package Dependency Management — how internal
workspace:links determine what an upstream filter pulls in.