一般的な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
import { format } from 'date-fns'
export function formatDate(date: Date) {
return format(date, 'yyyy-MM-dd HH:mm')
}
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に合わせる。
"hosting": {
"public": "build",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
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
- "source": "functions",
+ "source": "packages/functions",
"workspaces": [
"packages/common",
"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のスクリプトを追加します。
"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
以下のコードを書き実行すると、エラーが出ます。
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できるようにします。
{
- "main": "index.js"
+ "main": "dist/index.js"
}
{
"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時に必要なので追加しときます。)
"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をしてモジュールをインストールします。
"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が可能になります。
"dependencies": {
"common": "file:common-1.0.0.tgz",
predeployにprebuildとbuildコマンドを実行するようにして、firebase deploy --only functions
を叩けば無事デプロイができました。
"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のpredeploy
とpostdeploy
を使って以下のように書き換えました。
"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以下に移動するのが理想かと思いますが、とりあえずゴールは達成できたので今回はここまでとします。
最後の方は少し説明が雑になったので、詳細は以下のリポジトリをみていただければと思います。