NxでFirebase Functionsのアプリケーションを作成する その1

投稿日
NxでFirebase Functionsのアプリケーションを作成する その1

Nxを使っているプロジェクトでFirebase Functionsを使ってバックエンドを構築する事になりました。



NxにはFirebase Functionsを簡単に作成するためのテンプレートが用意されていないため、

自分で設定する必要があります。


この記事ではNxを使ってFirebase Functionsのアプリケーションを設定する方法を記載します。





ちなみに、理解するのが難しかったため細かめに記載してます…!!

(ドキュメントあんまりないし難しいし、半べそかきそうになってました😂)



この記事でFirebaseへのデプロイまで記載しようとしたのですが、

長くなるため前後編になっております。



後編はこちら

検証した環境

1 nx 14.6.3
2 firebase-admin 11.0.1
3 firebase-functions 3.23.0


Nxで実行するための下準備

firebase init 実行

monorepoのrootでfirebase init

$ firebase init


  • ESLintはNxのものがあるため nを選択
  • install dependenciesは後ほどNxワークスペース機能を利用するため nを選択
firebase initした時の設定

ここまでで以下のようなファイルが作成されます

.
├── .firebaserc
├── firebase.json
├── functions
│   ├── package.json
│   ├── src
│   │   └── index.ts
│   └── tsconfig.json


Nxのアプリケーションを作成

Nxにはfirebaseを作るようなテンプレートの用意がないため、

かわりにNxでnodeのアプリケーションを作成出来るようにします。

$ yarn add -D @nrwl/node



Nxでnodeのアプリケーションを作成します。


作成時に名前を聞かれるので私は backend としました

$ npx nx generate @nrwl/node:application
 
>  NX  Generating @nrwl/node:application
 
? What name would you like to use for the node application? ›
・・・

apps/backend/ 内に様々なファイルが作成されます



functionsを実行するためのファイルを用意

Nxのコマンドを使ってfirebase functionsの処理を行いたいので、

最終的に実行される apps/backend/src/main.ts を編集します

apps/backend/src/main.ts
import * as functions from 'firebase-functions'
 
// // Start writing Firebase Functions
// // https://firebase.google.com/docs/functions/typescript
//
export const helloWorld = functions.https.onRequest((request, response) => {
  functions.logger.info('Hello logs!', { structuredData: true })
  response.send('Hello from Firebase!')
})

↑は firebase initで functions/src/index.ts に書き出された内容をまるっとコピーしたものです



firebase関係のパッケージを追加

下準備の最後に firebase init 時に install dependenciesでnを選択したため、

firebase関連のパッケージが入っていないのでインストールします。

$ yarn add firebase-admin firebase-functions



Nxでfirebaseの処理を実行出来るようにする

ここからいよいよNxのアプリケーションでFirebase Functionsを実行出来るようにしていきます!

前提

Firebase Functionsを実行する上で確実に必要なnpm-scriptsは以下の2つ


  • 開発時に必要
    • serve
  • デプロイ時に必要
    • deploy
functions/package.json
{
  "scripts": {
    "build": "tsc",
    "build:watch": "tsc --watch",
    "serve": "npm run build && firebase emulators:start --only functions",
    "shell": "npm run build && firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "engines": {
    "node": "16"
  },
  "main": "lib/index.js",
  ・・・
}

まずserveコマンドを使えるようにします。

(この記事では長くなるのでserveコマンドのみの紹介です)



serveを実行出来るようにする

最終的にnx run backend:serveコマンドを実行した際にFirebase Functionsのサーバがローカルで立ち上がるようにします。

最終的に nx run backend:serve でFirebase Functionsのサーバがローカルで立ち上がるようにする



npm-scriptsのserveについて

Firebase Functionsが自動で作成したnpm-scriptsのserveコマンドを見てみると

functions/package.json
"scripts": {
  ・・・
  "build": "tsc",
  "serve": "npm run build && firebase emulators:start --only functions",
  ・・・
  1. tscでビルドする(デフォルトだと functions/lib/ 配下に書き出される)
  2. firebase emulators:start --only functions を実行する

という流れになっているのが分かります。



nxで再現する

nx run backend:serveで上記の流れを再現していきます。



nxのコマンドは、それぞれのアプリケーション内のproject.jsonファイルで管理します

backend/project.json
{
  ・・・
  "targets": {
    "build": {
      "executor": "@nrwl/node:webpack",
      "outputs": ["{options.outputPath}"],
      "options": {
        "outputPath": "dist/apps/backend",
      ・・・
    },
    "serve": {
      "executor": "@nrwl/node:node",
      "options": {
        "buildTarget": "backend:build"
      },
      ・・・

targets内のキー名がコマンド名になるため、

buildコマンド・serveコマンドが用意されている事が分かります!




試しにnpx nx run backend:buildを実行すると

  • apps/backend/src/main.ts → dist/apps/backend/main.js

に変換してくれる(ビルドしてくれる)事を確認出来ます。



つまり

  • npx nx run backend:buildを実行(dist/apps/backend/main.js を作成)
  • firebase emulators:start --only functionsでビルドされたmain.jsを読み込む

という流れが組めれば、

Firebase Functionsをローカルで実行出来そうです。




project.jsonを編集します

backend/project.json
{
  ・・・
  "targets": {
    "build": {
      ・・・
    },
    "serve": {
      "builder": "@nrwl/workspace:run-commands",
      "options": {
        "commands": [
          {
            "command":"nx run backend:build"
          },
          {
            "command":"firebase emulators:start --only functions --inspect-functions"
          }
        ],
        "parallel": false
      }
    },

この記述は下記と同じ意味合いになります

$ nx run backend:build && firebase emulators:start --only functions --inspect-functions



これでnx run backend:serveを実行してみます。

$ npx nx run backend:serve
> nx run backend:serve
・・・
i  functions: Watching "/Users/yuu/src/github.com/yuki-takara/<project-name>/functions" for Cloud Functions...
i  emulators: Shutting down emulators.
・・・
エミュレータが監視しているフォルダが \<プロジェクトroot\>/functions/ になっている

私の環境だとまずfunctionsのエミュレータ起動で失敗しますが、


それ以上に重要なのが上記画像の赤枠で囲った箇所の通り

Firebase Functionsのエミュレータが監視しているフォルダが 「<プロジェクトroot>/functions/」 という事ですね。


今回はビルド先が「<プロジェクトroot>/dist/apps/backend/」なのでここを読み込むようにします。





functionsの読み込み先を変更する

functionsの読み込み先変更はシンプルで、

firebase init時に作成された 「firebase.json」のsourceを指定する事で実現します。

firebase.json
{
  "functions": {
    "source": "dist/apps/backend"
  }
}

sourceを設定する際、

firebase.jsonに最初から記述されていた predeploy も削除して問題なしです。



ただし、ここまでだとまだエミュレータの起動が出来ません。

sourceで読み込み先フォルダを変更しても起動に失敗。。

先ほどまでとは違うエラーメッセージなので、

挙動が変わったのは分かりつつも--verboseしても何しても詳細が分からない。。😭



ここでかなりつまったものの、最終的に 「package.json」が足りない、という事が分かりました!





package.jsonを動的に作成する

ごにょごにょやっているうちに

firebase emulators:start --only functions は対象ディレクトリのpackage.jsonのmainに設定したjsファイルを実行している事が分かりました!


firebase.json
{
  "functions": {
    "source": "dist/apps/backend"
  }
}
dist/apps/backend/package.json
{
  ・・・
  "main": "main.js",
  ・・・

と記載すればエミュレータが「dist/apps/backend/main.js」を参照してくれる、という事です。





dist/apps/backend/にpackage.jsonを用意するために、

以下の記事を参考にscriptファイルを用意します。

【Nx+Angular+NestJS+Firebase】15分でフルスタックPWA環境を構築しデプロイする - Qiita



scriptファイルの作成先はNxが最初から用意してくれる tools フォルダに作成していきます


tools/scripts/generate-api-package-json.ts
import { readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
import * as fs from 'fs'
 
async function main() {
  const BASE_PATH = join(__dirname, '..', '..')
  const srcDir = BASE_PATH
  const distDir = join(BASE_PATH, 'dist/apps/backend')
 
  const src = readFileSync(join(srcDir, 'package.json')).toString()
  const dist = convertPackageJson(src)
 
  if (!fs.existsSync(distDir)) fs.mkdirSync(distDir, { recursive: true })
  writeFileSync(join(distDir, 'package.json'), dist)
}
 
function convertPackageJson(src: string): string {
  const packageJson = JSON.parse(src)
  delete packageJson['scripts']
  delete packageJson['devDependencies']
  packageJson['main'] = 'main.js'
  packageJson['engines'] = { node: '16' }
  return JSON.stringify(packageJson, null, '\t')
}
 
main().then()

このscriptは以下の事を実行します

  • ./package.json を元に dist/apps/backend/package.json を作成する
  • その際に元となるpackage.jsonで不要な項目は削除、必要な項目を改めて追加


後はこれをnx run backend:serveで実行すればよいのでproject.jsonを編集します

backend/project.json
{
  ・・・
  "targets": {
    "build": {
      ・・・
    },
    "serve": {
      "builder": "@nrwl/workspace:run-commands",
      "options": {
        "commands": [
          {
            "command":"nx run backend:build"
          },
          {
            "command":"ts-node ./tools/scripts/generate-api-package-json.ts"
          },
          {
            "command":"firebase emulators:start --only functions --inspect-functions"
          }
        ],
        "parallel": false
      }
    },
  1. ビルドする(dist/apps/backend/ 作成)
  2. dist/apps/backend/package.json 作成
  3. functions起動

の流れになります。



これでnx run backend:serveを実行すると

nx run backend:serve でFirebase Functionsのサーバがローカルで立ち上がるように!

Firebase Functionsが起動出来ました!



そして赤枠で囲ったURLにアクセスしてみると

apps/backend/src/main.ts に記載した内容がブラウザで表示された

無事 apps/backend/src/main.ts に記載した内容が表示されています。


これで開発が進められますね☺️





Firebase Functionsにデプロイする

記事が長くなったので次の記事で記載していきます!

プロフィール画像
Yuki Takara
都内でフリーランスのエンジニアをやってます。フロントとアプリ開発メインに幅広くやってます。