monorepo でも pnpm の利用を強制させる
パッケージマネジャーとして pnpm の利用を強制させる(i.e. npm
や yarn
を叩けないようにする)方法はいくつか知られていますが、既存の方法では monorepo(pnpm workspace)で管理するパッケージ内での強制は簡単ではありませんでした。本稿では、解決策として、Mise を用いてディレクトリ単位でエイリアスを張り、npm
や yarn
を叩くと「pnpm を利用してね」とだけ表示させる仕組みを紹介します。
背景
近頃 npm パッケージのサプライチェーン攻撃が猛威を振るっており、対策の一つとして pnpm が注目されつつあります。pnpm が注目されている理由としては、公開から時間が経ったパッケージのみのインストールを強制できたり、依存パッケージの lifecycle scripts をデフォルトでは実行しないところにあるようです。
さて、npm パッケージのパッケージマネジャーとして pnpm を利用する場合、npm や yarn といった他のパッケージマネジャーの利用は禁止したいところです。その方法として、不完全なリストですが以下が知られています:
- 個人単位で実施する方法
- パッケージ単位で実施する方法
これらの方法は概ね有効に働きますが、問題点もいくつかあります。まず、方法 1 は共同開発者に pnpm の利用を強制できず、方法 2 は only-allow
が侵害されるリスクがあります。方法 3 は、monorepo 構成(今回は pnpm の workspace のみ考えます)の場合、各パッケージのディレクトリ配下における npm/yarn の利用を禁止するためには、各パッケージに方法 3 を適用する必要があります。つまり、ルートの package.json
の .engines
のみ設定すればよい訳ではなく、各パッケージの package.json
の .engines
も同様に設定する必要があります。そのため、パッケージが増えると単純に面倒ですし、新しく作るパッケージで上記の設定を書き損じる可能性もあります[1]。
解決策
Mise というバージョン管理ツールを利用すると、コマンドや環境変数をディレクトリ単位[2]で設定できます。これと同じノリで、ディレクトリ単位でエイリアスを貼れば、先述した方法 1 を monorepo 全体に適用可能だと考えました。
という訳で、monorepo のルートで一度だけ設定すればよい、Mise だけを利用して npm
と yarn
のエイリアスを張る仕組みを考えました。開発者への負担は Mise の使用だけです。開発者はこれから説明する仕組みを施した monorepo のディレクトリ配下に移動するだけで、npm
や yarn
を叩くと「pnpm を利用してね」が表示されるようになります。
仕組み
ディレクトリ単位でエイリアスを張る方法は、「みんなが本当に欲しかったのは Makefile じゃなくてディレクトリレベルで管理できるエイリアスなのでは - Lambda カクテル」で提案されています。超絶ざっくり要約すると次の通りです。direnv
というコマンドを利用するとディレクトリ単位で環境変数を設定できます。これを利用して $PATH
を拡張し、$PATH
に追記したディレクトリ(e.g. <project-root>/.aliases
)にシェルスクリプトを置くと、ディレクトリ単位でコマンドを定義できます。これをエイリアスと呼んでいます。以下では l
というエイリアスの定義例を示しています(先行事例ではもっと賢く定義されています):
#!/bin/bash
ls -lah
さて、先行事例では direnv を利用して $PATH
に追記していましたが、今回は Mise で同様の振る舞いを実現します[3]。例えば mise.toml
へ以下のように記述すると、mise.toml
が存在するディレクトリ直下のディレクトリ .aliases
を $PATH
に追記できます。なお、ディレクトリは $PATH
の先頭に追記されるようです。
[env]
'_'.path = "{{config_root}}/.aliases"
あとは、パスを通したディレクトリに npm
や yarn
といったシェルスクリプトを置くだけです。スクリプトの内容は echo "pnpm を利用してね"
です[4]。
ここまでの説明をまとめると、mise.toml
やエイリアス npm
/yarn
のディレクトリ構成は以下の通りとなります:
.
├── .aliases
│ ├── npm
│ └── yarn
└── mise.toml
実例は本稿を管理している GitHub リポジトリにあります:
まとめ
本稿では、monorepo(pnpm workspace)でも pnpm の利用を強制させるため、Mise を用いて monorepo 全体で npm
/yarn
の利用を禁止する仕組みを紹介しました。
Mise はいいぞ。
-
Discussion で議論していますが、
package.json
の.devEngines.packageManager
を使う方法もまた、方法 3 と同様の問題点があります。 ↩︎ -
Mise で設定した環境変数はサブディレクトリでも有効です。これは後述する direnv でも同様です。 ↩︎
-
今回 direnv ではなく Mise を使っているのは、ぶっちゃけ好みです。今回紹介する手法は direnv でも実現できます。ただし、コマンドのバージョン管理に Mise を使っている場合は、環境変数の管理にも Mise を使うのが素直かなとは思います。 ↩︎
-
pnpm "$@"
と書いて pnpm として生きてもらうのもよいと思います。 ↩︎
Discussion
"packageManager": "pnpm@latest"
を package.jsonに追加する、でいいのでは?
ご指摘ありがとうございます!
ご指摘いただいた、
package.json
の.devEngines.packageManager
の設定により pnpm の利用を強制する方法は、本稿で紹介している「engines
を使う方法」と同様に、workspace に含まれる全てのpackage.json
の.devEngines.packageManager
を設定する必要があります。つまり、もしあるパッケージで上記の設定がされていない場合、そのパッケージのディレクトリ配下ではnpm install
を叩いてもエラーになりません。全部設定すればよいのではとも思いますが、本稿のモチベーションは「全ての
package.json
で同じ設定をするのは面倒くさいし間違えそう」といったところから始まっており、本稿の主張は Mise を活用すると一回設定するだけでよいので嬉しいというところになります。上のコメントにもあるように、
"packageManager": "pnpm@10.17.0"
のようにpackageManagerを使えばいいと思います(latestは危険かな)。またpnpmは自動的にpackageManager filedを見て適切なバージョンをダウンロードして使ってくれます。さらに、pnpm自体でnodeのversionも管理できます。
また、only-allowの侵害について本文中で触れられていますが、only-allowの作者はpnpmの作者 なので、only-allowが侵害されることとpnpm自体が侵害されることは同義です。なのでそのリスクは考えても仕方がないと思います。
個人的には、pnpmをglobalでinstallさせて、nodeおよびpnpm自身のバージョンはpnpmに寄せるのが最適解かなと考えています。
ご指摘ありがとうございます!
packageManager
については、https://zenn.dev/link/comments/6205d5a69616c6 でコメントいたしました。only-allow の侵害と pnpm の侵害は同義という点について、それぞれリポジトリが異なるのでどちらか一方のみ侵害される可能性はあるかと思いますが、考えすぎなのはそうかもです。過度に恐れて利用を避けなくてもよいかもですね。
バージョンをどこに寄せるかについては、個人的には
node
やpnpm
以外のコマンド(e.g.terraform
)をよく Mise で管理しているので、全て Mise で完結できると嬉しいなと考えています。この辺りは宗派だと思っているので、ご指摘の通り pnpm に寄せる方針もあると思います。