🌋

【v5.2対応】MelosでDart/Flutterのモノレポ管理

2024/03/19に公開

こんにちは、Flutterでのアプリ開発をメインとしている「Altive株式会社」の村松龍之介(@riscait)です!

以前にMelos v1.0.0 についてのチートシートという記事を書きました。

https://zenn.dev/altiveinc/articles/melos-for-multiple-packages-dart-projects

もう v5.2 がリリースされたとのことで進化を感じますね…!

弊社でもいくつかのアプリプロジェクトでMelosを使ったモノレポ運用を行っております。

弊社 flutter_app_template でもMelosを採用しています。
melos.yaml はこちら↓

https://github.com/altive/flutter_app_template/blob/main/melos.yaml

目次

Melosとは?

複数のパッケージを持つDartプロジェクト(Monorepoとも呼ばれる)の管理支援CLIツールです。

大規模なプロジェクトをバージョン管理されたパッケージに分割することはコード共有のために非常に有効です。

しかし、多くのリポジトリにまたがって変更を加えるのは、手間がかかり、追跡が難しく、リポジトリをまたがるテストも複雑になりがちです。

Melosでは、複数のパッケージが互いに独立しながらも、1つのリポジトリで一緒に作業できるように支援してくれます。

また、Invertase さんによって構築・メンテナンスが行われています。

主な機能

  • 複数のパッケージで同時にコマンドを実行
  • ローカルパッケージのリンクとインストール
  • ローカルパッケージとその依存関係のリストアップ
  • 自動バージョン管理とチェンジログの作成
  • pub.devへのパッケージの自動公開
  • CI/CD環境での自動化支援

Melosが使われているプロジェクト

Flutterアプリ開発者にとっては馴染みのありそうなリポジトリでMelosは実際に使われています。

READMEの who is using melos に多数リストアップされています。

https://pub.dev/packages/melos#who-is-using-melos

以下、いくつかをリストアップします。
それぞれの melos.yaml を見るだけでも勉強になりますね👀

FlutterFire, PlusPlugins, Riverpod等々、
個人的に日頃お世話になっているパッケージで使われていました。

Flutterモノレポ構成例

以下、FlutterアプリでモノレポとMelosを採用する場合のリポジトリ(ディレクトリ)構成の一例です。

- packages/
  - foo_package
    - lib/
      - src/
      - foo_package.dart
    - pubspec.yaml
  - bar_package
    - lib/
      - src/
      - bar_package.dart
    - pubspec.yaml
  - hoge_app
    - lib
      - src/
      - main.dart
    - pubspec.yaml
- .gitignore
- analysis_options.yaml
- melos.yaml
- pubspec.yaml

ルートに、 melos.yamlpubspec.yaml を配置します。

上の例でいうと、 hoge_appfoo_packagebar_package に依存している場合は、
hoge_apppubspec.yaml にパスで指定しましょう。

hoge_app/pubspec.yaml
dependencies:
  foo_package:
    path: ../foo_package/
  bar_package:
    path: ../bar_package/

Melosのセットアップ

インストール

以下のコマンドで pub.dev からグローバルなパッケージとしてインストールしましょう。

$ dart pub global activate melos

$ melos --version
5.2.0

melos.yaml ファイルの作成

Melosを使用したいワークスペース(リポジトリ)のルートに melos.yaml ファイルを新規作成します。

melos.yaml
name: my_project
repository: https://github.com/altive/flutter_app_template
packages:
  # packagesディレクトリとそのexampleパッケージを対象にする例(globeパターン使用)
  - packages/*
  - packages/*/example
ignore: # 除外したいパッケージがあればリストアップ可能
  - packages/foo

パスの指定には、 .gitignore でも使われている globパターンを使用できます。

pubspec.yaml ファイルの作成

モノレポ構成の場合、プロジェクトディレクトリのルート(ワークスペース)はDartパッケージではありませんが、 pubspec.yamlを配置する必要があります。

pubspec.yaml
name: foobar_workspace # 任意のワークスペース名

environment:
  sdk: ^3.3.0 # このワークスペースで使用したいDartのバージョン

dev_dependencies:
  melos: ^5.2.0 # このワークスペースで使用したいMelosのバージョン

指定したMelosのバージョンがワークスペース使用されます。

Commands

melos bootstrap

Melosのインストールと melos.yaml の作成が完了したら、次は melos bootstrap を実行しましょう。

  • 全パッケージの依存関係がインストールされます(内部的には pub get が使われます)
  • 全パッケージがローカルにリンクされます。
melos bootstrap
# または
melos bs

bootstrapコマンドのHooks

melos.yamlcommand.bootstrap.hooks を指定することでフックを指定することできます。

以下の例では、 bootstrap コマンド実行後に flutter gen-l10n を実行しています。

melos.yaml
command:
  bootstrap:
    hooks:
      post: |
        melos exec --flutter --dir-exists=lib/l10n -- "flutter gen-l10n"

Bootstrapが必要な理由

通常のプロジェクトでは、pubspec.yaml 内でパスを指定することでパッケージをリンクすることができます。
小規模なプロジェクトや公開しないパッケージでは有効ですが、
そうではない場合、パッケージの公開時に手動ですべてのパッケージのpubspec.yamlファイルをバージョンで更新する必要が出てきます。

また、パッケージが密結合(互いに依存している)している場合、どのバージョンを更新すべきかを手動でチェックする必要があり、煩雑です。

https://melos.invertase.dev/commands/bootstrap

melos clean

プロジェクトの一時ファイルをクリーンアップします。
bootstrap の前に実行するなどして完全に新しい状態でスタートできます。

Cleanコマンドについてより詳しくはこちら↓
https://melos.invertase.dev/commands/clean

cleanコマンドのHooks

melos.yamlcommand.clean.hooks を指定することでフックを指定することできます。

以下の例では、 clean コマンド実行後に flutter clean と Podfileの削除を行なっています。

melos.yaml
command:
  clean:
    hooks:
      post: |
        melos exec --flutter -- "flutter clean"
        melos exec --flutter --file-exists="ios/Podfile.lock" -- "cd ios && rm Podfile.lock"

melos analyze

v5.0.0 でビルトインコマンドとして追加されました🚀

各パッケージで flutter analyze が実行されます。

melos format

v5.1.0 でビルトインコマンドとして追加されました🚀

各パッケージで dart format . が実行されます。

melos exec

exec コマンドは、すべてのパッケージに対して任意のコマンドを実行するコマンドです。

# すべてのパッケージで依存関係を取得するコマンドを実行する
melos exec -- "flutter pug get"

繰り返し使うコマンドは、 exec コマンドを都度打って実行するのではなく、後述の scripts を定義し、melos run コマンドで簡単に呼び出せるようにしましょう👌

https://melos.invertase.dev/commands/exec

--concurrency (-c)

同時にコマンドを実行するパッケージの最大数を定義します。デフォルト数は実行プラットフォームにより異なります。

# パッケージ1つずつ順番にコード生成実行を行いたい場合
melos exec -c 1 -- "flutter pub run build_runner build -d"
# パッケージ8つで並行してコード生成監視を行いたい場合
melos exec -c 8 -- "flutter pub run build_runner watch -d"

--fail-fast

いずれかのパッケージでスクリプトが失敗した場合に、それ以降のパッケージでスクリプトを実行しないようにします。
デフォルトはfalseです。

melos exec --fail-fast -- "dart analyze ."

melos list

認識されているパッケージのリストを出力します。

melos list
foo_package
bar_package
hoge_app

https://melos.invertase.dev/commands/list

melos version

melos version コマンドは、パッケージのバージョンを更新するためのコマンドです。

Conventional Commits を使用してコミットしていれば、
SemVer(セマンティック バージョニング) に基づいてバージョンを自動で判定してくれます。

melos publish

パッケージを pub.dev に公開するためのコマンドです。
デフォルトで --dry-run が有効になっており、実際に公開される前に確認ができます。

確認後、本当に公開する場合は --no-dry-run をつけて実行します。

https://melos.invertase.dev/commands/publish

melos run

melos.yamlscripts: に定義したスクリプトを実行します。

以下の定義例では、すべてのパッケージで dart fix を実行する fix という名前のスクリプトを定義しています。
(より簡潔な書き方があるのですが、それは後述します)

melos.yaml
scripts:
  fix:
    run: melos exec -- "dart fix --apply lib"

このスクリプトを使用する際は、

melos run fix

で実行可能です👌

https://melos.invertase.dev/commands/run

Hooks

特定のMelosコマンドは、melos.yamlcommand セクションで hooks を定義することで、コマンドの前後にスクリプトを実行させられます。
コマンド実行前に処理を差し込む pre と、コマンド実行後に処理を差し込む post が用意されており、

特定のコマンドでは他にもフックが用意されています。
対応しているコマンドとフックは以下の通りです。

  • bootstrap
    • pre
    • post
  • clean
    • pre
    • post
  • version
    • pre
    • post
    • preCommit
  • publish (v5.0.0 で追加)
    • pre
    • post

CommandでのHooks設定例

command:
  bootstrap:
    hooks:
      post: dart pub run build_runner build
  clean:
    hooks:
      post: melos exec --flutter --concurrency=3 -- "flutter clean"
  publish:
    hooks:
      pre: dart pub run build_runner build
      post: dart pub run build_runner clean

https://melos.invertase.dev/configuration/scripts#hooks

Scripts

Scriptを定義することで、 melos run {script_name} で実行できるようになります。

https://melos.invertase.dev/configuration/scripts

スクリプトの名前を決めて定義

最小の構成は以下の通りです。
{名前}: {実行内容} で定義します。

melos.yaml
scripts:
  hello: echo 'Hello World'

実際には、以下の拡張構文を使用することも多いです。

melos.yaml
scripts:
  hello:
    name: hello-world
    description: 'My first script'
    run: echo 'Hello $TARGET!'
    env:
      TARGET: 'World'
  • name: スクリプトのユニークIDです。(使い所が分かりません)
  • description: 引数なしでmelos runを使用するときに表示される説明文
  • run: 実行されるコマンドを記述します
  • env: 環境変数をMapで指定できます

exec:melos exec を実行するスクリプトを定義

v2.4.0exec が追加されました。

melos.yaml
scripts:
  fix:
    run: dart fix
    exec:
      concurrency: 1

のように exec オプションを指定することで、 run: melos exec -- "dart fix" と同じように実行できます。

また、 exec のデフォルトオプションのままでよければ、以下のようにシンプルに記述できます👍

melos.yaml
scripts:
  fix:
    exec: dart fix

packageFilters: で対象のパッケージを絞り込む

packageFilters を使用しないスクリプトでは、認識されているすべてのパッケージを対象として実行されます。

packageFilters を使用することで、対象のパッケージを絞り込み、実行時にプロンプトで実行対象パッケージを選択することが可能になります。

下記の例では、 packageFilters にて3つの条件を指定しています。

melos.yaml
scripts:
  gen:
    run: dart run build_runner build -d
    exec:
      concurrency: 1
    packageFilters:
      flutter: true # Flutter SDKに依存するパッケージのみを対象にする
      dirExists: lib # libディレクトリを持つパッケージのみを対象にする
      dependsOn: build_runner # build_runnerに依存するパッケージのみを対象にする

実行時には、以下のように選択肢が表示されます。
1を選択したりEnterキーを押せば、全てのパッケージを対象に実行されます。

Select a package to run the gen script:

1) * [Default - Press Enter]
2) foo_package
3) bar_package
4) hoge_app

packageFilters の各フィルターはキャメルケースで指定します。
コマンドラインオプションではケバブケースで使用している同名オプションをキャメルケースとして使うことができます。

例: --depends-ondependsOn

packageFilters で使用できる、フィルタリング一覧は、後述の Filters を参照ください👌

steps で複数のスクリプトを実行するスクリプトを定義

通常のスクリプトの runexec でも && で繋げることで複数のスクリプトを実行するスクリプトを定義することができました。
v5.2.0 で新たな構文 steps が追加され、複数のスクリプトを実行するスクリプトを、より明瞭に定義することができるようになりました。

以下は、 iOSを対象とした pod:ios スクリプトと、 macOSを対象とした pod:macos スクリプトを順番に実行する pod スクリプトを定義した例です。

melos.yaml
scripts:
  pod:
    description: Run all pod install.
    steps:
      - echo 'pod install を実行します!'
      - pod:ios
      - pod:macos
      - echo 'pod install を実行しました!'

  pod:ios:
    description: Run pod install on iOS.
    exec: cd ios && pod install
    packageFilters:
      dirExists: [lib, ios]
      fileExists: "ios/Podfile"

  pod:macos:
    exec: cd macos && pod install
    description: Run pod install on macOS.
    packageFilters:
      dirExists: [lib, macos]
      fileExists: "macos/Podfile"

Filters

パッケージのフィルタリングオプションです。

bootstrap , clean , exec , list , publish , version コマンドなど、
run 以外の主要なコマンドで使用できます。

--no-private

publish_to: none が指定してある=プライベートなパッケージを対象から除外します。
(この指定がない場合は、プライベートなパッケージも含まれます)

使用例:

melos bootstrap --no-private

--published

現在(ローカル)のバージョンが pub.dev に存在するパッケージのみに絞り込む。

使用例:

melos bootstrap --published

現在(ローカル)のバージョンが pub.dev に存在しないパッケージのみに絞り込みたい場合は --no-published が使用できます。

--scope

指定した文字列を含むパッケージのみに絞り込む。

使用例:パッケージ名に app を含むもののみ flutter build ios を実行する。

melos exec --scope="*app*" -- flutter build ios

--ignore

指定した文字列を含むパッケージを対象から除外する。

使用例:パッケージ名に example を含まないもののみ flutter build ios を実行する。

melos exec --ignore="*example*" -- flutter build ios

--diff

指定の範囲で差分のあるパッケージのみに絞り込む。

# 指定のコミットと現在のブランチの差分があるパッケージのみで実行する。
melos exec --diff=<commit hash> -- flutter build ios

# リモートのmainブランチとHEAD間での差分があるパッケージのみで実行する。
melos exec --diff=origin/main...HEAD -- flutter build ios

--dir-exists

特定のディレクトリが存在するパッケージのみに絞り込む。

使用例: example ディレクトリを持つパッケージのみで実行する。

melos exec --scope="example" -- flutter build ios

--file-exists

使用例: analysis_options.yaml ファイルが存在するパッケージのみで実行する。

melos exec --file-exists="analysis_options.yaml" -- flutter build ios

--flutter

Flutter SDKに依存するパッケージのみに絞り込む。

使用例:

melos exec --flutter -- flutter test

Flutter SDKに依存しないパッケージのみに絞り込みたい場合は --no-flutter を使用できます。

--depends-on

特定の依存関係を持つパッケージのみに絞り込む。

使用例: build_runner に依存するパッケージでのみ実行する。

melos exec --depends-on="build_runner" -- flutter pub run build_runner build

特定の依存関係を持たないパッケージのみに絞り込みたい場合は --no-depends-on を使用できます。

環境変数

いくつかの環境変数が定義されており、 melos exec 内で使用することができます。
例: MELOS_PACKAGE_NAME など

またいくつかの環境変数を定義することもできます。
例: MELOS_SDK_PATH など

すべての環境変数の一覧は以下のリンクを参照ください👀
https://melos.invertase.dev/environment-variables

IDE Support

(VS Code)の拡張機能

VS Code向けに便利なエクステンションが用意されています。

https://melos.invertase.dev/ide-support#vs-code

  • melos.yaml の検証とオートコンプリート
  • CodeLenses を介したスクリプト実行できる
  • VS Codeの Taskとしてスクリプトを実行できる
  • コマンドパレットから、以下のようなMelosコマンドが実行可能にな
    • Melos: Bootstrap
    • Melos: Clean
    • Melos: Run script
    • Melos: Show package graph
  • パッケージの依存関係グラフを出力できる

task.json

VS CodeのTaskとして使いたいスクリプトは、 .vscode/tasks.json を作成してスクリプトを指定します。

tasks.json
{
  "version": "2.0.0",
  "tasks": [
    {
      "type": "melos",
      "script": "analyze",
      "label": "melos: analyze"
    }
  ]
}

パッケージの依存関係グラフを出力できる

中〜大規模なアプリでは、何気にこの機能がありがたいです。パッケージ間がどのような依存関係になっているのかがグラフとして出力できます。

公式ドキュメント例はもっと参考になります↓

https://melos.invertase.dev/ide-support#package-graph

IntelliJ

https://melos.invertase.dev/ide-support#intellij

READMEにバッジを表示する

以下を貼り付けることで、Melosを使用したプロジェクトであることを手軽に示すことができます🌋

[![melos](https://img.shields.io/badge/maintained%20with-melos-f700ff.svg?style=flat-square)](https://github.com/invertase/melos)

宣伝

Altive株式会社では、Flutterアプリの開発・運営を承っております。
お気軽にお問い合わせください👍
https://altive.co.jp/contact


Riverpod の実践入門本を公開中です📘
https://zenn.dev/riscait/books/flutter-riverpod-practical-introduction

参考リンク

GitHub repository
https://github.com/invertase/melos

GitHubで編集を提案
Altiveエンジニアリングブログ

Discussion