vlt と、遊んでみた
vlt(ヴォルト、と読むそうです)という、新しい JavaScript のパッケージマネージャが出ました。npm の作者 Isaac Schlueter が、元 npm チームの人たちともう一度集まって作っている会社の道具で、いまは 1.0.0-rc.32。正式な 1.0 の、すこし手前です。
この記事は、それを pnpm と並べて、砂場でいじりくりまわした記録です。ベンチマークは同じ機械・同じ回線で各一回だけなので、厳密な計測というより、手触りの報告として読んでください。
最初の驚き — install しても、ビルドが走らない
まっさらなプロジェクトに express と vitest を入れてみると、五秒ほどで終わって、こう言われました。
"message": "2 packages that will need to be built,
run \"vlt build\" to complete the install."esbuild のようなネイティブビルドが要る子は、install では展開されるだけで、コードは一行も走りません。走らせたいときに vlt build を打つ。install と build が、最初から別のフェーズに割られています。
これ、pnpm も最近は同じ方向です(スクリプトはデフォルトで止めて、pnpm approve-builds で個別に許可する形)。supply chain 攻撃の多くが postinstall スクリプトから入ってくる時代なので、「入れること」と「実行を許すこと」を分けるのは、もう新しい発想ではなく共通の作法になりつつあるみたいです。vlt はそれをコマンドの形にまで持ち上げた、という感じ。
node_modules の形は、ほとんど pnpm
中を覗くと、トップレベルには直接依存だけが居て、実体はぜんぶシンボリックリンクでした。
node_modules/express
-> .vlt/~npm~express@4.22.2/node_modules/expresspnpm の .pnpm/ ディレクトリと同じ思想です。宣言していない依存が require できてしまう幽霊依存も、同じ仕組みで防がれます。macOS では実体ファイルも reflink(コピーオンライト)で中央キャッシュと共有されるので、ディスクの使い方も pnpm と同等でした。
ひとつだけ、名前に個性があります。~npm~express@4.22.2 — レジストリの名前が、パッケージの住所に最初から入っている。npm レジストリ以外(JSR や、自社のサーバーレスレジストリ vsr)を並べて使う未来を、ディレクトリ名の段階から想定している設計です。
いちばん楽しかったところ — 依存グラフに、CSS で問いかける
vlt の顔は、たぶん速度ではなくて vlt query です。依存グラフを、CSS のセレクタで引けます。
vlt query '#send' # send は、どこから来た子?
vlt query ':root > :dev' # 直接の dev 依存だけ
vlt query ':not([license=MIT]):not(:root)' # MIT 以外のライセンスを列挙
vlt query ':outdated(major)' # major が古いもの
vlt query ':severity(high)' # 既知の脆弱性(Socket.dev のデータ)いちばん気に入ったのは、この子です。
vlt query ':attr(scripts, [postinstall])'「うちの機械でコードを走らせたがっているのは、だれ?」が、一行で引けます。うちの砂場では vite → esbuild だけが手を挙げました。
pnpm だと pnpm why と pnpm audit と pnpm licenses に分かれている問いが、ここでは一つの言語です。しかも同じセレクタが、スクリプト実行の対象指定(vlt run --scope=':workspace' test — pnpm の --filter 相当)にも、mermaid の図の出力(--view=mermaid)にも、そのまま使えます。「依存グラフは、クエリできるデータであるべき」という主張が、道具ぜんたいを一本、貫いています。
pnpm のプロジェクトでも、query は動く
ここで欲が出ました。query だけ、いまの pnpm のプロジェクトに借りられないかな。
動きました。vlt のロックファイルが無くても、pnpm が作った node_modules を読んで、そのままグラフにしてくれます。乗り換えなくても、監査の道具としてだけ持ち込める。
ひとつだけ白状すると、「読み取り専用」のつもりで走らせた query が、node_modules/.vlt-lock.json というキャッシュを黙って書いていました。git の追跡外なので実害はないのですが、よそのお家に上がるときは、それを知っておくとよいです。
ワークスペースと、黙って空振りした話
モノレポは vlt.json に書きます。workspace:* プロトコルもちゃんと通って、相対シンボリックリンクでつながりました。
ただ、ここで一回まちがえました。設定の鍵を "workspace"(単数)と書いたら、エラーも警告もなく、空の依存グラフのまま install が成功してしまった。正しくは "workspaces"。複数形に直したら全部つながったのですが、まちがった鍵に何も言ってくれないのは、RC らしい若さだと思います。知らない鍵を見たら教えてくれる日を、待っています。
速さの話 — 正直に
React + Vite + ESLint という、よくある構成(186 パッケージ)で測りました。cold はキャッシュもロックファイルも無い状態、warm は node_modules だけ消して入れ直した状態です。
| cold | warm | |
|---|---|---|
| vlt | 3.8s | 1.07s |
| pnpm | 4.1s | 0.53s |
| npm | 8.7s | 0.95s |
公式の「npm より 73% 速い」は、cold ならだいたい本当でした。そして cold では pnpm とすでに互角です。ただ warm — 日々の開発でいちばん多く踏む道 — では、pnpm がまだきれいに倍速い。CPU の使い方を見ると pnpm は並列に張りついていて、vlt にはまだ絞る余地が残っている感じでした。
(express だけの小さい構成では cold も pnpm が勝ったので、規模や回線で入れ替わる程度の差、と読むのが正直なところだと思います。)
RC らしい粗さも、書いておく
vlt -aの出力が、JSON エスケープされた文字列のまま画面に出てくる(\nが生で見える)vlx cowsay(npx 相当)をパイプ越しに打つと、確認プロンプトが見えないまま無言で固まる(--yesを付けると通る)- install のたびに
.gitignoreと.npmignoreを勝手に作ってくれる(親切と、お節介の、ちょうど中間)
どれも致命傷ではなくて、直る種類のものです。肝心の中身のほうは堅くて、vitest も esbuild も、symlink の上でふつうに動きました。
すわりなおして、結論
きょう pnpm から乗り換える理由は、見つかりませんでした。warm の速さも、道具としての枯れ方も、まだ pnpm が上です。
でも vlt は「pnpm をもう一回作った」道具ではないみたいです。node_modules の形はほとんど同じで、そこはもう答えが出ている。違うのはその上で、依存グラフを一つのクエリ言語で見通せるようにしたことと、レジストリを複数形で考えていること。乗り換え先というより、隣に置ける道具が先に来た、という印象です。
pnpm のプロジェクトのまま、ライセンスの棚卸しや「postinstall を持ってる子探し」に vlt query だけ借りる — それは、きょうからでも成立します。1.0 が出て、warm が pnpm に並んだら、もう一度すわりなおして考えます。
つづきは、また、そのときに。