2022-6-3
Firebaseプロジェクトでモノレポ構成にする際のやり方
要約
- npm/yarnのworkspace機能はワークスペース毎にlockファイルが生成されないので使ってはいけない。 lerna等、各ワークスペースにlockファイルが生成される手法でモノレポを実現すること。
- 共通化したいコードはfunctionsパッケージに配置して、他パッケージはfunctionsパッケージを参照する。
詳細
前提の話
firebase-cli
で firebase deploy
コマンドを実行した際、Functionsがどのようにデプロイされるかはざっくり以下のようになっているはず。
- プロジェクトのfunctionsディレクトリ内のファイルがサーバーへ送信される。その際、
node_modules
は除外される。 - サーバーで
npm install
が実行され、必要なパッケージがインストールされる。 - なんやかんやあってFunctionsの実行準備が整う。
これはあくまで推測ではあるんだけど、以下のissueのコメントにも似たような内容が書いており、特に否定もされていないので恐らく正しいとは思う。
https://github.com/firebase/firebase-functions/issues/172#issuecomment-391967103
依存関係の扱い | Firebase Documentation
npm/yarnのworkspaceではワークスペース毎にlockファイルが生成されない問題
npm/yarnのworkspace機能はルートにしかlockファイルが生成されない。となると「前提の話」で書いた以下の部分が問題となる。
- プロジェクトのfunctionsディレクトリ内のファイルがサーバーへ送信される。その際、
node_modules
は除外される。- サーバーで
npm install
が実行され、必要なパッケージがインストールされる。
ワークスペースにはlockファイルが生成されないので、サーバーへ送信されるのはpackage.jsonのみとなり、その状態で npm install
されることとなる。となると、当然細かいバージョンがローカルやCIと異なってしまう。これを放置するとFirebaseだけで再現する不具合が発生する可能性が高く、非常にまずい。このことを知らずに遭遇したら相当解決に悩むかと思う。
npm/yarnのworkspace機能について調べたが、どうもワークスペース毎にlockファイルを生成する方法が見当たらず、Firebaseプロジェクトでの採用は諦めたほうが良さそう。 (yarn v2のissueでワークスペースごとのlockファイル生成機能が進行中っぽいが、いつ頃使えるようになりそうなのかはよくわからない)
lernaであればワークスペース毎にlockファイルが生成されるのでlernaを使うか、いっそこういうモノレポのための特別な物を使わないという選択もありだと思う。モノレポを採用する理由というのは同一リポジトリ内の他パッケージを参照したいというのがモチベーションだと思うが、それは npm install ../shared
のように相対パスでのインストールで事足りるからだ。
ルートにだけlockファイルがある状態だと本当にローカルとFirebaseでバージョン差異が発生するのか?
この「lockファイルがfunctionsディレクトリにないと、ローカルとFirebaseでバージョンの差異が発生するのでは?」という点に関して、本当に起こりうるのか検証を行った。
検証は以下の手順で行った。
- npmのworkspace機能を使ってリポジトリを作成。ワークスペースとして
packages/functions
を用意。 packages/functions
にてnpm install lodash@4.17.0
とし、lodashをインストール。これによりルートパッケージのlockファイルにもlodashに関する情報が書き込まれる。packages/functions
のpackage.jsonにて、"lodash": "4.17.0"
を"lodash": "^4.17.0"
と書き換えることで、新しいバージョンのパッケージがリリースされている状況を再現。lodash.VERSION
でlodashのバージョンが取得できるので、現在使用されているlodashのバージョンを取得するAPIを作成し、デプロイ。実際にFirebase上ではどのバージョンのlodashが使用されるのか確認する。
結果、ローカルでは 4.17.0
、Firebaseでは 4.17.21
が使用されていた。
というわけで、functionsディレクトリにlockファイルが無いとバージョンの差異が発生する可能性があるという懸念は正しいと思っていいのかなと。
functionsは同一リポジトリ内の他パッケージを参照してはいけない
前提に書いたとおり、サーバーへ送られるのはfunctionsディレクトリ内のファイルだけである。
なので、相対パスやシンボリックリンクでfunctionsディレクトリ外のパッケージを参照していると、デプロイ時に以下のようなエラーで落ちる。
npm ERR! 404 Not Found - GET https://registry.npmjs.org/@sample-firebase-project%!f(MISSING)shared - Not found
functionsディレクトリ外の物を参照できないのはどうしようもないので、共通化したいコードはfunctions内に配置して他パッケージはfunctionsを参照するというのが一番楽。楽ではあるけど全くスマートじゃないのでいいやり方が出てき次第ここに書くつもり。