pnpm --filter build: Why It Throws 93 Module Not Found
This article may contain affiliate links. Its content is not affected by advertising.
In short
`pnpm --filter <pkg>` only runs that package's own script — it doesn't include building the workspace packages it depends on. To build those too, append `...` to `<pkg>`.
Conclusion
In a pnpm workspace monorepo, running pnpm --filter web build inside Docker to build only the target package left the workspace packages it depends on without a generated dist, and Turbopack’s resolution attempt threw 93 “Module not found” errors. Appending ... to the end of the --filter target name — so install and build both cover “the target package and its dependencies” — fixed it.
Symptom
In a pnpm workspace of apps/* and packages/*, apps/web depends on packages/ai, packages/db, and packages/observability. Trying to build just this web app in the Cloud Run apps/web/Dockerfile produced problems at two separate stages.
First, the deps stage only copied the package.json files for apps/web and packages/db.
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY apps/web/package.json apps/web/package.json
COPY packages/db/package.json packages/db/package.json
RUN pnpm install --frozen-lockfile
Running pnpm install --frozen-lockfile against the whole workspace while the package.json files for packages/ai and packages/observability — both dependencies of apps/web — were missing meant the workspace structure recorded in the lockfile didn’t match the files actually copied in, and install failed on a lockfile mismatch.
Even after getting install to pass, the builder stage also scoped the build to web alone.
RUN pnpm --filter @scope/web build
This command only runs @scope/web’s own build script. The build scripts for packages/ai, packages/db, and packages/observability never run, so none of their dist/ directories get generated. apps/web‘s source imports those workspace dependencies through node_modules, but with the resolved dist missing, the Turbopack build used by Next.js 16 couldn’t resolve the dependency packages’ entry points and threw 93 “Module not found” errors.
Cause
pnpm --filter <package-name> scopes the command’s execution target to only the named package itself. It never propagates to other packages in the workspace. This holds for both install and build.
- When install hits this constraint:
--frozen-lockfilechecks consistency against the whole workspace’s lockfile, so copying in only a subset ofpackage.jsonfiles while scoping the target creates a mismatch against what the lockfile expects, and install fails - When build hits this constraint: the
buildscript doesn’t propagate outside the target package, so the dependency packages’distis never built and stays missing
pnpm expresses “the target package plus its dependencies” by appending ... to the end of the package name. Without ..., --filter @scope/web refers literally to @scope/web alone and never walks the dependency graph.
Fix
We stopped splitting out a separate deps stage, copied in the whole monorepo along with its workspace structure, and used a ...-suffixed filter for both install and build.
# Pull in the whole monorepo (node_modules/.next etc. already excluded via .dockerignore)
COPY . .
# Install only @scope/web and its dependency subtree (ai / db / observability + transitive)
RUN pnpm install --frozen-lockfile --filter @scope/web...
# ...
# `...` includes web's workspace dependencies (ai / db / observability) in the build too.
# pnpm's recursive run executes in dependency order (dependencies first),
# so ai/db/observability's dist is generated first and web becomes resolvable
RUN pnpm --filter @scope/web... build
pnpm install --frozen-lockfile --filter @scope/web... with ... appended installs against the whole workspace’s package.json set, scoped to @scope/web and its dependency subtree. Since we now copy in everything, no files get dropped by the scoping.
We changed the build side the same way, to pnpm --filter @scope/web... build. pnpm’s recursive execution resolves the dependency graph and runs each package’s script in dependency order (dependents run after what they depend on), so ai, db, and observability build first and their dist is in place by the time web’s build runs, letting Turbopack resolve the dependency packages.
Recurrence risk
Nothing in this record describes adding a check that verifies --filter’s scope in CI. This particular case was fixed as a single missing ..., but the underlying structure of pnpm workspaces — a scoped command not propagating to its dependencies — hasn’t changed. Writing another script or Dockerfile stage that scopes a command to a single target again leaves room for the same symptom to reappear.
Frequently asked questions
Q1What's the difference between `pnpm --filter web build` and `pnpm --filter web... build`?
Adding `...` includes the workspace packages it depends on in the build target too. Without it, only web's own build script runs, so dependency packages' dist is never generated. Turbopack then tries to resolve that dist, and when it's missing you get Module not found.
Q2Does it work if I add `--filter web...` only to install and leave build alone?
No. Install aligns every workspace package.json and settles the lockfile; build is what produces dist. Adding `...` only to install still leaves dependencies unbuilt, so dist never exists and Module not found doesn't go away.
Q3Why does a missing dist turn into a Turbopack Module not found at all?
A workspace package's package.json points main/exports at files under dist. The source exists, but if the dist folder itself doesn't exist, Turbopack can't resolve that entry the moment it tries, and treats it as Module not found.
Environment verified
- pnpm 11.4.0 (packageManager pinned) / Next.js ^16.0.0 (build via Turbopack)
- Hit in a production Docker build on 2026-06-04, fixed same day
What this article is based on
- file lines 11-22commit d91461a
- file lines 24-41commit d91461a
- file lines 17-27commit 8a2ad05
- file lines 43-46commit 8a2ad05
- JSON file lines 7-11commit 8a2ad05
- JSON file lines 25-25commit 8a2ad05
- JSON file lines 19-21commit 8a2ad05
- YAML file lines 1-3commit 8a2ad05
Every claim in this article comes from the records above. The repositories we operate are private so we cannot link to them, but which file, which lines, and at which commit we read them is recorded for every article. Nothing here is written from guesswork.