Turborepoで構築するFirebaseモノレポ環境
前提
- functionsのみのモノレポ化を対象としている
- hostingはモノレポ化の対象外(当方はvercelを利用している)
デプロイフロー
Firebase Functionsモノレポの問題点
Firebase Functionsのデプロイフロー(仕様)
- 対象ディレクトリ(
apps/functions
)をサーバにアップロード - アップロードされたディレクトリで
npm install
を実行
問題点
npm install
時に、ローカルパッケージ( packages/domain
)を見つけられずにエラーになる。
解決策
バンドルを組み合わせたデプロイフロー
- functionsのビルド&バンドルをデプロイ前に行う
- 対象ディレクトリ(
apps/functions
)をサーバにアップロード - アップロードされたディレクトリで
npm install
を実行
ポイント
ローカルパッケージへの依存をバンドルにより事前に解決する。
構成
⏺ root/
├── 📁 apps/
│ ├── 📁 web/ # Next.js
│ │
│ └── 📁 functions/ # Firebase Cloud Functions
│ ├── 📁 src/
│ │ └── 📄 index.ts
│ ├── 📄 package.json
│ ├── 📄 tsconfig.json
│ ├── 📄 tsup.config.ts
│ └── 📄 turbo.json
│
├── 📁 packages/
│ └── 📁 domain/ # ビジネスロジック
│ ├── 📁 src/
│ │ └── 📄 index.ts
│ ├── 📄 package.json
│ └── 📄 tsconfig.json
│
├── 📄 package.json # ワークスペース設定
├── 📄 turbo.json # Turborepo設定
└── 📄 firebase.json # Firebase設定
設定
モノレポ化にあたって、重要なポイントのみ説明する。 フルコードは、GitHubリポジトリを参照のこと。
apps/functions
functions/src/index.ts
- 通常通りにローカルパッケージからインポートする
import { logSampleMessage } from "@repo/domain"; // <------------------------ 1
import { onRequest } from "firebase-functions/v2/https";
export const helloWorld = onRequest((request, response) => {
logSampleMessage();
response.send("Hello from Firebase! by turbo! with bundle!");
});
functions/package.json
tsup
でビルド&バンドルを行う- ローカルパッケージへの依存を明記しない
{
"scripts": {
"build": "tsup" // <----------------------------------------------------- 1
},
"dependencies": { // <----------------------------------------------------- 2
"firebase-admin": "^12.6.0",
"firebase-functions": "^6.0.1"
},
"devDependencies": { // <-------------------------------------------------- 2
"firebase-functions-test": "^3.1.0",
"tsup": "^8.4.0",
"typescript": "^4.9.0"
}
}
domainパッケージへの依存を明記すると、Firebase側のインストールでエラーが発生してしまう。 ローカルビルド時にバンドルすることでエラーを避ける。
functions/tsconfig.json
- domainパッケージへの依存はパスエイリアスで解決する
{
"compilerOptions": {
"paths": {
"@repo/domain": ["../../packages/domain/src"] // <-------------------- 1
}
}
}
functions/tsup.config.ts
- バンドルサイズ削減とビルド短縮のため基本的に除外し、Firebase側のインストールに任せる
import * as fs from "node:fs";
const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8")); // <------ 1
export default defineConfig({
bundle: true,
external: [...Object.keys(pkg.dependencies || {})], // <------------------- 1
});
functions/turbo.json
- ルートの
turbo.json
を継承する - domainへの依存を明示する
{
"extends": ["//"], // <---------------------------------------------------- 1
"tasks": {
"build": {
"dependsOn": ["@repo/domain#build"] // <------------------------------- 2
}
}
}
Turborepoは package.json
の dependencies
と devDependencies
に記載された情報から依存関係を自動検知する。
今回の場合、 package.json
でdomainパッケージへの依存を明記していないため、Turborepoには依存関係が認識されない。
この状態で turbo run build --filter=functions...
を実行しても、domainパッケージのビルドは行われない。
そこで turbo.json
にdomainパッケージへの依存を明示することで、functionsのビルド前にdomainパッケージのビルドを強要する。
packages/domain
Firebaseを意識した特別な設定はなし。
domain/src/index.ts
export function logSampleMessage(): void {
console.log("Hello from domain package! with bundle!");
}
root
package.json
Firebaseを意識した特別な設定はなし。
{
"scripts": {
"build:functions": "turbo run build --filter=functions...",
"deploy:functions": "firebase deploy --only functions"
}
}
turbo.json
Firebaseを意識した特別な設定はなし。
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": ["dist/**"]
}
}
}
firebase.json
- rootからの相対パスでfunctionsを指定する
- デプロイ前にfunctionsのビルド(
pnpm build:functions
)を実行する
{
"functions": [
{
"source": "apps/functions", // <--------------------------------------- 1
"predeploy": "pnpm build:functions" // <------------------------------- 2
}
]
}
メリット・デメリット
メリット
- Firebaseの問題をfunctionsディレクトリ内で完結して解消している
- domainやrootの設定・コードに変更が不要(汚染されない)
- バンドルの設定がシンプル
- 複雑な設定・構成を必要としない
- ソースコード(
functions/src/index.ts
)内でローカルパッケージをインポートできる- ローカルパッケージへのジャンプも機能し、開発体験を損なわない(その逆も可能)
デメリット
- 今のところ見つかっていない
-
本ブログは「技術的自由研究の備忘録」を目的としている。ソースコードは GitHubリポジトリ に公開している ↩
-
お気づきの点や改善案があれば、遠慮なくお知らせいただきたい。ご意見やご感想を歓迎します ↩