thumbnail

※本ブログの目的と内容1、読者の方へ2

前提

  • functionsのみのモノレポ化を対象としている
  • hostingはモノレポ化の対象外(当方はvercelを利用している)

デプロイフロー

Firebase Functionsモノレポの問題点

Firebase Functionsのデプロイフロー(仕様)

  1. 対象ディレクトリ( apps/functions )をサーバにアップロード
  2. アップロードされたディレクトリで npm install を実行

問題点

npm install 時に、ローカルパッケージ( packages/domain )を見つけられずにエラーになる。

解決策

バンドルを組み合わせたデプロイフロー

  1. functionsのビルド&バンドルをデプロイ前に行う
  2. 対象ディレクトリ( apps/functions )をサーバにアップロード
  3. アップロードされたディレクトリで 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

  1. 通常通りにローカルパッケージからインポートする
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

  1. tsup でビルド&バンドルを行う
  2. ローカルパッケージへの依存を明記しない
{
  "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

  1. domainパッケージへの依存はパスエイリアスで解決する
{
  "compilerOptions": {
    "paths": {
      "@repo/domain": ["../../packages/domain/src"]  // <-------------------- 1
    }
  }
}

functions/tsup.config.ts

  1. バンドルサイズ削減とビルド短縮のため基本的に除外し、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

  1. ルートの turbo.json を継承する
  2. domainへの依存を明示する
{
  "extends": ["//"], // <---------------------------------------------------- 1
  "tasks": {
    "build": {
      "dependsOn": ["@repo/domain#build"] // <------------------------------- 2
    }
  }
}

Turborepoは package.jsondependenciesdevDependencies に記載された情報から依存関係を自動検知する。 今回の場合、 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

  1. rootからの相対パスでfunctionsを指定する
  2. デプロイ前にfunctionsのビルド( pnpm build:functions )を実行する
{
  "functions": [
    {
      "source": "apps/functions", // <--------------------------------------- 1
      "predeploy": "pnpm build:functions" // <------------------------------- 2
    }
  ]
}

メリット・デメリット

メリット

  • Firebaseの問題をfunctionsディレクトリ内で完結して解消している
    • domainやrootの設定・コードに変更が不要(汚染されない)
  • バンドルの設定がシンプル
    • 複雑な設定・構成を必要としない
  • ソースコード( functions/src/index.ts )内でローカルパッケージをインポートできる
    • ローカルパッケージへのジャンプも機能し、開発体験を損なわない(その逆も可能)

デメリット

  • 今のところ見つかっていない
  1. 本ブログは「技術的自由研究の備忘録」を目的としている。ソースコードは GitHubリポジトリ に公開している 

  2. お気づきの点や改善案があれば、遠慮なくお知らせいただきたい。ご意見やご感想を歓迎します