- バックエンド / リーダー候補
- PdM
- Webエンジニア(シニア)
- 他19件の職種
- 開発
- ビジネス
現在WantedlyではNode.jsのパッケージ管理にyarn v1を使っています。現在私は開発者体験の改善を目指してyarn v2への移行を検討しているのですが、その過程でyarn v2が誤解されがちだと感じるようになりました。そこで社内への情報提供も兼ねて、いくつか誤解されがちだと思われる点を紹介したいと思います。
(わかりやすさのためにyarn v2と呼んでいますが、 yarn v3以降も含みます。これらはメジャーバージョンアップではあるもののyarn v1→v2のようにアーキテクチャが刷新されるわけではないからです)
ポイント1: yarnをv2にするのにPnPは必須ではない
yarn PnPはyarn v2の目玉機能で、パッケージをnode_modules以下に展開せずに仮想化してロードできるようにするというものです。node_modulesの展開作業が不要になるほか、依存関係の管理がより厳密になり間違いにくくなるなどの利点があります。しかしライブラリやツールがPnPに対応しているか心配だという人も多いのではないでしょうか。
実は、yarnのバージョンをv2にするのにPnPは必須ではありません。yarn v2は3段階の異なる運用方法を提供しています。
- node-modulesモード
- PnPモード (Zero-Installs無し)
- Zero-Installs
node-modulesモードでは旧来のパッケージ管理ツールと同様にnode_modulesディレクトリが生成されるので、これによりかなりの互換性が担保されます。まずはnode-modulesモードでyarn v2への移行をしてみる、というのは一つの選択肢としてありえると思います。
なお、Zero-Installsは単なるPnPよりもさらに思い切ったモードで、依存パッケージも一緒にコミットしてしまうことで、git clone後のyarn installを不要にするというものです。私自身はこの考え方に疑問もあるのですが、yarn v2はこのZero-Installsを理想と考えていることを念頭に置いておくと、yarn v2の設計思想が理解しやすいかもしれません。
ポイント2: PnPの対応状況はかなり良くなっている
yarn PnPには主に以下のような互換性の懸念があります。
- ツールの対応。yarn PnPはNode.jsの `require` を独自実装に差し替えることで実現しているので、Node.jsの実装によらずに依存解決をするツールでは別途対応が必要になります。
- ライブラリの対応。node_modulesの場合、package.jsonの依存関係記述に漏れがあっても偶然requireできてしまう場合があります (hoistingの悪影響)。PnPではこの問題が起きないため、今まで偶然動いていたパッケージがかえって動かなくなってしまうことがあります。
ツールの対応状況はyarn v2のドキュメントに記載がありますが、PnPの対応状況を改善するための努力が見てとれます。残念ながらAngular, React Native, Flowを含むいくつかのツールでは対応が難しいようですが、Wantedlyのようにこれらの技術スタックに該当していない組織であればそのまま移行できる可能性が十分にあります。また公式リリースで対応していないツールの代表例としてTypeScriptがありますが、plugin-compatにより自動的に対応パッチが適用されるため何もしなくてもそのまま使えます。
……ただし、VSCodeを使う場合に追加の作業が必要で、開発者体験という観点では大きな問題があります (この記事の後半で説明します)
一方ライブラリの対応状況の実情はわかりませんが、ユーザー側で暫定対応できるように packageExtensions
という設定があるため心配は不要です。特によく使われるライブラリの依存関係が壊れている場合、 @yarnpkg/plugin-compat のデータベースに修正が登録されていたり、上流に修正がサブミットされたりしています。少なくとも、筆者の経験上はまだ困ったことはありません。
ポイント3: yarn v2のインストールは不要
全ての開発者にyarn v2のインストールをお願いしないといけないのか、その場合v1との共存はできないのか…… というのは心配になる点かもしれません。実は yarn v2の(開発環境への)インストールという作業は必要ありません。
鍵となるのは yarn v1 のyarn policiesという機能です。これはプロジェクト内にyarn本体を同梱しておくとそれを使ってくれる機能で、その場合グローバルのyarnは単に別のバージョンのyarnを読み込むだけのブートローダーとして機能します。 (yarn v1はyarn v2を読み込むこともできるようになっています)
yarn v2ではyarn v2本体のグローバルインストールは原則として行わず、 yarn v1 からロードして使うことが推奨されています。 (そもそもグローバルインストールする方法があるのか、筆者は知りません)
yarnとcorepack
このようにyarn v1は廃止されたわけではなく、他のパッケージマネージャを読み込むブートローダーの役割が残されました。このブートローダーとしての役割を切り出してNode.jsへの同梱を目指しているのがcorepackのようです。corepackに関しては日本語の解説記事があるので詳しくはそちらに譲ります。
ポイント4: yarn v2 を使っているプロジェクト
仮にどんなに出来が良くても、ユーザーがいないものを使うのには勇気がいるものです。2021年5月現在、筆者が確認できた範囲では以下のようなプロジェクトがすでにyarn v2を使っているようです。
- yarn v2自身 https://github.com/yarnpkg/berry (pnpモード + Zero-Installs)
- Babel https://github.com/babel/babel (node-modulesモード)
- Jest https://github.com/facebook/jest (node-modulesモード)
- Storybook https://github.com/storybookjs/storybook (node-modulesモード)
yarnを使っているプロジェクトの中でもv1がまだまだ主流のようですが、上の3つだけでも大きな安心につながるのではないかと思います。yarn v2自身以外で、pnpモードで運用している(ある程度大きい)プロジェクトを見つけられなかったのは残念です。
yarn v2の利点
ここまでで主要な誤解について説明できたと思うので、ここからは利点と気をつけるべき点を思いつく範囲で紹介したいと思います。
git依存関係のよりよいサポート
yarn v2ではgitリポジトリを依存関係のソースに指定したとき、そのパッケージのprepackスクリプトを (devDependencies がインストールされた状態で) 実行してくれるようになっています。TypeScriptなどトランスパイルが必要なプロジェクトでは、prepackにトランスパイル処理を記述することでgit経由でも正しく使えるようになります。 (このことはyarn v2のlifecycle scriptの説明で明記されています)
特に社内で使われる共通ライブラリに変更を加えるとき、動作確認しやすくなるのではないかと期待しています。
PnPによるyarn installの効率化
時間のかかる処理(特に人間の作業をブロックする処理)は開発者体験の敵です。 npm install / yarn install は多数のファイルをファイルシステムに展開するという性質上、時間もかかるしストレージ容量も消費します。
yarn PnPはパッケージをnode_modulesに展開せず、かわりにパッケージごとにzipに再圧縮して.yarn/cache内にフラットに並べます。 (※ tgzはアーカイブしてから圧縮、zipは圧縮してからアーカイブなのでzipのほうがランダムアクセス効率が良い) これにより以下の改善が期待できます。
- 圧縮によりディスクに書き出す量が減る。
- アーカイブ化によりディスクに書き出すファイル数が減る。
- フラットに並べるので、node_modulesのように同じパッケージの同じバージョンのコピーが重複することがない。
- フラットに並べるので、node_modulesのようにhoisting条件の変化によって再展開の必要が生じることがない。
PnPによるrequireの予測可能性の向上
node_modulesに展開された依存ツリーは元々の依存グラフを正確に表現しません。そのため、本来依存関係に書かれていないのに偶然requireできるパッケージが存在してしまいます。いつどのようなパッケージが偶然requireできてしまうかはhoistingの条件によるので、予測可能性が低く混乱のもとです。
yarn PnPでは依存関係に書かれていないパッケージをrequireすることはできない (strictモード) ため、このような問題は最初から発生しません。
PnPによるlinkの振舞いの是正
node_modulesを使っている限りにおいてはファイルシステムの構造から完全に自由になることはできません。これによりnpm linkやyarn linkといった「ファイルシステム上の別のプロジェクトに依存できるようにする機能」は期待通りに動かないケースがありました。
yarn PnPでは依存関係はファイルシステムの構造に縛られないので、このような問題に遭遇することなくlinkを使えるようになると期待しています。これもまた社内ライブラリの開発効率向上に貢献するのではないかと思います。
Zero-Installsによるセットアップのさらなる簡略化
yarn PnPはパッケージをnode_modulesに展開せず、かわりにパッケージごとにzipに再圧縮して.yarn/cache内にフラットに並べます。この.yarn/cacheをgitにコミットしてしまうというワークフローをZero-Installsと呼び、yarn v2は通常のPnPとZero-Installsの両方をサポートしています。
Zero-Installsを採用する場合、git cloneしてきたプロジェクトは yarn install
を実行せずにそのまま動かせるようになります。もちろんあらかじめyarn v1をインストールする必要がありますが、corepackがバンドルされるようになればNode.jsだけで済むことになります。
(なお筆者はこの考え方にやや懐疑的です。Rust Cargoやgo modulesではビルド時に自動的に不足している依存関係を取得してくれるので、これに近いことをyarnでもできればそれで十分じゃないかなと思っています)
yarnの最新安定版であること
忘れられがちですが、yarn v2はすでにリリースされているため「最新安定版」であり、yarn v1は「1つ前のバージョン」です。一般論として、ソフトウェアは新しいほうがいいはずです (枯れたバージョンの利点なども場合によってはありますが)。
yarn v2で特に対応が必要なところや厄介なところ
一方、筆者がWantedly内でyarn v2に移行するにあたって厄介そうだと見込んでいる点は以下の通りです。
yarn.lockのフォーマット変更
yarn v1が生成するyarn.lockは「YAMLもどき」でしたが、yarn v2ではYAMLのサブセットが出力されるようになりました。またシリアライズされるオブジェクトの構造にも多少の違いがあり互換性はありません。
もちろんyarn v2はyarn v1のlockfileを認識してアップグレードしてくれますが、逆はないようです (yarn v2のlockfileをyarn v1に渡したら単純に無視されました)。 基本的にダウングレードの必要性が生じることはないと思いますが、退路を確保するためにyarn-lock-v1というツールを作りました。 https://github.com/qnighy/yarn-lock-v1
yarn以外にyarn.lockを読み書きするツールを使っている場合にも注意が必要です。たとえばdependabotは2021年5月現在yarn v2に対応していません。試しに導入してみたところ、yarn v2のlockfile自体はある程度認識されているようですが、yarn v1形式で書き戻されてしまうのでほぼ役に立ちません。筆者はRenovateへの切り替えを検討しています。
PnPify
yarn v2はNode.jsの標準ライブラリやTypeScriptなどにパッチを当てることでPnPを実現しています。しかし、yarnの介在しない場所で実行されるツールに対してはこの手法は使えないため、追加の対応が必要になることがあります。
具体的にはエディタの対応です。これはエディタによるので網羅的な情報提供はできませんが、たとえばVSCodeを使っている場合
yarn add --dev @yarnpkg/pnpify
yarn pnpify --sdk vscode
を実行してVSCode用の設定を生成する必要があります。これはTypeScriptはもちろん、VSCode側が持っているeslintやprettierのかわりにプロジェクトのeslintやprettierを使わせるために必要です。
VSCodeのセキュリティー上の方針なのか、単に上記コマンドを実行しただけではPnPifyしたツールには切り替わりません。VSCodeを開いてユーザーが明示的に許可を出す必要があります。
またvscodeを対象にpnpifyを実行すると、推奨拡張機能リストも生成されます。 (プロジェクトを開いたときにインストールするか尋ねられます) これはyarn v2でインストールしたパッケージの中身を見るのに必要なので、特にTypeScriptで型定義ファイルに飛んで読むような使い方をよくする人は入れておくべきです。
この辺りはyarn v2が掲げるZero-Installsの理想に真っ向から対立する問題だと思うのですが、TypeScriptやVSCodeにも相応の事情があるので、簡単には解決しないでしょう。
Lifecycle scripts
Lifecycle scriptsはパッケージマネージャが特定のタイミングで実行してくれる特別なscriptのことです。npmは以下のlifecycle scriptsをサポートしています。またyarn v1もこれとほぼ同等の範囲をサポートしています。
- prepublish, prepublishOnly, preprepare, prepare, postprepare
- prepack, postpack
- install, uninstall, version, shrinkwrap, test, start, stop, restart
- 上記のpre-とpost-
- 任意のscript名に対するpre-とpost-
一方、yarn v2では以下の4種類に大幅削減され、実行条件も一部変更されました。
- prepack
- postpack
- prepublish
- postinstall
もしlifecycle scriptsに依存した処理がある場合は注意が必要です。たとえばhuskyはデフォルトではprepare hookを作りますが、yarn v2と組み合わせる場合はpostinstall hookを使いつつpublish時に困らないような細工が必要なようです。
パッケージ管理の将来
PnPを提供しているのはyarn v2だけではありません。pnpmもまた、別の方法でyarn PnPに近い環境を実現しています。そして、yarn v1がnpmとの互換性を重視してきたのに対して、これらの次世代パッケージマネージャは互いに異なる発展を遂げようとしているようにも見えます。ということは、これまで以上に「どのパッケージマネージャを使うか」の選択が重大な選択になっていくようにも思えます。
Wantedlyではこれまでyarnを使ってきたので、yarn v2に移行するのはある程度妥当な選択肢だと思っていますが、場合によってはpnpmも選択肢として検討したほうがいいかもしれません。
まとめと結論
yarn v2について誤解されがちな以下の点を紹介しました。
- yarnをv2にするのにPnPは必須ではない
- PnPの対応状況はかなり良くなっている
- yarn v2のインストールは不要
- yarn v2を使っている著名プロジェクトはすでにある
その上で、yarn v2を導入する利点と懸念点をまとめました。
これらを通して私は次のように考えています。
- yarn v2 の目指す理想は基本的には正しいと思うし、リソースも十分につぎ込まれていているので将来性はある。
- ただしPnPの互換性は100%解決されているわけではないので、現時点での採用は技術スタック次第のところがある。
- PnPを抜きにすれば、yarn v2に上げることはほとんどの場合可能であるように見える。リリースから1年以上経った今でも著名OSSでの普及率がそこまで高くないように見えるのはちょっと不思議で、リソースや技術的な問題というよりも宣伝の問題なのではないかという気がする。
この記事を通じて、yarn v2の利点と欠点がより正しく理解され、Node.jsのパッケージ管理エコシステムに関する議論が前進することを願っています。