複数のServerlessのパッケージを1つのモノレポにまとめるまでの旅路(あるいはyarn v2の話)
はじめに
お仕事でServerless Frameworkで作ったAPIパッケージを複数扱っているのですが、eslintやjest、tsconfigの似たような設定がいろんなところに散らばってたり、ロジックが似通っている部分がある状況でした。そんな状況を改善するために複数のServerlessパッケージを1つのモノレポにまとめることを行いました。
モノレポ化への旅路
モノレポの作成先を用意する
モノレポを作成する場合、
- 新しくリポジトリを作る
- 既存のどれかのリポジトリに他のリポジトリを合流させる
の2つの選択肢があると思います。今回の場合は、中心となるリポジトリがあったのでそこに他のリポジトリを合流させる方法を取りました。
合流先のリポジトリをモノレポ化する
まずリポジトリのルートにpackages/<パッケージ名>
ディレクトリを用意し、リポジトリの内容をそこにすべて移動させます。
その後、ルートでyarn init -y
を行いモノレポパッケージを作成します。
lernaを使って他のリポジトリをimportする
lernaというモノレポ用のツールがあるのですが、これが他のリポジトリをモノレポのリポジトリに持ってくる(importする)機能があり、これが非常に便利なので使います。
npx lerna import <他のリポジトリへのパス> --flatten
マージコンフリクトを解消したコミットがあるとimportに失敗するので、--flatten
オプションを指定します。詳細は下記のサイボウズのブログが詳しいです。
yarn workspaceを設定する
yarnはworkspaceというモノレポをやるにはうってつけの機能を持っています。package.jsonにworkspaces
の設定を行うことでworkspace機能を有効化することができます。
{
...
"workspaces": {
"packages": [
"packages/*"
]
}
}
packages配下にすべてのリポジトリを持ってくる場合は上記の指定で大丈夫です。もう少し込み入ったリポジトリ構成にする場合は個別に指定を書きます。
yarn workspaceを設定したら一度yarn install
を行います。そうすると、モノレポのルートにすべての依存関係が記述されたyarn.lock
ファイルが生成され、各リポジトリにあるyarn.lock
ファイルは無視されるようになります。(なので各リポジトリのyarn.lock
ファイルは消します)
yarn v2の導入
yarn v1のworkspace機能も十分な機能を持っているのですが、yarn v2のほうがよりworkspaceの機能が強化されているので、そちらを使います。
yarn v2の有効化
yarn v1の最新版が入っている状況で、下記のコマンドでyarn v2を有効化することが出来ます。
yarn set version berry
そして、このままだと下記のbugがあるので、yarn set version from sources --branch 2262
で少し新しめのyarn v2を入れておきます。
ちなみに、yarn set version from sources
で最新の入れてもよいのですが、どうも安定していない感じがするので個人的には2262
がオススメです。
yarn v2の設定
yarn v2はyarn v1に比べて設定ファイル(.yarnrc.yml
)とディレクトリ(.yarn
)が増えています。
yarn v2は本来はPlug'n'Playというnode_modules
を生成しないで使うモードがデフォルトなのですが、それだと色々不都合があったり、またyarn v1からのマイグレーションという点ではnode_modules
の方が楽なので、node_modules
を生成するモードで使います。
.yarnrc.yml
に下記の設定を足します。
nodeLinker: node-modules
hoist
の設定
yarnはパッケージのhoist機能を持っているのですが、これを適切に設定しないとパッケージが勝手にルートに巻き上げられてしまいます。
簡単に説明します。パッケージROOT
(パス/ROOT
)はパッケージA
(パス/ROOT/packages/A
)をworkspaceとして保持していて、パッケージA
はライブラリX
を依存として持っています。またX
はY
を依存として持っていて、Y
はZ
を依存として持っています。hoist無しの状態でインストールすると、下記のようにファイルが展開されます。
/ROOT/packages/A/node_modules/X/node_modules/Y/node_modules/Z
※実際は他の依存との兼ね合いもあって、もっと複雑な感じになりますが、ここで簡略化して説明してます
ここから、yarn v2のnmHoistingLimits
の設定に応じて下記のように変化します。
none
nmHoistingLimits
のデフォルト値です。出来るだけhoist
しようとします。なので下記のようになります。
/ROOT/node_modules/X
/ROOT/node_modules/Y
/ROOT/node_modules/Z
/ROOT/packages/A
workspaces
hoist
する上限が、workspaceになる設定です。今回のモノレポ化ではこれを使っています。
/ROOT/packages/A/node_modules/X
/ROOT/packages/A/node_modules/Y
/ROOT/packages/A/node_modules/Z
workspaceレベルでhoist
が行われるので結構使いやすい設定だと思います。
dependencies
workspaceの直接の依存を超えてhoist
しない設定です。
/ROOT/packages/A/node_modules/X/node_modules/Y
/ROOT/packages/A/node_modules/X/node_modules/Z
このような感じになる気配がしてます。ちゃんと使い込んでないので不正確かもしれません
オフラインキャッシュの設定
yarn v2はオフラインキャッシュというリポジトリに依存関係を保持させ、インターネット接続がない場合でもyarn install
が可能な機能を持っています。
オフラインキャッシュの場所
yarn v2が設定された状態でyarn install
を行うと、.yarn/cache
以下に自動でオフラインキャッシュが作成されます。このオフラインキャッシュはyarn v2がnpmjs.com
から取ってきた圧縮ファイルを展開し、再度zipで圧縮しなおしたファイルになってます。
オフラインキャッシュのメリット
- インターネット接続が厳しい環境でも比較的戦えるようになる(っぽい)
僕の普段の開発環境では特にインターネット接続での問題はないのですが、たとえばプロキシーが強制されてyarn install
が辛い、という環境ではオフラインキャッシュを使うことでだいぶ開発の負担が軽減される気がします。
オフラインキャッシュのデメリット
- リポジトリにオフラインキャッシュをコミットすると、結構な容量になる
依存関係が全て入るので、serverlessを使ったAPIのパッケージの場合最低でも100MBぐらいは行くのではないかと思います。Githubのリポジトリの最大は1GBだったと思うので、少し怖いですね。
ただ、当たり前ですがライブラリをアップデートすると古いバージョンのオフラインキャッシュは消えますし、使うライブラリも常に増えていくという訳ではないので、そんなに問題になることはないのかなーと。
- オフラインキャッシュを生成した環境によって、どうもchecksumが変わる問題があるっぽい
上記のissueの通りなのですが、オフラインキャッシュを生成した環境によってキャッシュのchecksumが変わるかもしれない問題があり(実際遭遇しました)、これを回避するにはリポジトリにオフラインキャッシュを入れるのが手取り早い解決策になってしまいます。そのため、オフラインキャッシュをリポジトリに含めざるを得なくなることがままあるだろうな、というのがデメリットですね。
ignoreの設定
yarn v2は.yarn
以下に色々ファイルを生成するので、何をignoreするのかが結構難しいと思います。
ここに公式の設定が有るので利用すると良いと思います。(node_modules
を使う場合はオフラインキャッシュをignoreするような設定になってますが、ignoreしなくても動作に影響は無いです)
補足
yarn v2でモノレポをやるに当たって、実運用するなら知っておいたほうが良いことを補足します。
node_modules/.bin
を参照する
yarn v2でルートのyarn v2では各workspaceからルートのnode_modules/.bin
を参照することは出来ません。(意図した仕様のようです)
そのため、例えばルートにjest/eslint/prettierを依存として追加し、各workspaceからはそれをlint: "eslint src ..."
みたいにnpm scriptsで参照するというのは少しトリックが必要になります。
上記のissueにある通りにやれば大丈夫なのですが、例としてルートのeslintを各workspaceから参照させる設定方法を書きます。
- ルートのpackage.jsonに
g:eslint
というnpm scriptsを追加する。
...
"scripts": {
"g:eslint": "cd $INIT_CWD && eslint"
}
...
- 各workspaceでは、
g:eslint
を使ってeslintを起動する
...
"scripts": {
"lint": "yarn g:eslint ..."
}
...
これで良い感じにルートのeslintを使うことが出来るようになります。(jestもprettierなども同様の手順で実行可能です)
至極簡単にこのトリックの説明をしておくと、ルートのnode_modules/.bin
は各workspaceからは見えないが、ルートのnpm scriptsは各workspaceから見えるという仕組みを使ってます。($INIT_CWD
は組み込みの変数で、実行時のカレントディレクトリを保持しています。つまり各workspaceのディレクトリを保持しているわけですね)
依存ライブラリの自動更新
Githubならdependabotで依存ライブラリの自動更新を行っている人が多いと思うのですが、dependabotはyarn v2に対応していません。(2021年5月16日時点)
なので、代わりにRenovateを使います。
Github Appsとして導入でき、かつ無料なのでこちらで問題ない場合が多いのではないかと思います。
リポジトリの設定の共通化
eslint
eslintはルートの設定に対して、個々のリポジトリの設定がディープマージされるという仕様になっているのでモノレポと非常に相性が良いです。ルートに共通設定をまとめて差分だけを個々のパッケージに置きましょう。
jest
ルートに共通の設定を生成する関数を置いて、個々のパッケージではそれをrequireで読み込んで使うというやり方を取ってます。
- ルート: jest.config.base.js
module.exports = (...) => {
return {
...
};
};
- 個々のパッケージ: jest.config.js
const config = require('../../jest.config.base.js');
module.exports = config(...);
webpack
jestと基本的に同じ。
tsconfig
tsconfigには継承という機能があるので、モノレポのルートに共通tsconfig設定を定義して、個々のリポジトリでそれを継承します。
- ルート: tsconfig.base.json
{
...
}
- 個々のパッケージ: tsconfig.json
{
"extends": "../../tsconfig.base.json",
...
}
prettier
prettierの設定はもともと各リポジトリで共通なので、ルートに1つ用意して終わりにしました。各パッケージで差分がある場合でも、jest/webpackのような方法をとれば自由自在に設定できると思います。
VSCode
VSCodeにはワークスペース機能というのものがあり(yarn workspaceとは別物)、これを使うと非常にyarn workspaceとかみ合った運用を行うことができます。
ルートに<任意の名称>.code-workspace
というファイルを作成します。
{
// ワークスペースのフォルダとして扱うディレクトリを指定する
// nameはなんでもよく、サイドバーにそのまま表示されるのでわかりやすい名前を書くといいかも
"folders": [
{
"name": "project-root",
"path": "."
},
{
"name": "API / HogeHoge",
"path": "packages/hogehoge"
}
...
],
// 各フォルダで適用される共通設定
"settings": {
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
...
},
// .vscode/extentions.jsonと同じことを書ける場所
"extensions": {
"recommendations": [
...
]
}
}
このファイル、通常は.vscode/
以下に作成するファイルの集合体みたいな存在になっており、ワークスペース内の各フォルダーの指定や共通設定、お勧め拡張機能の指定などができます。
まとめ
書いてみて思ったのですがほぼほぼyarn v2の設定の話に費やしてしまいました。実際Node.jsでモノレポをやる場合は、このパッケージマネージャーをどう選ぶか(npm/yarn/pnpm)、どう設定するかという問題が一番大きいのかなと思います。
今回npm workspaceやpnpm workspaceも試したのですが、serverless offlineが謎のout of memoryエラーで落ちたりして、最終的にyarn v1でnoHoist: "*"
をするか、yarn v2を使うかという状況になっていました。最初yarn v2はやはりPlug'n'Play
の印象が強くあまり乗り気でなかったのですが、nodeLinker: node-modules
の設定を知りこれなら行けるかもと思っていざ導入してみると、オフラインキャッシュは非常に快適でyarn v1の手触りのまま諸々が改善された状態を得ることが出来たと思ってます(yarn v1はもともとinstallが早かったですが、yarn v2はそれをさらに上回ってる感じがします)
プロジェクトによって最適なパッケージマネージャーの選択は変わると思いますが、yarn v2でモノレポを組み立てようとしてる方に参考になれば幸いです。
Discussion