はじめに
複数のパッケージを1つのリポジトリで管理していると、「このパッケージだけバージョンを上げたのに、依存している別パッケージのpublishを忘れた」「変更したパッケージだけテストを回したいのに、全部実行して時間がかかる」といった悩みに突き当たります。いわゆるモノレポ特有のつらさです。
Lernaは、まさにこの課題を解決するために作られたツールです。複数のJavaScript/TypeScriptパッケージを1つのリポジトリ(モノレポ)で管理しながら、バージョン管理・パッケージ公開・タスク実行をまとめて面倒みてくれます。
面白いのは、Lernaが一度は開発が停滞し「もう終わったツール」と見なされかけた過去を持つことです。しかし2022年にNx開発元のNrwlチームがスチュワードシップ(管理主体)を引き継ぎ、Nxのタスクスケジューラやキャッシュ機構を取り込んだ形で現役ツールとして復活しました。今回はそんなLernaの基本から実践的な使い方まで見ていきます。
Lernaはブラウザではなくローカル環境・CI環境で動くCLIツールのため、この記事にLiveCodesの実行サンプルはありません。その代わり、実際にコピペして試せるコマンド例を多く載せています。
Lernaとは
Lernaは、複数のパッケージを含むリポジトリ(モノレポ)のバージョン管理・パッケージ公開・タスク実行を行うためのビルドシステムです。npm/Yarn/pnpmのワークスペース機能と組み合わせて使うのが基本で、Lerna自体が依存関係のインストールやリンクを担うわけではありません。
主な特徴
- バージョニングと公開の自動化 -
lerna versionとlerna publishで、変更のあったパッケージを検出し、バージョンを上げてnpmに公開するところまでを一気通貫でこなせる - 依存関係を考慮したタスク実行 -
lerna runは、パッケージ間の依存グラフに沿って正しい順序でスクリプトを実行する(例:headerとfooterをビルドしてからremixappをビルド) - Nxのキャッシュ・並列実行を活用 - 内部でNxのタスクランナーを使っており、変更のないパッケージのビルド結果をキャッシュして再利用できる
- 独立バージョニング / 統一バージョニングの両対応 - パッケージごとに個別のバージョンを持たせる(Independent mode)か、リポジトリ全体で1つのバージョンに揃える(Fixed/Locked mode)かを選べる
インストール
新規リポジトリでLernaを使い始める場合は、lerna init コマンドが空のディレクトリに必要な設定一式(lerna.json、package.jsonのworkspaces設定など)を作成してくれます。
mkdir my-monorepo && cd my-monorepo
npx lerna init
既存のリポジトリに導入する場合も同じく lerna init を使います。
npx lerna init
グローバルにインストールしたい場合は以下のように導入できます。
npm install --global lerna
# または
yarn global add lerna
# または
pnpm add --global lerna
基本的な使い方
lerna init を実行すると、packages/ 配下にある各パッケージをLernaが認識できるようになります。まずはリポジトリの構成を確認してみましょう。
# 現在管理下にあるパッケージの一覧を表示
npx lerna list
# 各パッケージの依存関係をグラフとして表示
npx lerna list --graph
パッケージ内のnpmスクリプト(buildやtestなど)は、lerna run でまとめて実行できます。依存関係の順序はLernaが自動で解決してくれるため、開発者が実行順を意識する必要はありません。
# すべてのパッケージのbuildスクリプトを実行(依存関係の順に実行される)
npx lerna run build
# 特定のパッケージだけを対象にする
npx lerna run test --scope=header
# build,test,lintをまとめて実行
npx lerna run build,test,lint
例えば header と footer に依存する remixapp がある構成で lerna run build を実行すると、header→footer→remixapp の順にビルドが走ります。この依存順序の自動解決こそが、Lernaを単なる並列実行ツールと分けている点です。
実践的なユースケース
パターン1: バージョンを一括で更新する
複数パッケージの変更をまとめてバージョンアップし、gitタグを打ちたいケースです。lerna version は変更のあったパッケージを検出し、対話形式でバージョン番号(patch/minor/major)を選ばせたうえで、package.jsonの更新・コミット・タグ付けまでを自動化します。
# 変更のあったパッケージについて対話的にバージョンを決定し、
# package.jsonの更新・git commit・git tagまで行う
npx lerna version
# privateなパッケージ(remixappなど)はバージョニング対象から除外する
npx lerna version --no-private
# 全パッケージのバージョンを揃えて上げたい場合(Fixed mode)
npx lerna version minor
lerna version はデフォルトで「独立バージョニング(Independent mode)」で動作します。各パッケージに同じバージョン番号を強制したい場合は、lerna.json の version を固定値(例: "1.0.0")にしておくとFixed modeに切り替わります。
パターン2: パッケージをnpmに公開する
バージョンを上げたあと、実際にnpmへpublishするパターンです。lerna publish は内部でlerna versionの処理を含んでおり、バージョン更新から公開までを1コマンドで完結できます。
# バージョン更新からnpm publishまでを一括で実行
npx lerna publish
# 既にlerna versionで上げたバージョンを、そのまま公開する場合
npx lerna publish from-package
# 公開前に実際の変更内容をdry-runで確認する
npx lerna publish --dry-run
--no-private を付けたバージョンや、publishConfig で公開設定を個別に持つパッケージなど、細かい制御も可能です。CIパイプラインに組み込む場合は --yes を付けて対話プロンプトをスキップするのが定番です。
パターン3: 変更のあったパッケージだけをテストする
モノレポが大きくなるほど、「全パッケージのテストを毎回回す」のは時間の無駄になっていきます。Lernaは --since オプションで、指定したgit参照(ブランチやコミット)以降に変更があったパッケージだけに対象を絞れます。
# mainブランチとの差分があるパッケージだけbuildを実行
npx lerna run build --since=main
# 直前のコミットから変更があったパッケージだけtestを実行
npx lerna run test --since=HEAD~1
CIでプルリクエストごとに全パッケージを回す代わりに、この--sinceを使うことでビルド時間を大きく削減できます。Nxのキャッシュ機構と組み合わせれば、変更のないパッケージは再ビルドすらされません。
まとめ
Lernaは、複数パッケージのバージョニング・公開・タスク実行という、モノレポ運用でつまずきやすいポイントをまとめて解決してくれるツールです。一時期は開発が停滞していましたが、Nxチームによるスチュワードシップ移管後はNxのキャッシュ・並列実行の恩恵を受けながら開発が続けられており、現在も活発にメンテナンスされています。
すでにnpm/Yarn/pnpmのworkspacesを使っている方なら、npx lerna init を実行するところから気軽に試せます。まずは lerna run でタスク実行の依存順序解決を体験し、慣れてきたら lerna version / lerna publish でリリースフローを自動化してみてください。