Dart のモノレポサポート

毎年少なくとも言語を1つ学習する。
— 達人プログラマー ―熟達に向けたあなたの旅― 第2版

毎年の年末年始に読み返すほど、達人プログラマーが好きで今年は改めて Dart を勉強している。
ひとまず Dart の言語仕様 を一通り目を通して、特に 3.0 以降については割と現代的な言語になっているような印象を受けた。
それこそ話題になっていた Null Safety などもあるし、最近の言語のトレンドを追っている印象がある。

その中でひときわ面白いなと思ったのが、モノレポサポートを Pub workspaces という機能で言語自ら提供していることだった。 ( Dart 3.6.0 から利用できる機能)

モチベーションとしては、 dart pub get を各パッケージごとに実行してしまうと個々のパッケージで利用しているバージョンが変わってしまうというようなもので非常に理解できる。
また Flutter などのモバイルアプリに関してはマルチモジュール構成が当たり前になりつつあるのでモノレポサポートは自然だったのかもしれない。

この機能について色々調べていく中で、 Melos という昔からあるモノレポサポートのライブラリがあることを知った。
また 7.0系からは Pub workspaces の上に様々な便利機能を提供するという形になっていた。
see also https://melos.invertase.dev/guides/migrations#6xx-to-7xx

Melos 自体の機能の詳細にはついては https://melos.invertase.dev/ を見てもらうとして、Melos によって「全てのパッケージに対してテストを実行する」みたいなよくあるシチュエーションを簡単に解決することができる。

 # pubspec.yaml
 # 7.0系以前は melos.yaml を使っていたが 7.0系からは pubspec.yaml に統合された
 # see also https://melos.invertase.dev/guides/migrations#6xx-to-7xx
 # ....
 melos:
   scripts:
     test:
       # Dart テストを実行し、カバレッジレポートを生成するスクリプト
       # 詳細は https://github.com/dart-lang/test/tree/master/pkgs/test を参照
       run: |
         # カバレッジデータを生成
         dart test --coverage="./coverage"
         # coverage パッケージをグローバルに有効化
         dart pub global activate coverage
         # カバレッジデータをフォーマットし、HTML レポートを生成
         dart pub global run coverage:format_coverage --report-on=lib --lcov -o ./coverage/lcov.info -i ./coverage
         genhtml -o ./coverage/report ./coverage/lcov.info
       exec:
         concurrency: 1
     # カバレッジデータを収集し、統合レポートを生成するスクリプト
     collect_coverage:
       run: |
         # Melos を使用して各パッケージのカバレッジファイルをリストアップ
         # パッケージ名の後に /coverage/lcov.info を追加する
         target_lcov_files=$(melos list --parsable | sed 's|$|/coverage/lcov.info|')
         # -a オプションを使用して、複数の lcov ファイルを統合 
         lcov -o ./coverage/combined.lcov $(echo "$target_lcov_files" | sed 's/^/-a /')
         # 統合されたカバレッジデータから HTML レポートを生成
         genhtml ./coverage/combined.lcov --output-directory ./coverage/report
         open ./coverage/report/index.html

上記の記述をすると以下のようにそれぞれのスコープに応じたスクリプトを書き分けることができる。
機能としては、 Workspace Scripts にもある runexec を使っている。

melos run test # 全てのパッケージに対して実行
melos run collect_coverage # ルートに対して1度だけ実行する

また容易に想像がつくが、割とスクリプトが簡単に増えていってしまう。
実際に利用している人たちのプラクティスをもっと知りたいものの個人的に以下のような工夫をすることで今のところ便利に使えている。

# pubspec.yaml
# ....
melos:
  scripts:
    tasks:
      run: yq '.melos.scripts | keys | .[]' pubspec.yaml
    tasks:run:
      run: melos run $(yq '.melos.scripts | keys | .[]' pubspec.yaml | peco)
    # 他のスクリプトたち
    # ...

peco の代わりに fzf でも良いと思うが、要するに melos run tasks:run で定義しているスクリプトから選択して、そのまま実行できるというもの。

またスクリプトは、絞り込みを前提として apps:server:run のようにある特定のコンテキストで : で区切っていくような形にしてみている。

yq などを利用しないで済むなら嬉しいと思い invertase/melos#895 を挙げてみている。
余裕があったら Melos のコードを追ってみたいが、一旦様子見。