Rebounder Tech Blog

運用している当事者が書く、本番システムの記録。

pnpm --filter buildがModule not foundを93件出した原因

公開 読了時間 約5分執筆: Rebounder 開発チーム(当該システムの運用当事者)

※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。

結論

`pnpm --filter <pkg>` は指定したパッケージ自身のスクリプトしか実行せず、依存workspaceパッケージのbuildまでは含まない。依存先も一緒にbuildするには`<pkg>...`と`...`を付ける必要がある。

結論

pnpm workspaceのモノレポで、Dockerの中でpnpm --filter web buildのように対象パッケージだけを指定してbuildすると、依存しているworkspaceパッケージのdistが生成されないままTurbopackが解決を試みて、“Module not found”が93件出ました。 --filterの対象名の末尾に...を付けて「対象パッケージとその依存先」をまとめてinstall・buildの範囲に含めることで解消しました。

症状

apps/* packages/*のpnpm workspaceで、apps/webpackages/aipackages/dbpackages/observabilityに依存しています。Cloud Run用のapps/web/Dockerfileでこのwebアプリだけをbuildしようとしたところ、2段階で問題が出ました。

まずdepsステージがapps/webpackages/dbpackage.jsonしかCOPYしていませんでした。

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

apps/webが依存するpackages/aipackages/observabilitypackage.jsonが無いままworkspace全体を対象にしたpnpm install --frozen-lockfileを実行すると、lockfileが記録しているworkspace構成とコピーしたファイルの構成が一致せず、lockfile不一致でinstallが失敗します。

installを通した後のbuilderステージも、build対象をwebだけに絞っていました。

RUN pnpm --filter @scope/web build

このコマンドは@scope/web自身のbuildスクリプトだけを実行します。packages/aipackages/dbpackages/observabilitybuildスクリプトは一度も走らず、それぞれのdist/は生成されません。apps/webのソースはworkspace内の依存パッケージをnode_modules経由のimportで参照しますが、その解決先であるdistが存在しないため、Next.js 16のbuildが使うTurbopackが依存パッケージのエントリを解決できず、“Module not found”を93件出しました。

原因

pnpm --filter <パッケージ名>は、そのコマンドの実行対象を指定したパッケージ自身だけに絞ります。workspace内の他のパッケージへは一切波及しません。これはinstallでもbuildでも同じ挙動です。

  • installフェーズでこの制約に引っかかる場合:--frozen-lockfileはworkspace全体のlockfileとの整合を見るため、対象を絞ってpackage.jsonの一部だけをコピーすると、lockfileが期待する構成と食い違って失敗します
  • buildフェーズでこの制約に引っかかる場合:buildスクリプトが対象パッケージの外に伝播しないため、依存先パッケージのdistはビルドされないまま残ります

pnpmはこの「対象パッケージ+その依存先も含める」という範囲指定を、パッケージ名の末尾に...を付けることで表現します。...が無い--filter @scope/webは文字通り@scope/web単体だけを指しており、依存関係グラフを辿ってはくれません。

直す

depsステージを分けるのをやめて、モノレポ全体をワークスペースの構造ごとCOPYしたうえで、install・buildの両方に...付きのフィルタを使うように変更しました。

# モノレポを丸ごと取り込む(.dockerignoreでnode_modules/.next等は除外済)
COPY . .

# @scope/web とその依存サブツリー(ai / db / observability + 推移)だけinstall
RUN pnpm install --frozen-lockfile --filter @scope/web...

# ...

# `...` でweb の workspace 依存(ai / db / observability)も含めてbuildする。
# pnpmはrecursive runを依存順(依存先が先)で実行するので、
# ai/db/observabilityのdistが先に生成されweb が解決可能になる
RUN pnpm --filter @scope/web... build

...を付けたpnpm install --frozen-lockfile --filter @scope/web...は、@scope/webとその依存サブツリーだけを対象にworkspace全体のpackage.jsonを揃えてinstallします。全体を丸ごとCOPYしているため、絞り込みによる取りこぼしは起きません。

build側もpnpm --filter @scope/web... buildに変更しました。pnpmのrecursive実行は依存関係を解決してから依存順(依存されている側を先)に各パッケージのスクリプトを実行するため、aidbobservabilityが先にbuildされてdistが揃い、その後に実行されるwebのbuildがTurbopackで依存パッケージを解決できるようになります。

再発防止

この根拠には、--filterのフィルタリング範囲を検査でチェックするような仕組みを追加したという記述はありません。今回は...の付け忘れという1箇所の書き方の問題として直っていますが、pnpm workspaceで「対象を絞ったコマンドが依存先へ伝播しない」という構造自体は変わっていません。同じDockerfileや別のスクリプトで再び対象だけを指定するフィルタを書けば、同じ症状が再現する余地は残っています。

よくある質問

Q1`pnpm --filter web build`と`pnpm --filter web... build`は何が違いますか?

`...`が付くと、そのパッケージが依存するworkspaceパッケージも一緒にbuild対象へ加わります。付けない場合はweb自身のbuildスクリプトしか実行されず、依存パッケージのdistは生成されません。Turbopackはそのdistを解決しようとするため、無いとModule not foundになります。

Q2installだけ`--filter web...`にしてbuildは付けなくても直りますか?

直りません。installは全workspaceのpackage.jsonを揃えてlockfileの整合を取るフェーズ、buildはdistを作るフェーズです。install側だけ`...`を付けても依存パッケージはbuildされずdistが存在しないままなので、Module not foundは解消しません。

Q3distが無いとなぜTurbopackのModule not foundになるのですか?

workspaceパッケージのpackage.jsonはmain/exportsがdist配下のファイルを指します。ソースは存在してもdistフォルダ自体が無いため、Turbopackがそのエントリを解決しようとした時点でファイルが見つからずModule not foundとして扱われます。

確認した環境

  • pnpm 11.4.0(packageManager固定)/ Next.js ^16.0.0(Turbopackでbuild)
  • 2026-06-04 に本番Dockerビルドで発生・同日修正

この記事の根拠

  • ファイル 11〜22行目コミット d91461a
  • ファイル 24〜41行目コミット d91461a
  • ファイル 17〜27行目コミット 8a2ad05
  • ファイル 43〜46行目コミット 8a2ad05
  • JSONファイル 7〜11行目コミット 8a2ad05
  • JSONファイル 25〜25行目コミット 8a2ad05
  • JSONファイル 19〜21行目コミット 8a2ad05
  • YAMLファイル 1〜3行目コミット 8a2ad05

本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。