Secretlintはmonorepoとなっていて、パッケージを公開する際にlernaを利用していました。 しかし、lernaは現在メンテナンスされていないため、publish機能だけを取り出したlerna-liteと効率的なmonorepo向けのビルドツールであるTurborepoへ移行しました。

追記: NxによってLernaがメンテナンスされるそうです。

次の記事で書いていますが、Lernaはもともとall-in-one的なツールでしたが、それぞれの分野でメジャーなものが各機能と同等のものを実装しています。 一番大きな所としては、Workspaces機能を各パッケージマネージャーが実装したため、Lerna独自でこの機能を持つ必要性がかなり薄くなりました。

元々はLernaがWorkspace管理(依存関係の管理)、タスク管理(パッケージの依存を見てタスクを実行)、Publish管理(バージョンの更新やCHANGELOGの作成、公開)などの機能を持ったツールとしてスタートしています。

Workspace管理は、npm 7+/Yarn/pnpmなどのパッケージマネージャーがworkspaces機能としてサポートし始めました。 タスク管理は、TurborepoやNx(Nxは全部入り)などがより効率的に実行できるツールとして誕生しています。 Publish管理は、まだ成熟したツールは少ないですがchangesets/Ship.js/lerna-lite、パッケージマネージャー自体が持っていることがあります。 - 2022-02-22のJS: Deno 1.19、Next.js 12.1、Monorepo Toolsまとめ - JSer.info

一方で、monorepoのバージョン管理ツールでlernaより使われているものはありません。

https://www.npmtrends.com/changesets-vs-lerna-vs-shipjs-vs-@microsoft/rush

パッケージマネージャー自体は、publishはサポートしていますが、パッケージのバージョン更新については扱っていないことが多いです。

Lernaでは、--canaryでのCanaryバージョンの公開、--conventional-commitsでのConventional Commitsのサポート、--create-release githubでのGitHubリリースの連携などがありました。

この辺の機能を使ったリリースフローは自分の中である程度固まっていて、あまり代替となるツールはありませんでした(あっても別のリリースフローになってしまう)

lerna-liteは、この部分(具体的にはlerna versionlerna publish)の機能だけを取り出してメンテナンスされているForkです。 このリリースフロー以外の機能については、npmやYarn自体でもある程度なんとかなりますが、この部分だけはリリースフロー自体が大きく変えない限り変わるものがありませんでした。

今回は、Lernaからlerna-liteへの移行と、 合わせてTurborepoでのビルドやテストの効率化を行いました。

lerna runからTurborepoへの移行

TurborepoはVercelに買収されて、Vercelで開発されているmonorepo向けのビルドシステムです。 リモートキャッシュや並列ビルドなども対応したビルドシステムです。

Bazelなどもこれらの機能を持っていますが、設定が複雑になることが知られています。 Turborepoは、設定はスクリプトにおける依存関係をpackage.jsonに追加するだけ(大部分は定型的)で、簡単に導入できるようになっています。

他のmonoreoツールについては、次のサイトでも解説されています。

まずは、リリース周り以外のビルドやテストなどをLernaではなくTurborepoを使って実行するようにしました。 次のPRで実際の変更が見れます。

Yarn v1 + Lernaから、Yarn v1 + Turborepoに移行していくつか異なる点はありました。(Turborepo v1.1)

  • Turborepoの方が厳密なので、入れ忘れていたdevDependenciesがエラーになった
  • ENV_A=1 turbo run build と実行して、各パッケージのnpm run buildにはENV_Aが渡されなかった
  • Turborepoは循環参照を見つけても、ハングするだけでエラーを報告してくれなかった(Lernaではエラーを報告してくれた)
    • 今後改善される可能性はありそう

速度については、ローカルキャッシュで導入したのでCI自体はそこまで多くは変わりませんでした。 キャッシュがあって、変更がないときのturbo runの結果はキャッシュが使われるので、何度も実行する場合には早くなったと思います。

これで、lerna versionlerna publish以外では、Lernaを使わないようにできました。

LernaをLerna-Liteに移行

lernaからlerna-liteへの移行は簡単です。

lerna versionlerna publishは互換性がある同じ名前のコマンドが用意されているので、次のようにインストールするだけです。

yarn remove lerna -W
yarn add @lerna-lite/cli --dev -W

@lerna-lite/cliを入れるとlernaコマンドが利用できるので、他に変更する必要はありません。

実際のPRは次で見れます。

Secretlintでは、lerna runを先に外したので使っていませんでしたが、@lerna-lite/runを使うとlerna runコマンドも利用できます。

npmを使っている場合は、lerna-liteはnpm 8が必要になっています。 Node.jsは14が必要で、最初は16が必要でしたが修正しています。

これで、lernaからlerna-lite + turborepoに移行できました。

おわり

Secretlintでは、パッケージをCIからリリースするようにしています。

npmのMFAがpublishに対して有効の場合は、OTP codeが必要ですが、OTP codeには有効期限があるため大量のパッケージをpublishすると期限が切れてしまう可能性があります。 そのため、有効期限がないnpm automation tokensを作成して、CI経由でpublishしています。

しかし、このnpmのtokenはリポジトリ/パッケージ単位ではなくユーザー単位になっています。 複数人で開発するリポジトリでは、リポジトリへ個人のnpm tokenは設定すると、別のメンテナーから利用できてしまう問題があります。 そのため、monorepoごとにnpmユーザーを作成して、そのnpmユーザーのnpm automation tokensを利用する必要があります。

これがとても面倒で、GitHubの権限でGitHub Actionsからnpmへpublishする機能が入ってくれると、こんなややこしいことをしなくて済むのにと思いました。

リポジトリ単位のnpm tokenを扱うツールとしては、GCP周りのライブラリで使われているWombat Dressing Roomがあります。

リリースノートについては、GitHubがPRから自動的に作成する機能を追加したりしています。 そのため、Lernaの--create-release githubなどのリリースノート周りの機能もあまり使わなくなってきた気がします。

ローカルからpublishするよりもCIからpublishする方がクリーンで安全なので、今後はそういう方向になっていく感じはします(lernaはローカル = 人間向けの機能がちょこちょこあるので、この辺もomit出来そう)。 ただし、先ほども書いたnpm publishに必要なトークンの管理が面倒という問題があるので、この部分が改善されてきたら、またリリースフローは変わりそうな気がしました。