Berry(Yarn)とPlug'n'Play


Yarn v2 こと Berry ( Berry こと Yarn v2 か? )を、触ってみました。

Warning

  • npm や yarn(berry) の使い方については触れません。
  • v1 を Yarn、v2 を Berry と呼びます。

Berry(v2)

現在、Berry は npm 経由でインストールすることができます。yarnpkg 配下に置かれているものの、yarn の v2 がそのままの yarn という名前ではなく berry に変更されるのは、 v1 と v2 では機能や設計思想が大きく異なるからでしょう。 以下は、Berry で追加される機能の一部です。私の独断と偏見でピックアップしました。

Zero-installs

既存の Yarn では、yarn install をした際に yarn.lock を参照し、複数の環境でも同様の結果が得られるよう担保していますが、この仕組みでは権限やネットワークなどに関する問題が多くあります。 上記の問題の解決策として、Berry では .yarn ディレクトリに cache ディレクトリを生成し、合わせて .pnp.js も生成します。あとは .yarn ディレクトリ と .pnp.js をリポジトリ内に置いておくだけで、ゼロインストールが実現できるわけです。

cache ディレクトリの中身は、ただの Zip ファイルが詰め込まれているだけであり、言うなれば node_modules の軽量版というイメージでしょう。 従来の node_modules のようにハイカロリーなディレクトリではないため、Git でコミットしても大きな問題にはならないでしょう。

しかし、ファイルサイズが小さくなったとはいえ、コミットされるオブジェクトが膨大になることは変わりないため、ここはプロジェクト毎にゼロインストールを採用するか否か議論が生まれると思います。

Zero-installs の実現に大きく寄与している Offline Cache についても、余力があれば目を通しておくとよいでしょう。

New Command: yarn dlx

npm で言うところの npx ですね。 dlx stands for download and execute だそうです。 一時的な実行環境を用意し、その中でパッケージをダウンロードし、スクリプトを実行するコマンドです。

基本的には npx と同じですが、dlx コマンドは明確にリモートスクリプトで使うと謳われています。 というのも、npx の場合はローカル及びリモートの両方で使われることを想定しているため、タイポしたときに suggestion が表示されます。もし、悪意のあるユーザーによって npx 経由で適当なコマンドを実行された場合、この suggestion の情報が表示され、脆弱性に繋がるというわけです。 上記のような問題を Berry では yarn run と yarn dlx の役割を明確に定義に回避しているというわけです。

Normalized Shell

こちら、個人的に一番気になっています。Windows や MacOS など、OSに依りシェルコマンドが異なるので、その問題に対する回答です。Yarn が簡易的なシェルのインタプリタを実装し、このインタプリタによって環境に依らずコマンドが実行できるというわけです。 上記のインタプリタは @yarnpkg/shell として提供されています。

また、自前でインタプリタを実装したことにより、npm では提供していないような引数の展開なども行えるようになっています。

{
  "scripts": {
    "lint-and-build": "yarn lint \"$@\" && yarn build \"$@\""
  }
}

New Lockfile Format

今回の Breaking Changes の代表格かなと思います。 試しに、 Yarn と Berry のそれぞれで react のモジュールを落としてみました。便宜上、...を記載していますが、無視してください。

# v1 の yarn.lock

# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1

...

react@^16.13.1:
  version "16.13.1"
  resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
  integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==
  dependencies:
    loose-envify "^1.1.0"
    object-assign "^4.1.1"
    prop-types "^15.6.2"

...
# v2 の yarn.lock

# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!

__metadata:
  version: 4

...

"react@npm:^16.13.1":
  version: 16.13.1
  resolution: "react@npm:16.13.1"
  dependencies:
    loose-envify: ^1.1.0
    object-assign: ^4.1.1
    prop-types: ^15.6.2
  checksum: 9c3763a71a6d21c9cef68533ef711522b8a47e8664b0a47250d859dcc34056f3adf846f5a44439e1da95d043b2bc83a6fa6b7bfd178010d1130be296c25bed38
  languageName: node
  linkType: hard

...

ヘッダーはもちろんのこと、それぞれのパッケージ情報も変更されていますね。intergrity → checksum となっていたり、languageName や linkType というパラメータが増えているようです。 また、従来の Yarn は Yaml を拡張したフォーマットで yarn.lock を生成していましたが、他の開発者が混乱したりパーサーで問題が発生したりしたので、Berry からは純粋な Yaml 形式に戻るようです。

TypeScript Codebase / Modular Architecture

モジュールアーキテクチャの採用に加えて Flow から TypeScript に乗り換えたようです。 ESLint や Babel のように、積極的にプラグインの開発を受け入れてコミィニティを活性化させようとしているのでしょう。

Berry を利用するだけではあまり旨味がなく Berry に関する開発を行う人得な変更なので、あまり深く触れません。

Workspace-aware CLI / Workspace Releases / Workspace Constraints

Beery から急に Workspace に本気を出してきたようです。複数の Workspace が存在する時のバージョンの管理、リリースの問題 を解決してくれるようです。 Workspace Constraints という新しい概念が追加され、 Prolog という言語でそれぞれの Workspace で満たすべき条件(制限)を定義できるようです。

私自身は検証で「Workspace を考慮して install できてるんか...?」程度の問題しか直面したことがなく、今回の変更による旨味はあまり実感がないですね。 今後の大規模な開発に備えてキャッツアップはしていきます。

Others

全ての変更を確認する場合はこちらこちら を見ましょう。 日本語で読みたい場合は、 Qiitaのこちらの記事がよいかもしれません。

開発者的な話ですと、Berry で追加される機能は適切に分離されていて、 @yarnpkg/xxx という名前で独立したモジュールが提供されていますので、一度確認してみるとよいでしょう。

Plug'n'Play

Plug'n'Playとはインストールの戦略であり、従来の node_modules 及び Nodeによる依存解決の代わりに、 .pnp.js という単一のファイルを生成し Berry によりパッケージの場所を解決する仕組みです。似たような設計思想で言えば、npm/tink(https://github.com/npm/tink)が真っ先に思い当たりますが、こちらは開発が頓挫しているようで、あまり期待できません。

Plug'n'Play は飽くまで戦略であり、実体は Zero-installs を実現するための .pnp.js になります。 具体的に、Plug'n'Play ( .pnp.js ) によって以下のような問題が解決されます。

  1. node_modules の存在によるインストールなどの速度低下
  2. Node にパッケージという概念が存在しないことによる、依存関係の不透明性
  3. stat や readdir などファイルの照会(確認)に伴うアプリケーション起動にかかる時間的なコスト
  4. node_modules の設計的に異なるバージョンのパッケージを同じディレクトリに共存させることが不可能

Berry の豆知識

ソース読んでちょっとだけわかったので、メモ程度に書いておきます。

Berry では定義されているコマンドを YarnCommand クラスで受け取り、それぞれで Project.find() を行っています。 Project クラス は、特定の Workspace に対するあらゆる操作を管理するクラスで、 install や add コマンドを実行した時に適宜 Configuration クラス をインスタンス化した上で Project クラスがインスタンス化されます。

Project.install() 内で適宜、 resolve / fetch / link といった処理を行うが、 yarn.lock の生成は persist() で行われ、 .pnp.js の生成は linkEverything() で行われます。

ちなみに、.pnp.js 生成の処理が少し追いづらく、Configulation.getLinkers() で取得する PnpLinker の finalizeInstall() 内でインラインスクリプトの生成やマージ処理を行っています。

まとめ

Plug'n'Play に対応できていないパッケージが多く、すぐには採用できないな、というのが結論です。 npm との互換性を断ち切っている部分もあり、生半可な気持ちで Berry には乗り換えられないでしょうから、エコシステムが整うまでは様子見です。

Berry の体験( 特に CLI と 速度 )は既存の Yarn に比べて遥かによいので、Berry が主流になってほしいなと思ったりしてます。

直近は、もう少し Resolution と Linking 周りのコードを読んでいきたい。