pnpm 12のバージョン固定でpnpm-lock.yamlが2つのドキュメントに分かれる — packageManager・devEngines・GitHubが読めないロックファイル

pnpm 12はpackageManagerが設定されていると、1つのpnpm-lock.yamlに---で区切った2つのYAMLドキュメントを書き込みます。pmOnFailでGitHubの依存関係グラフを守る方法と、Corepackを外す具体的な手順をまとめます。

ほぼ黒のカードに、白枠の正方形が3x3のグリッドに分かれオレンジと濃いグレーの正方形が並び1マスだけ空白のpnpmのロゴと、白のpnpmのロゴタイプ
目次

はじめに

このブログは pnpm 11.3.0 で動いていました。pnpm 12 への移行は package.json の packageManager フィールドを 1 行変えるだけに見えました。インストールは 26ms で終わって Already up to date と表示されたのに、pnpm-lock.yaml は 158 行増えていました。

増えた行は pnpm 自身を固定する新しい YAML ドキュメントで、pnpm 12 はこれをファイルの先頭に置き、元の依存関係の記述を 2 つ目のドキュメントへ押し出します。GitHub の依存関係グラフは最初のドキュメントしか読みません。そのままなら GitHub には pnpm しか見えず、このリポジトリの Dependabot アラートは修正済みとして閉じていたはずです。

対処は pmOnFail: ignore という設定 1 つで、ロックファイルを 1 つのドキュメントに保てます。代わりに止まるのは、pnpm がピンのバージョンへ自分で切り替える動作です。これはもともと Corepack が担っていた仕事でした。本記事では、このピンが Corepack から pnpm、そして devEngines へと移ってきた流れを追い、それでも Corepack を外せる理由と、最終的にピンを devEngines に移した経緯を示します。

ロックファイルに 2 つの YAML ドキュメントが入った

pnpm 12 のピンがあると、pnpm-lock.yaml は pnpm 自身を記録する環境ドキュメントから始まり、依存関係の記述は 2 つ目のドキュメントに移ります。このブログのロックファイルに増えた 158 行はまさにそれでした。

pnpm-lock.yaml
---
lockfileVersion: '9.0'
importers:
.:
configDependencies: {}
packageManagerDependencies:
pnpm:
specifier: 12.10.1
version: 12.10.1
packages:
'@pnpm/exe.android-arm64@12.10.1':
resolution: {integrity: sha512-…}
cpu: [arm64]
os: [android]
# … プラットフォーム別バイナリごとに 1 エントリが続き、そのあと 2 つ目のドキュメントが始まる

条件は pnpm のロックファイルのドキュメントに書かれています。環境ドキュメントが書かれるのは、プロジェクトが devEngines.packageManager をバージョンを問わず宣言したとき、または従来の packageManager フィールドで pnpm 12 以降を固定したときです。

このブログの pnpm@11.3.0 のピンではドキュメントが 1 つ、pnpm@12.10.1 では 2 つになったのはこのためです。

2 ドキュメント構成の pnpm-lock.yaml と GitHub が読む範囲ロックファイルを 2 つの YAML ドキュメントを含む 1 つのファイルとして描いている。1 つ目の環境ドキュメントには pnpm とそのプラットフォーム別バイナリだけがあり、2 つ目に依存関係の記述がある。GitHub の依存関係グラフからの矢印は 1 つ目だけを指し、その結果として依存 0 件とアラートのクローズにつながる。2 つ目のドキュメントには「読まれない」と印が付いている。pnpm-lock.yaml---ドキュメント 1: 環境packageManagerDependencies: pnpm 12.10.1@pnpm/exe.* のプラットフォーム別バイナリ---ドキュメント 2: 依存関係の記述importers、packages、snapshotsプロジェクトの依存すべて読まれないGitHub の依存関係グラフ読む依存が 0 件に見えるアラートが修正済み扱いで閉じる
GitHub のパーサーは最初のドキュメントで止まります。そこにあるのは pnpm とそのプラットフォーム別バイナリだけで、プロジェクトの依存は 1 つもありません。

ドキュメントを 1 つだけ読み込む YAML パーサーは、環境ドキュメントしか受け取りません。GitHub の依存関係グラフもその 1 つです。

dependabot/dependabot-core#15904 では、あるリポジトリの SBOM(ソフトウェア部品表)エクスポートが、2 ドキュメント構成のロックファイルがデフォルトブランチに入ってから 1 日で 652 パッケージから 14 に減りました。残った 14 は pnpm、その @pnpm/exe.* バイナリ、ワークフローのアクション、そしてリポジトリ自身でした。最初の報告では、依存を何も更新していないのにアラートが閉じています。

ピンを devEngines.packageManager(バージョン範囲も指定できる、package.json の新しいピンのフィールド)に移しても避けられません。依存を 1 つだけ持つ作業用プロジェクトを 3 つ用意すると、結果は次のとおりでした。

ピンpnpm-lock.yaml のドキュメント区切り
"packageManager": "pnpm@12.8.2"2
"packageManager": "pnpm@12.8.2" と pmOnFail: ignore0
devEngines.packageManager を 12.8.2 に完全一致で指定2

このブログのアップグレード:package.json の 1 行

すでに pnpm 11 を使っているリポジトリなら、アップグレードはピンの変更と設定 1 つで済みます。このブログに必要な設定の移動は pnpm 11 の時点で済んでいて、allowBuilds と overrides は今回の変更前から pnpm-workspace.yaml にありました。

package.json
"packageManager": "pnpm@11.3.0",
"packageManager": "pnpm@12.10.1",
pnpm-workspace.yaml
pmOnFail: ignore

このリポジトリのアドバイザリパッチャーは pnpm audit --json を解析するので、pnpm が Rust で書き直されても、このコマンドの出力形式が変わらないことが条件でした。試したところ、問題はありませんでした。

アップグレード後、GitHub の SBOM エクスポートには 621 パッケージが並び、@pnpm/exe のバイナリは 1 つもありませんでした。pmOnFail: ignore が効いているということです。GitHub が最初のドキュメントしか読まないのは変わりませんが、ロックファイルには依存のドキュメントしか残っていないので、依存はすべて読まれます。環境ドキュメントが残っていれば、一覧は pnpm と @pnpm/exe のバイナリだけになっていたはずです。

pnpm 12 で変わること

pnpm 12 は pnpm を Rust で書き直したもので、pnpm 11 のコマンド、設定、ロックファイル形式をそのまま引き継いでいます。リリース記事には “Upgrading should not feel like a migration” とあります。

公式のベンチマークでは、小さなパッケージを大量に含むテストプロジェクトで、クリーンインストールが pnpm 11 の 7.36 秒から 4.29 秒に短縮されています。キャッシュ、ロックファイル、node_modules がすべてそろった状態での再インストールは 396ms から 15ms です。また、pnpm は 12.4.0 から Python パッケージも管理できます(pnpm-workspace.yaml で python.enabled: true を設定)。ただし公式ドキュメントでは実験的機能とされています。

公開は 2026-08-26 で、このブログが移行した時点の最新は 12.10.1 でした。主な破壊的変更は次のとおりです。

  • pnpm install --frozen-lockfile false は受け付けられなくなりました。--no-frozen-lockfile と書きます。
  • pnpm-workspace.yaml にサポートされていないキーがあると、ERR_PNPM_UNRECOGNIZED_WORKSPACE_SETTINGS で失敗します。
  • Git ホストから取得する依存(github:owner/repo や git+ssh の書き方)は、常にホストの HTTPS アドレス経由で解決されるようになり、ロックファイルに SSH アドレスが記録されなくなりました。

バイナリの形も変わりました。pnpm 12 は、インストール後は Node.js を必要としないネイティブ実行ファイルです。ただし npm 経由でインストールする場合は、今も Node.js 22.13 以降が必要です。

大きな移行だったのは pnpm 11 のほうです。pnpm 11 は設定を package.json と .npmrc から移し、onlyBuiltDependencies を allowBuilds に置き換えました。pnpm 10 から直接上がるリポジトリはこれらを一度に踏むので、記事の最後に落とし穴としてまとめています。

pnpm-lock.yaml を 1 つのドキュメントに保つ

pmOnFail: ignore にすると、pnpm はロックファイルに自分自身を記録しなくなります。pmOnFail は pnpm 11 で入った設定で、実行中の pnpm がピンのバージョンと違うときの動作を決めます。値は download(デフォルト)、error、warn、ignore の 4 つです。ignore では pnpm がピンに従って動かなくなるので、記録するものもなくなります。

ドキュメントを 1 つに保てる値は ignore だけです。名前にある「fail」は、実行中の pnpm とピンのバージョンが一致しないことを指します。ただし、バージョンが一致していても、pmOnFail が ignore でなければ環境ドキュメントは書き込まれます。スタンドアロンの pnpm 12.10.1 を、pnpm@12.8.2 に固定した作業用プロジェクトで実行した結果は次のとおりです。一致するときの列は、pnpm@12.10.1 に固定して実行した結果です。

pmOnFail実行中の pnpm がピンと違うときバージョンが違うときの --- 区切りの数一致するときの --- 区切りの数
download(デフォルト)12.8.2 に切り替わる22
errorERR_PNPM_BAD_PM_VERSION で失敗する—(インストール失敗)2
warn[WARN] This project is configured to use 12.8.2 of pnpm. Your current pnpm is v12.10.1 を表示して 12.10.1 で実行する22
ignore何も表示せずに 12.10.1 で実行する00

ロックファイルにすでに環境ドキュメントがある状態でこの設定を足しても、環境ドキュメントは消えません。pnpm install は Already up to date と表示し、ロックファイルには 2 つのドキュメントが残ったままでした。ピンを変える前のロックファイルを取り戻してから、インストールし直すことで解消しました。

ターミナルウィンドウ
入力
git checkout main -- pnpm-lock.yaml && pnpm install
出力
Already up to date
Done in 34ms using pnpm v12.10.1
ターミナルウィンドウ
入力
grep -c '^---$' pnpm-lock.yaml
出力
0

代償として、pnpm はバージョンを自動で切り替えられなくなります。pnpm@12.8.2 に固定したプロジェクトでスタンドアロンの pnpm 12.10.1 を実行すると、pmOnFail: ignore を設定する前はピンのバージョンを、設定した後は自分自身のバージョン 12.10.1 を返します。

before pmOnFail: ignore
入力
pnpm --version
出力
12.8.2
after pmOnFail: ignore
入力
pnpm --version
出力
12.10.1

ignore では、PATH 上にある pnpm がそのまま実行されます。ピンが効くのは、pnpm 以外の何かがピンを読む場所だけです。たとえば CI では pnpm/action-setup がピンのバージョンをインストールします。

ピンを読むのは誰か:Corepack、pnpm、そして devEngines

Corepack は Node.js 16.9 から 24 に同梱されていた小さなツールで、「プロジェクトごとにどのバージョンのパッケージマネージャーを使うか」という問題を解決するためのものです。プロジェクトが宣言したバージョンを読み、対応する Yarn や pnpm を自動でダウンロードして実行するので、事前にインストールしておく必要がありません。

packageManager はもともと Corepack のフィールドで、後から pnpm も自分で読むようになりました。devEngines.packageManager は、pnpm が現在推奨している、ツールをまたいで使えるフィールドです。どのツールがどのフィールドに従って動くのかがわかって初めて、pmOnFail: ignore の代償が見えてきます。

pnpm のバージョン固定の年表上から下へ日付付きの 7 つの出来事が並ぶ。2021 年 9 月、Node.js 16.9.0 が Corepack と packageManager フィールドを同梱。2024 年 8 月、pnpm 9.7.0 がオプトインでピンのバージョンへ自分で切り替えられるようになる。2024 年 10 月、npm 10.9.0 が devEngines を実装。2025 年 1 月、pnpm 10.0.0 で自己切り替えがデフォルトに。2025 年 10 月、Node.js 25.0.0 が Corepack の同梱を終了。2026 年 4 月、pnpm 11 が devEngines.packageManager と pmOnFail を追加し、packageManager を legacy と呼ぶ。2026 年 8 月、pnpm 12 ではピンがロックファイルに環境ドキュメントを書き込む。2021-09Node.jsNode.js 16.9.0 が Corepack と packageManager フィールドを同梱2024-08pnpmpnpm 9.7.0 がピンのバージョンへ自分で切り替え可能に(オプトイン)2024-10npmnpm 10.9.0 が devEngines を実装2025-01pnpmpnpm 10.0.0 で自己切り替えがデフォルトに2025-10Node.jsNode.js 25.0.0 が Corepack の同梱を終了2026-04pnpmpnpm 11: devEngines.packageManager と pmOnFail、packageManager は legacy 扱い2026-08pnpmpnpm 12: ピンがロックファイルに環境ドキュメントを書き込む
ピンは Corepack のフィールドとして始まりました。切り替えは pnpm が引き継ぎ、devEngines がツールをまたいで使える新しい置き場所になっています。

Corepack は 2021 年 9 月に Node.js 16.9.0 に同梱され、packageManager フィールドも一緒に登場しました。値は pnpm@x.y.z の完全一致で、後ろにハッシュを付けることもできます。Corepack の pnpm シムがこのフィールドを読み、一致するリリースを取得します。

pnpm 9.7.0 は 2024 年 8 月に manage-package-manager-versions を追加し、Corepack なしでも同じ切り替えができるようにしました。pnpm 10.0.0 は 2025 年 1 月にこれをデフォルトで有効にしました。

devEngines は、OpenJS Foundation のパッケージメタデータ相互運用に関する議論から生まれました。npm は 2024 年 10 月に 10.9.0 で実装し、pnpm 11.0.0 は 2026 年 4 月に devEngines.packageManager を追加しました。バージョン範囲と、フィールドごとの onFail ポリシーに対応しています。

pnpm のドキュメントは現在 packageManager を従来のフィールドと呼んでいます。pnpm 11.23.0 以降の pnpm init は両方を書き出します。devEngines.packageManager と、Corepack 向けに残した完全一致の packageManager です。

どのツールがどのピンのフィールドを読むか行が読み手、列が packageManager フィールド、固定バージョンの devEngines、範囲指定の devEngines の表。Corepack は packageManager を読み、なければ固定の devEngines を使い、範囲指定では失敗する。pnpm と pnpm/action-setup は 3 つとも読み、devEngines が優先される。pnpm は pmOnFail または devEngines 自身の onFail が ignore でなければ、ピンのバージョンに切り替える。Dependabot はどれからでもピンの pnpm で実行するが、更新はしない。GitHub の依存関係グラフの行は全列にまたがり、ロックファイルの 1 つ目のドキュメントしか読まないため、環境ドキュメントを書くピンはすべての依存を隠してしまう。packageManagerdevEngines(固定)devEngines(範囲)Corepack読んで、そのバージョンを取得packageManager がないときに使う失敗: expected asemver versionpnpmpmOnFail: ignoreでなければ切り替えonFail: ignoreでなければ切り替え範囲を解決してロックファイルに記録pnpm/action-setupversion 入力がなければインストールpackageManagerより優先範囲をそのままpnpm self-update へDependabotその pnpm で実行、更新はしないその pnpm で実行、更新はしない最新の一致で実行、更新はしないGitHub の依存関係グラフロックファイルの 1 つ目のドキュメントしか読まないため、環境ドキュメントを書くピンはすべての依存を隠してしまう
ピンの読み方はツールごとに少しずつ違います。onFail か pmOnFail を ignore にしない限り、どのフィールドで固定しても環境ドキュメントが書き込まれ、GitHub の依存関係グラフには依存が見えなくなります。

このマシンで確かめた結果です。

  • 素の pnpm 11.3.0 バイナリは、pmOnFail: ignore で止めるまで packageManager のバージョンに自分で切り替わりました。
  • Corepack 0.34.6 は、完全一致の devEngines.packageManager しか持たない package.json から pnpm 12.8.2 を実行しました。
  • 同じ Corepack は範囲指定を拒否しました。Invalid package manager specification in package.json (pnpm@^12.8.0); expected a semver version です。

残りはソースコードから確認しました。pnpm/action-setup v6.1.0 は、version 入力、devEngines.packageManager、packageManager の順に見ます。Dependabot は実行する pnpm を決めるために両方のフィールドを読み、packageManager を優先し、どちらも更新しません(#4830、#13722)。

ランタイム、パッケージマネージャー、依存パッケージ:3 つの層と 3 つのピン

ランタイム、パッケージマネージャー、依存パッケージは 3 つの層で、それぞれ別のものが固定しています。ロックファイルを読むのは pnpm で、pnpm は Node.js の上で動きます。ロックファイルが解析される時点では、この 2 つはもう決まっています。

3 つの層とそれぞれを固定するもの3 つの層が積み重なっている。依存パッケージは pnpm-lock.yaml の依存関係の記述で固定され、pnpm がそれを読む。パッケージマネージャーの pnpm は packageManager または devEngines.packageManager で固定され、pnpm 12 からは同じ pnpm-lock.yaml の環境ドキュメントでも固定される。この 2 つが同じファイルであることを括弧で示している。pnpm はランタイムの Node.js の上で動き、Node.js は CI では actions/setup-node、ローカルでは mise、asdf、nvm で固定され、engines は範囲を確認するだけ。固定するもの依存パッケージastro、sharp などpnpm-lock.yaml の依存関係の記述パッケージマネージャーpnpm 12.10.1packageManager または devEngines.packageManagerpnpm-lock.yaml の環境ドキュメントpnpm 12 で追加ランタイムNode.js 24CI では actions/setup-nodeローカルでは mise、asdf、nvmengines は範囲を確認するだけpnpm がロックファイルを読むpnpm は Node.js の上で動く同じ pnpm-lock.yaml
ロックファイルは自分を解析するプログラムを選べません。バージョンのピンが package.json にあるのはそのためです。

このブログがローカルマシンで固定していないのは、ランタイムの層です。"engines": { "node": ">=24" } は確認するだけで、>=24 は Node.js 26 も通します。ローカルの pnpm build も、Node.js 26 で動いていることを警告していました。

00:30:29 [WARN] [@astrojs/vercel]
The local Node.js version (26) is not supported by Vercel Serverless Functions.
Your project will use Node.js 24 as the runtime instead.
Consider switching your local version to 24.

CI では actions/setup-node で Node.js を固定しています。ローカルマシンで同じバージョンにそろえるには、mise、asdf、nvm のようにバージョンファイルを読むツールが必要です。そもそも Corepack は、Node.js のバージョンを管理するものではありません。

pnpm 12 の環境ドキュメントは、ロックファイルが 1 つ上の層に手を伸ばして pnpm 自身を固定したものです。GitHub の依存関係グラフは、まだそこまで追いついていません。

Corepack を外す

pnpm を使うなら、Corepack はもう不要です。

  1. pnpm は 10.0.0 からデフォルトでピンのバージョンに自分で切り替えます。
  2. Corepack は devEngines が想定しているバージョン範囲を受け付けません。
  3. Node.js は、2025 年 3 月の TSC の投票を経て、2025-10-15 リリースの 25.0.0 で Corepack の同梱をやめました。

Corepack 自体のメンテナンスは続いています。npm で公開されていて、2026-08-28 時点で 0.36.0 です。メンテナーは、少なくとも Node.js 24 のサポート終了までは保守すると表明しています。Node.js 25 以降では、別途インストールする必要があります。

このマシンでは、pnpm コマンドは実は最初から Corepack のシムでした。シムとは同じ pnpm という名前の小さなプログラムで、実行するとプロジェクトのピンを読み、対応するバージョンの pnpm をダウンロードしてから、そのバージョンにコマンドを渡します。次の ls の結果から、pnpm が Corepack 内のファイルを指していることがわかります。

ターミナルウィンドウ
入力
ls -la $(which pnpm)
出力
lrwxr-xr-x 1 root wheel 41 Mar 3 2026 /usr/local/bin/pnpm -> ../lib/node_modules/corepack/dist/pnpm.js

ブログのディレクトリでは pnpm --version が 12.10.1 を、ホームディレクトリでは 11.3.0 を返していたのは、このシムのせいです。macOS では、次の 3 ステップで置き換えました。

  1. ピンのバージョンで pnpm そのものをインストールします。スタンドアロンのスクリプトは PNPM_VERSION を読み、pnpm 12 のネイティブバイナリ(40 MB の arm64 実行ファイル)を ~/Library/pnpm に置きます。

    ターミナルウィンドウ
    curl -fsSL https://get.pnpm.io/install.sh | PNPM_VERSION=12.10.1 sh -

    スクリプトは最後に、自分では置き換えられないシムを名指しする警告を出して終わります。

    Warning: "pnpm" on PATH is /usr/local/bin/pnpm (a Corepack shim), which comes before ~/Library/pnpm/bin.
    Your shell keeps running that pnpm, not the one pnpm installed to ~/Library/pnpm/bin.
    To finish switching, run "corepack disable pnpm" or move ~/Library/pnpm/bin ahead of /usr/local/bin in PATH.
  2. シムを削除します。/usr/local にグローバルインストールした Corepack は、シムを root の所有にします。そのため corepack disable pnpm には sudo が必要です。

    ターミナルウィンドウ
    入力
    corepack disable pnpm
    出力
    Internal Error: EACCES: permission denied, unlink '/usr/local/bin/pnpm'

    sudo corepack disable pnpm で pnpm と pnpx が消え、Corepack の yarn のシムはそのまま残りました。

  3. シェルがどの pnpm を見つけるか確認します。

    ターミナルウィンドウ
    入力
    which pnpm && pnpm --version
    出力
    ~/Library/pnpm/bin/pnpm
    12.10.1

ステップ 3 で表示された 12.10.1 がこのリポジトリのピンと一致しているのは、インストール時に PNPM_VERSION=12.10.1 を指定したからにすぎません。pmOnFail: ignore の下では pnpm は自分で切り替えないので、あとでピンを新しいバージョンに変えても、マシンの pnpm は何の表示もなく 12.10.1 のままです。

pnpm self-update <version> を使えば、この 2 つを一致させたまま保てます。プロジェクト内で実行すると packageManager を書き換え、インストール済みの pnpm も同じバージョンに更新します。pmOnFail: ignore を設定して pnpm@12.10.1 に固定したプロジェクトで 12.10.0 に変えたときの出力は次のとおりで、ロックファイルは 1 つのドキュメントのままでした。

ターミナルウィンドウ
入力
pnpm self-update 12.10.0
出力
The current project has been updated to use pnpm v12.10.0
Successfully updated pnpm to v12.10.0

ピンを devEngines に移す

Corepack を外したので、Corepack のフィールドである packageManager を残す理由はなくなりました。pnpm 12 が推奨しているのは devEngines.packageManager なので、このブログは最終的にピンをそちらへ移しました。

package.json
"packageManager": "pnpm@12.10.1",
"devEngines": {
"packageManager": {
"name": "pnpm",
"version": "12.10.1",
"onFail": "ignore"
}
},

devEngines.packageManager には pmOnFail と同じ働きをする独自の onFail があるので、pnpm-workspace.yaml の pmOnFail: ignore も同時に外しました。依存を 1 つだけ持つ作業用プロジェクトでの結果は次のとおりです。

ピンpnpm-lock.yaml の --- 区切りの数
devEngines で 12.10.1 を完全一致で指定2
さらに pnpm-workspace.yaml に pmOnFail: ignore0
代わりに devEngines 内の "onFail": "ignore"0

pnpm self-update <version> も devEngines.packageManager.version を同じように書き換え、ロックファイルは 1 つのドキュメントのままです。このブログのロックファイルは、切り替えの前後でまったく変わりませんでした。

pnpm 10 から上がるときの落とし穴

ここからは 2 つ目のリポジトリでの話です。Docker ビルドと GitHub Actions を使うモノレポで、同じ週に pnpm 10 から pnpm 12 へ直接移行しました。pnpm 11 の変更がすべて一度に降りかかりました。

落とし穴 1:package.json と .npmrc の設定が黙って無視される

pnpm 11 以降は package.json の "pnpm" フィールドを読まず、.npmrc からは認証とレジストリの設定しか読みません。package.json に overrides ブロックがあっても、出るのは警告だけです。

[WARN] The "pnpm" field in package.json is no longer read by pnpm. The following keys were ignored: "pnpm.overrides". See https://pnpm.io/settings for the new home of each setting.

.npmrc の shamefully-hoist=true には何のメッセージも出ませんでした。pnpm 12.10.1 で試しにインストールしたところ、node_modules/.modules.yaml には "publicHoistPattern": [] が記録されていました。ホイスティングに頼っていたパッケージは、実行時に暗黙の import を失います。どちらも camelCase にして pnpm-workspace.yaml に移します。

pnpm-workspace.yaml
overrides:
'@faker-js/faker': ^8.4.1
shamefullyHoist: true

これらの設定が読まれていなくてもインストールは成功と表示されるので、失敗を待つのではなく警告を探してください。

落とし穴 2:pnpm 10 がスキップしていたビルドスクリプトでインストールが失敗する

allowBuilds が onlyBuiltDependencies に代わり、strictDepBuilds のデフォルトが true になりました。一覧にないビルドスクリプトを持つ依存があるとインストールが失敗します。pnpm 10 では、何も言わずにスクリプトを飛ばしていました。

Error: ERR_PNPM_IGNORED_BUILDS
× installing dependencies
╰─▶ Ignored build scripts: @firebase/util@1.14.0, classic-level@1.4.1, core-
js@3.48.0, protobufjs@7.5.4, typeorm-fixtures-cli@3.1.0
help: Run "pnpm approve-builds" to pick which dependencies should be allowed
to run scripts.

それぞれを false として並べると、以前の許可リストにある依存のスクリプトだけを実行していた pnpm 10 の動作をそのまま保てます。

pnpm-workspace.yaml
allowBuilds:
grpc-tools: true
sharp: true
'@firebase/util': false
classic-level: false
core-js: false
protobufjs: false
typeorm-fixtures-cli: false

落とし穴 3:新しく足したピンで --frozen-lockfile が壊れる

pnpm 12 のプロジェクトに packageManager を足すと、ロックファイルに含めるべき内容が変わります。それぞれロックファイルを持つ 2 つのサブプロジェクトにピンだけを足したところ、CI が実行する frozen インストールが拒否されました。

× resolve package manager dependencies
╰─▶ Cannot update packageManagerDependencies with "frozen-lockfile" because
the lockfile is not up to date

CI の --frozen-lockfile インストールを通す方法は 2 つあります。

  1. 各プロジェクトでフラグなしの pnpm install を 1 回実行する。pnpm が環境ドキュメントをロックファイルに書き込むので、更新されたロックファイルをコミットします。
  2. pmOnFail: ignore、または devEngines.packageManager の "onFail": "ignore" を設定する。pnpm が環境ドキュメントを書き込まなくなるので、このエラーはそもそも起きません。このブログは最終的に onFail のほうを選びました。

その代わり、このチェックもなくなります。ロックファイルは pnpm のバージョンを記録しなくなるので、後からピンを上げても pnpm-lock.yaml は変わらず、--frozen-lockfile には比べる対象がありません。pnpm self-update は以前、ignore でも環境ドキュメントを書き戻していました(pnpm/pnpm#12228、修正済み)。ピンを変えたら、そのたびに区切りの数を数え直してください。

ターミナルウィンドウ
入力
grep -c '^---$' pnpm-lock.yaml
出力
0

落とし穴 4:pnpm/action-setup@v4 は pnpm 12 をインストールできない

pnpm/action-setup は v6.0.0 で pnpm 11 に、v6.1.0 で pnpm 12 に対応しました。@v6 に上げて version: 入力を消せば、アクションは package.json からピンを読みます。両方を残して食い違わせると、Multiple versions of pnpm specified で失敗します。

Docker イメージでは逆の扱いが必要で、pnpm@12 ではなく npm install -g pnpm@12.8.2 のように完全一致で指定します。このコマンドは npm 経由で動き、npm は pnpm の minimumReleaseAge を読みません。メジャーバージョンだけの指定では、数分前に公開されたリリースが入ってしまいます。

落とし穴 5:Vercel のビルドイメージは pnpm 10 までしかない

Vercel のパッケージマネージャーのドキュメントは pnpm 6 から 10 までを挙げていて、バージョンは lockfileVersion から選びます。packageManager を読むのは ENABLE_EXPERIMENTAL_COREPACK=1 で Corepack を有効にしたときだけです。pnpm 11 と 12 への対応を求める vercel/vercel#17434 はまだ開いています。

このブログがこの問題を避けられているのは、vercel build を GitHub Actions の中で pnpm/action-setup の後に実行しているからです。CLI は PATH 上にすでにある pnpm でインストールします。pnpm 12 で初めて本番デプロイしたときのログには、ロックファイルから pnpm@10.x と判定した行と、実際には pnpm v12.10.1 でインストールした行が両方とも出ていました。

Detected `pnpm-lock.yaml` version 9 generated by pnpm@10.x with package.json#packageManager pnpm@12.10.1
Installing dependencies...
? Verifying lockfile against supply-chain policies (616 entries)...
✓ Lockfile passes supply-chain policies (616 entries in 2.3s)
Lockfile is up to date, resolution step is skipped
Done in 2.6s using pnpm v12.10.1

Vercel 自身の Git 連携でビルドするプロジェクトでは、イメージに入っている pnpm が使われます。

まとめ

  • pnpm 12 は pnpm 11 の設定とロックファイル形式を引き継いでいるので、すでに pnpm 11 のリポジトリは package.json のピンを変え、pnpm-workspace.yaml に設定を 1 つ足すだけでアップグレードできます。
  • pnpm 12 のピンは pnpm-lock.yaml の依存関係の記述より前に環境ドキュメントを書き込み、GitHub の依存関係グラフはその最初のドキュメントしか読みません。devEngines.packageManager でも同じことが起きます。
  • pmOnFail: ignore でロックファイルは 1 つのドキュメントに保たれます。download、error、warn はバージョンが一致していても 2 つ書き込みます。後から足しても既存の環境ドキュメントは消えないので、以前のロックファイルを戻してインストールし直します。
  • この設定は pnpm の自己切り替えを止めます。それはもともと Corepack の仕事でした。packageManager は Corepack のフィールドとして始まり、devEngines.packageManager は pnpm が現在推奨する、ツールをまたいで使えるフィールドです。
  • pnpm を使うなら Corepack は不要で、Node.js 25 以降には同梱されていません。Corepack がなく pmOnFail: ignore の場合、ピンが強制されるのは CI だけです。インストール済みの pnpm とピンは pnpm self-update <version> で一致させておけます。
  • このブログは最終的に devEngines.packageManager でピンを固定し、pmOnFail の代わりにその onFail: "ignore" を使っています。
  • pnpm 10 から上がる場合は、package.json と .npmrc の設定を pnpm-workspace.yaml に移し、ビルドスクリプトをすべて allowBuilds に並べ、ピンを足したらロックファイルを作り直し(または pmOnFail: ignore を使い)、pnpm/action-setup を v6.1.0 に上げます。

参考リンク

この記事をシェア