3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

10年間 yarn v1 だった Qiita のモノレポを pnpm に移行した話

3
Last updated at Posted at 2026-08-28

はじめに

Qiita のフロントエンドは、つい最近まで yarn v1 で依存関係を管理していました。2026年の春から夏にかけて、これを pnpm に移行しました。

8つのパッケージがそれぞれ独立した lockfile を持ち、workspace も使っていない、という構成からの移行です。yarn v1 の開発が止まってから移行したい気持ちはずっとあったのですが、実際に動き出したきっかけは AI エージェントによる開発の並列化でした。

この記事では、Qiita に yarn が導入された10年前の話から、pnpm への移行を終えるまでの経緯と、途中で踏んだ想定外をまとめます。

約10年前、yarn 公開から10日後

今から約10年前、2016年10月11日に yarn が公開されました。その10日後の 2016年10月21日に、Qiita のリポジトリに「yarn を試した」という Pull Request が作られました。

当時の yarn はまだ v0.16 で、かなり早い段階から検証が始まっていたようです。PR には npm との速度比較も残っていて、当時から yarn のインストールの速さが評価されていたことが分かります。

とはいえ導入はすぐには終わらず、CI や本番デプロイまで面倒を見る必要があり、PR がマージされたのは約2ヶ月越しの 2016年12月15日でした。

そして PR のコメントでは、「負債にしたくないので、できるだけ最新のバージョンを使うのがよさそう」というやり取りがされていました。10年後、その yarn v1 が負債になって移行することになるとは、たぶん誰も思っていなかったはずです。

それから10年、8つのパッケージを持つモノレポに

yarn を導入した当初は、ルートに package.json が1つあるだけの構成でした。そこからフロントエンドのパッケージが少しずつ増えていき、最終的にはルート・各アプリ・内部ライブラリで、合計8つの package.jsonyarn.lock が並ぶ構成になりました。

このとき、yarn workspaces は使っていませんでした。パッケージごとに独立した node_modules を持ち、パッケージをまたぐ処理はルートの npm scripts で自前に組んでいます。「そのディレクトリに cd するだけ」のスクリプトを用意して、それ経由でパッケージごとのコマンドを定義する、という形です。

"_exec-in:app:foo": "cd client/foo &&",
"install:foo": "run-s '_exec-in:app:foo {@}' -- yarn install --prefer-offline",

これがパッケージの数だけ並んでいて、パッケージが増えるたびにルートの package.json に一式を足す必要がありました。

内部ライブラリの受け渡しも同様に自前で、ビルド成果物を専用のディレクトリに出力して、各アプリはそれを file: プロトコルで参照していました。

"dependencies": {
  "@internal/foo": "file:../../lib-dist/foo"
}

ライブラリを修正したら、ビルドして成果物を出力し直し、各アプリで再インストールする、という手順が必要でした。

yarn 1.x は frozen になり、更新が止まった

一方で、yarn 側にも動きがありました。yarn は v2 で大きく設計が変わり(いわゆる Yarn Berry)、v1 系は開発が止まります。yarnpkg/yarn のリポジトリには、いまこう書かれています。

The 1.x line is frozen - features and bugfixes now happen on https://github.com/yarnpkg/berry

最後のリリースは 2024年3月の v1.22.22 で、Qiita が使っていたのもこのバージョンでした。バグ修正もセキュリティ対応も今後は入らないので、使い続ければそのまま負債になります。

そのため移行したい気持ちはありつつ、ほかの負債や優先すべきタスクも多く、しばらく後回しになっていました。

2026年、AI エージェントによる開発の並列化で状況が変わった

状況が変わったのは、AI コーディングエージェントを使った開発が本格化してからです。

複数のタスクを並行して進めるために、git worktree を切って作業する運用が始まりました。worktree ごとに独立した作業ディレクトリができるので、当然そこには node_modules も必要になります。8つのパッケージがそれぞれ独立した node_modules を持つので、worktree の数だけ丸ごと増えていきます。

そして厄介だったのは yarn のキャッシュです。yarn v1 のキャッシュはリポジトリの外にあるので、worktree を消してもキャッシュは残り続けます。並列数を増やすにつれてこれが膨らんでいき、ディスクの空きが減ってきたら yarn cache clean で消す、という運用になっていました。1度に100GB以上のキャッシュを消したこともあります。消せば空きは戻りますが、気づいて手で消す作業が必要で、worktree を増やすほど頻度も上がります。消してもまた溜まるので、運用でしのぐしかない状態でした。

後回しにしていた移行の優先度が、ここで一気に上がりました。

pnpm への移行を決め、1パッケージずつ置き換えていった

移行先は pnpm にしました。パッケージの実体をグローバルなストアに1つだけ置いて、各 node_modules からはリンクで参照するので、worktree をいくつ作ってもディスク上の実体は増えません。今回の一番の動機がここでした。他にも、依存関係の解決が厳密なことや、インストールが速いこと、開発が活発なことも理由です。社内の別のリポジトリではすでに pnpm を使っていたので、知見があったのも理由でした。

移行の方針は、8つのパッケージを1つずつ pnpm に移行し、全部が終わってから workspace 化するというものにしました。

一気に workspace 化してしまうと、パッケージマネージャーの変更と構成の変更が同時に起きて、何が原因で壊れたのか分からなくなります。1つずつであれば、壊れたときの原因はそのパッケージに閉じますし、途中で問題が起きても、先に移行したパッケージは pnpm のまま動き続けます。移行の途中は yarn と pnpm が混在する状態になりますが、そこは許容しました。

依存される側の内部ライブラリを先に、参照する側のアプリを後に、という順序だけ守って進めました。

途中、想定外のことも発生した

段階的に進めたおかげで大きな事故はありませんでしたが、想定外は色々ありました。

暗黙に解決できていた依存が解決できなくなった

yarn v1 は依存パッケージを node_modules の直下に平坦に展開するので、package.json に書いていないパッケージも参照できてしまいます(phantom dependency)。pnpm はこれを許さないので、暗黙に頼っていた箇所がビルドエラーになりました。

node_modules の中を相対パスで直接指していた import も同様で、pnpm ではパスの構造が変わるため解決できなくなります。こうした箇所は、パッケージ名で参照する形に直していきました。

pnpm 10 は依存のビルドスクリプトを実行しない

pnpm 10 から、依存パッケージの postinstall などのビルドスクリプトは既定で実行されなくなりました。セキュリティ上は正しい挙動ですが、移行時は「このパッケージのビルドスクリプトは実行が必要か」を1つずつ判断する必要があります。

結果として、ネイティブモジュールのビルドやバイナリのダウンロードを行うパッケージについて、なぜ許可する / しないのかをコメント付きで設定に残すことになりました。

lockfile が壊れる

pnpm-lock.yaml は、GitHub の Web 上でマージすると、同じキーを2つのブランチが書き換えているのにコンフリクトにならず、そのキーが重複した YAML ができてしまうことがあります。git の3-way マージは行単位で見ます。YAML は行構造が壊れないままキーだけが重複するので、マージがそのまま通ってしまうためです。

pnpm 10 は壊れた lockfile を警告しつつ無視して再解決するので、CI は通ります。そのかわり、ローカルでインストールするたびに lockfile に差分が出る、という分かりにくい壊れ方をします。

.gitattributes で lockfile を merge=binary にして必ずコンフリクトさせる対策を入れてみたのですが、Web 上のマージには効かず、Dependabot の rebase を妨げてしまったので撤去しました。この記事を書いている時点では、まだ解決していません。

Dependabot が pnpm workspace をうまく扱えなかった

workspace 化したあと、Dependabot が package.json だけを更新して pnpm-lock.yaml を更新しない PR を作るようになりました。lockfile をパッケージごとに分ける構成で lockfile が更新されない、という Dependabot 側のバグが原因です。この記事を書いている時点では直っていないので、設定側で回避しています。

yarn と pnpm の混在で lockfile に差分が出る

移行の途中は yarn と pnpm が混在するので、その組み合わせ特有の問題も踏みました。これは別の記事に書いたので、詳細はそちらをどうぞ。

移行開始から約2ヶ月半、workspace 化まで完了

最初のパッケージの PR を出したのが 2026年3月末、workspace 化が完了したのが 6月17日でした。

1パッケージあたりの期間はばらついていて、依存が少ないものは数日、大きいものは1ヶ月近くかかっています。想定外を踏むたびに調査と修正が入るので、事前に見積もった工数はあまり当たりませんでした。

その後、内部ライブラリの file: 参照をやめて workspace の参照に置き換えたり、不要になった npm scripts を整理したりして、一連のタスクを完了としたのは7月末です。移行を決めてから数えると、およそ半年かかりました。

とはいえ、この期間はずっと移行だけをしていたわけではなく、メインで別のプロジェクトを進めながらの片手間です。専任でやればもっと短く終わると思います。

おわりに

10年分の積み重ねを移していく作業だったので、正直もっと大変になると思っていました。実際にやってみて助かったのは、想定外を踏んだときの調査を AI エージェントに任せられたことです。

lockfile に謎の差分が出る、キャッシュが効いた CI だけ通る、といった問題は、原因が pnpm の挙動なのか設定なのか自分たちのコードなのか、切り分けるまでが一番しんどいところです。そこを調べさせて、当たりを付けてから自分で確認する、という進め方ができたので、1パッケージずつ着実に進められました。

パッケージマネージャーの移行は後回しにしやすいタスクですが、後回しにするほど積み上がるタスクでもあります。同じような構成で移行を迷っている方の参考になれば嬉しいです。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?