LoginSignup
1
2

firebaseプロジェクトでmonorepo構成(npm workspace)

Last updated at Posted at 2023-07-17

一般的なfirebaseプロジェクトでのmonorepo構成を作ってみます。
いきなり全体を作らず、順に構成していきます。

以下の4つを達成したらゴールとします。

  • 共通コードを読み込んだ状態でreactのローカル環境が動く
  • firebase hostingのデプロイが完了し期待通りの出力結果
  • 共通コードを読み込んだ状態でfirebase functionsのローカル環境が動く
  • firebase functionsのデプロイが完了し期待通りの出力結果

Reactのテンプレート作成

npx create-react-app my-monorepo --template typescript

作成すると以下の構成になります。

├── package-lock.json
├── package.json
├── src
│   └── App.tsx
└── tsconfig.json

packages/commonを作成

npm init -w packages/common

とりあえず質問は全てEnterでOK。

.
├── package-lock.json
├── package.json
├── packages
│   └── common
│       └── package.json
├── src
│   └── App.tsx
└── tsconfig.json

cracoでwebpackの設定をカスタマイズ

srcディレクトリ以外でTypeScriptファイルをJavaScriptにトランスパイルせずに読み込みたい場合、それを可能にするためにはwebpackの設定をカスタマイズします。そのためcracoを追加します。

npm i -D @craco/craco
touch craco.config.js

共通関数を作成

npm install -w packages/common date-fns
packages/common/index.ts
import { format } from 'date-fns'

export function formatDate(date: Date) {
  return format(date, 'yyyy-MM-dd HH:mm')
}
App.tsx
import { formatDate } from 'common'
// 省略
<p>現在の時間: {formatDate(new Date())}</p>

ここまでで1つゴールが達成できました。

  • 共通コードを読み込んだ状態でreactのローカル環境が動く

firebase hostingにアップロード

ローカル環境では動くことが確認できたのでfirebase hostingにアップロードしてみます。

firebase init

デフォルトで選択をすすめると以下のような構成になります。

├── craco.config.js
├── firebase.json
├── functions
│   ├── package.json
│   ├── src
│   │   └── index.ts
│   └── tsconfig.json
├── package-lock.json
├── package.json
├── packages
│   └── common
│       ├── index.ts
│       └── package.json
├── src
│   └── App.tsx
└── tsconfig.json

hostingのデプロイ用の設定

firebase.jsonをreact-create-appに合わせる。

firebase.json
  "hosting": {
    "public": "build",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }

package.jsonにスクリプトを追加して、

package.json
  "scripts": {
    "deploy:hosting": "npm run build && firebase deploy --only hosting"
  }

実行すればデプロイは完了します。

npm run deploy:hosting
  • firebase hostingのデプロイが完了し期待通りの出力結果

functions側の設定

functionsをpackages/functionsに移動します。

├── craco.config.js
├── firebase.json
├── package-lock.json
├── package.json
├── packages
│   ├── common
│   │   ├── index.ts
│   │   └── package.json
│   └── functions
│       ├── package.json
│       ├── src
│       │   └── index.ts
│       └── tsconfig.json
├── src
│   └── App.tsx
└── tsconfig.json
firebase.json
- "source": "functions",
+ "source": "packages/functions",
package.json
  "workspaces": [
    "packages/common",
    "packages/functions"
  ]

見通しをよくするため、今回使わないスクリプト、依存関係をpackage.jsonから一度削除します。

packages/functions/package.json
{
  "name": "functions",
  "scripts": {
    "build": "tsc",
    "serve": "npm run build && firebase emulators:start --only functions",
    "shell": "npm run build && firebase functions:shell",
    "deploy": "firebase deploy --only functions"
  },
  "engines": {
    "node": "16"
  },
  "main": "lib/index.js",
  "private": true
}

改めて必要なパッケージをpackages/functionsにインストールします。

npm install -w packages/functions firebase-admin firebase-functions

serve:functionsのスクリプトを追加します。

package.json
  "scripts": {
    "start": "craco start",
    ... 
    "deploy:hosting": "npm run build && firebase deploy --only hosting",
    "serve:functions": "npm run serve -w packages/functions"
  }

スクリプトを実行しAPIにアクセスすると、Hello from Firebase! が返ってきました。
とりあえずAPIが動くことは確認できました。

npm run serve:functions

functionsでcommonをimport

以下のコードを書き実行すると、エラーが出ます。

packages/functions/index.ts
import * as functions from "firebase-functions";
import { formatDate } from "common"

export const helloWorld = functions.https.onRequest((request, response) => {
  functions.logger.info("Hello logs!", {structuredData: true});
  response.send("Hello from Firebase!" + formatDate(new Date()));
});
functions: Failed to load function definition from source: FirebaseError: Failed to load function definition from source: Failed to generate manifest from function source: Error: Cannot find module '/my-monorepo/node_modules/common/index.js'. Please verify that the package.json has a valid "main" entry

エラーメッセージの通り、現在packages/commonにindex.jsはありません。

└── packages
    └── common
        ├── index.ts
        └── package.json

commonのmainをdist/index.jsにしてtsconfig.jsonも追加しbuildできるようにします。

packages/common/package.json
{
-  "main": "index.js"
+  "main": "dist/index.js"
}
packages/common/tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "outDir": "./dist",
  },
}

serveの前にcommonをbuildするようなスクリプトを追加します。(ここではpackはなくても動きますが、deploy時に必要なので追加しときます。)

package.json
  "scripts": {
    "prebuild:common": "cd packages/common && tsc && npm pack && mv *.tgz ../functions",
    "serve:functions": "npm run prebuild:common && npm run serve -w packages/functions",
  }

これで npm run serve:functions を実行すると先ほどのエラーは解消され動くようになりました。

  • 共通コードを読み込んだ状態でfirebase functionsのローカル環境が動く

functionsをdeploy

今回のようなfunctionsの下にないローカルのcommonパッケージを使いたい場合はnpm packする必要があります。
以下のignoreの通り、node_modulesのファイルはdeployされないため、サーバー側でnpm ciをしてモジュールをインストールします。

firebase.json
  "functions": [
    {
      "source": "packages/functions",
      "codebase": "default",
      "ignore": [
        "node_modules",
        ".git",
        "firebase-debug.log",
        "firebase-debug.*.log"
      ],
    }
  ],

サーバー側でcommonパッケージをインストールするには
functions下にnpm packしたファイルを置き、package.jsonにfile:common-1.0.0.tgzと指定することでサーバーがでもinstallが可能になります。

packages/functions/package.json
  "dependencies": {
    "common": "file:common-1.0.0.tgz",

predeployにprebuildとbuildコマンドを実行するようにして、firebase deploy --only functionsを叩けば無事デプロイができました。

firebase.json
  "functions": [
    {
      "source": "packages/functions",
      "predeploy": [
        "npm run prebuild:common",
        "npm run build -w functions",
      ]
    }
  ],

期待した出力も確認できました。

  • firebase functionsのデプロイが完了し期待通りの出力結果

predeploy, postdeploy

完成したかと思いきや、再度 serve:functions で実行するとpackages/commonのコードが反映されていませんでした。

node_modulesにdeploy時にインストールしたコードが残ったままになっているので、functionsではpackages/commonよりfunctions/node_modeusle/commonのコードが優先されてしまいます。

└── functions
    ├── common-1.0.0.tgz
    ├── firebase-debug.log
    ├── lib
    │   ├── index.js
    │   └── index.js.map
    ├── node_modules
    │   └── common
    │       ├── dist
    │       │   └── index.js
    │       ├── index.ts
    │       ├── package.json
    │       └── tsconfig.json

functionsのデプロイ時にのみpackとinstallは行えばよいので、firebase.jsonのpredeploypostdeployを使って以下のように書き換えました。

"functions": [
  ...,
  {
    "predeploy": [
      "npm run build -w packages/common",
      "npm pack -w packages/common && mv *.tgz packages/functions",
      "cd packages/functions && npm i ./common-1.0.0.tgz",
      "npm run build -w functions"
    ],
    "postdeploy": [
      "npm -w packages/functions remove common",
      "echo 'deploy completed!!'"
    ]
  }
],

その他

npmの不具合

npm packとnpm install *.tgz したときにnpm 8.19 より下のバージョンではインストールができない問題がありました。
npmのバージョンを8.19.2にしたことで正常にインストールできるようになりました。

npm packでdist以下のファイルが含まれない

.gitignoreに**/dist/*を書いていることでnpm packにdist/index.js以外が含まれませんでした。
空の.npmignoreファイルを追加することで回避できました。

さいごに

Reactのコードもpackages/frontのようにpackages以下に移動するのが理想かと思いますが、とりあえずゴールは達成できたので今回はここまでとします。

最後の方は少し説明が雑になったので、詳細は以下のリポジトリをみていただければと思います。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2