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/express

pnpm の .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 whypnpm auditpnpm 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 だけ消して入れ直した状態です。

coldwarm
vlt3.8s1.07s
pnpm4.1s0.53s
npm8.7s0.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 に並んだら、もう一度すわりなおして考えます。

つづきは、また、そのときに。