⛓️

monorepo でも pnpm の利用を強制させる

に公開
4

パッケージマネジャーとして pnpm の利用を強制させる(i.e. npmyarn を叩けないようにする)方法はいくつか知られていますが、既存の方法では monorepo(pnpm workspace)で管理するパッケージ内での強制は簡単ではありませんでした。本稿では、解決策として、Mise を用いてディレクトリ単位でエイリアスを張り、npmyarn を叩くと「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 だけを利用して npmyarn のエイリアスを張る仕組みを考えました。開発者への負担は Mise の使用だけです。開発者はこれから説明する仕組みを施した monorepo のディレクトリ配下に移動するだけで、npmyarn を叩くと「pnpm を利用してね」が表示されるようになります。

仕組み

ディレクトリ単位でエイリアスを張る方法は、「みんなが本当に欲しかったのは Makefile じゃなくてディレクトリレベルで管理できるエイリアスなのでは - Lambda カクテル」で提案されています。超絶ざっくり要約すると次の通りです。direnv というコマンドを利用するとディレクトリ単位で環境変数を設定できます。これを利用して $PATH を拡張し、$PATH に追記したディレクトリ(e.g. <project-root>/.aliases)にシェルスクリプトを置くと、ディレクトリ単位でコマンドを定義できます。これをエイリアスと呼んでいます。以下では l というエイリアスの定義例を示しています(先行事例ではもっと賢く定義されています):

<project-root>/.aliases/l
#!/bin/bash

ls -lah

さて、先行事例では direnv を利用して $PATH に追記していましたが、今回は Mise で同様の振る舞いを実現します[3]。例えば mise.toml へ以下のように記述すると、mise.toml が存在するディレクトリ直下のディレクトリ .aliases$PATH に追記できます。なお、ディレクトリは $PATH の先頭に追記されるようです。

mise.toml
[env]
'_'.path = "{{config_root}}/.aliases"

あとは、パスを通したディレクトリに npmyarn といったシェルスクリプトを置くだけです。スクリプトの内容は echo "pnpm を利用してね" です[4]

ここまでの説明をまとめると、mise.toml やエイリアス npm/yarn のディレクトリ構成は以下の通りとなります:

.
├── .aliases
│   ├── npm
│   └── yarn
└── mise.toml

実例は本稿を管理している GitHub リポジトリにあります:

https://github.com/ajfAfg/zenn-content/tree/7dcdd84bf6cb8ce8de4adaf4338a14c177fd013f

まとめ

本稿では、monorepo(pnpm workspace)でも pnpm の利用を強制させるため、Mise を用いて monorepo 全体で npm/yarn の利用を禁止する仕組みを紹介しました。

Mise はいいぞ。

脚注
  1. Discussion で議論していますが、package.json.devEngines.packageManager を使う方法もまた、方法 3 と同様の問題点があります。 ↩︎

  2. Mise で設定した環境変数はサブディレクトリでも有効です。これは後述する direnv でも同様です。 ↩︎

  3. 今回 direnv ではなく Mise を使っているのは、ぶっちゃけ好みです。今回紹介する手法は direnv でも実現できます。ただし、コマンドのバージョン管理に Mise を使っている場合は、環境変数の管理にも Mise を使うのが素直かなとは思います。 ↩︎

  4. pnpm "$@" と書いて pnpm として生きてもらうのもよいと思います。 ↩︎

GitHubで編集を提案

Discussion

yujayuja

"packageManager": "pnpm@latest"

を package.jsonに追加する、でいいのでは?
https://docs.npmjs.com/cli/v10/configuring-npm/package-json?v=true#devengines

ajfAfgajfAfg

ご指摘ありがとうございます!

ご指摘いただいた、package.json.devEngines.packageManager の設定により pnpm の利用を強制する方法は、本稿で紹介している「engines を使う方法」と同様に、workspace に含まれる全ての package.json.devEngines.packageManager を設定する必要があります。つまり、もしあるパッケージで上記の設定がされていない場合、そのパッケージのディレクトリ配下では npm install を叩いてもエラーになりません。

全部設定すればよいのではとも思いますが、本稿のモチベーションは「全ての package.json で同じ設定をするのは面倒くさいし間違えそう」といったところから始まっており、本稿の主張は Mise を活用すると一回設定するだけでよいので嬉しいというところになります。

ryoppippiryoppippi

上のコメントにもあるように、"packageManager": "pnpm@10.17.0"のようにpackageManagerを使えばいいと思います(latestは危険かな)。またpnpmは自動的にpackageManager filedを見て適切なバージョンをダウンロードして使ってくれます。
https://pnpm.io/settings#managepackagemanagerversions

さらに、pnpm自体でnodeのversionも管理できます。
https://pnpm.io/settings#usenodeversion
https://pnpm.io/package_json#devenginesruntime

また、only-allowの侵害について本文中で触れられていますが、only-allowの作者はpnpmの作者 なので、only-allowが侵害されることとpnpm自体が侵害されることは同義です。なのでそのリスクは考えても仕方がないと思います。

個人的には、pnpmをglobalでinstallさせて、nodeおよびpnpm自身のバージョンはpnpmに寄せるのが最適解かなと考えています。

ajfAfgajfAfg

ご指摘ありがとうございます!

packageManager については、https://zenn.dev/link/comments/6205d5a69616c6 でコメントいたしました。

only-allow の侵害と pnpm の侵害は同義という点について、それぞれリポジトリが異なるのでどちらか一方のみ侵害される可能性はあるかと思いますが、考えすぎなのはそうかもです。過度に恐れて利用を避けなくてもよいかもですね。

バージョンをどこに寄せるかについては、個人的には nodepnpm 以外のコマンド(e.g. terraform)をよく Mise で管理しているので、全て Mise で完結できると嬉しいなと考えています。この辺りは宗派だと思っているので、ご指摘の通り pnpm に寄せる方針もあると思います。