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

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

Nxを使ってFirebase Functionsのアプリケーションを設定する方法その2です。




その1では yarn run serve を実行出来るようになったので、

この記事ではFirebase Functionsにデプロイするところまで記載していきます。



その1の記事内容を実装している前提で記載をしていきます

検証した環境

1 nx 14.6.3
2 firebase-functions 3.23.0
3 depcheck 1.4.3



Firebase Functionsにデプロイ出来るようにする

firease initで作成されるpackage.jsonは以下のようになっています

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",
  ・・・
}

この中のdeployコマンドをNxのアプリケーションで実行出来るようにしていきます。



deployは

  • firebase deploy --only functions

を実行出来れば良い事が分かります。



が、1つ落とし穴があり

デプロイを実行する際にpredeployでbuildも実行しています。

firebase.json
{
  "functions": {
    "predeploy": "npm --prefix \"$RESOURCE_DIR\" run build"
  }
}

そのため

  • yarn run build
  • firebase deploy --only functions

を実行出来れば良いはず。



nx run backend:build・serveを変更する

デプロイをする際もビルドを実行する必要がある、ということが分かりました。


そのため nx run backend:buildを共通化します。



やる事としては nx run backend:serveで実行していた内容の一部をnx run backend:buildで行えるようにします。



変更前

apps/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
    }
  },

変更後

apps/backend/project.json
"targets": {
  "build-node": {  //変更前のbuildをリネームしてbuild-nodeに
    ・・・
  },
  "build": {
    "builder": "@nrwl/workspace:run-commands",
    "options": {
      // build-nodeを実行後、serveで実行していたpackage.jsonの書き出しを行う
      "commands": [
        {
          "command":"nx run backend:build-node"
        },
        {
          "command":"ts-node ./tools/scripts/generate-api-package-json.ts"
        }
      ],
      "parallel": false
    }
  },
  "serve": {
    "builder": "@nrwl/workspace:run-commands",
    "options": {
      "commands": [
        {
          "command":"nx run backend:build"
        },
        {
          "command":"firebase emulators:start --only functions --inspect-functions"
        }
      ],
      "parallel": false
    }
  },

ポイントは

  • 既存のbuild → build-node に変更
  • 新たに追加した build に serve で実行していたpackage.jsonをコピーするスクリプトを移動

記事を記載する上でjsonファイルにコメントをしてますが

jsonはコメントが出来ないファイル形式のため注意です




deployコマンドを追加

nx run backend:buildは共通化出来たので、

deployコマンドをpjoject.jsonに追加します

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



これで npx nx run backend:deploy を実行してみると

$ npx nx run backend:deploy

> nx run backend:deploy
・・・
Error: There was an error deploying functions

 >  NX   ERROR: Something went wrong in run-commands - Command failed: firebase deploy --only functions

エラーが起きます🤔




今回私がFirebase Functionsを導入しようとしているmonorepoプロジェクトは、

フロント側にNext.jsやMaterial-UIを使用しています。


エラー内容を見るとそれらのパッケージが悪さをしているように見えます。




この一覧の流れを見る限り、

Nx + Firebase Functionsのデプロイの動きとしては

  1. buildをする(dist/apps/backend/にmain.jsを書き出す)
  2. ルートのpackage.jsonを元に dist/apps/backend/package.json を作成
  3. dist/apps/backend/package.json 内の記載されているパッケージを install
  4. Firebaseにデプロイ

という手順を踏んでいて、

今回のエラーは「3.」の部分でエラーになったように見受けられます。





コピーする際のpackage.jsonを調整する

そこで上記手順「2.」のdist/apps/backend/package.json を作成する際に、

記載するパッケージの内容を調整します。


最終的に「apps/backend」で利用しているパッケージさえ読み込めればよいはず。




まず depcheck というパッケージをインストール

$ yarn add -D depcheck

これは指定したフォルダやファイルで使用しているパッケージのみを選別してくれるライブラリです。




次にこのライブラリを利用しつつ

「tools/scripts/generate-api-package-json.ts」を変更していきます。


変更時の内容は以下の記事を参考にさせていただきましたmm

Nx で Fireabase Functions を扱う方法 | ティーポットは珈琲を淹れられない

tools/scripts/generate-api-package-json.ts
import { readFileSync } from 'fs'
import { join } from 'path'
//@ts-ignore
import depcheck from 'depcheck'
import * as fs from 'fs'
import * as path from 'path'

const PACKAGE_JSON_TEMPLATE = {
  engines: { node: '16' },
  main: 'main.js',
}

async function main(): Promise<void> {
  const APPLICATION_NAME = 'backend'
  console.log(`Application name: ${APPLICATION_NAME}`)

  /*****************************************************************************
   * package.json
   * - Filter unused dependencies.
   * - Write custom package.json to the dist directory.
   ****************************************************************************/
  const ROOT_PATH = path.resolve(__dirname + '/../..')
  const DIST_PROJECT_PATH = `${ROOT_PATH}/dist/apps/${APPLICATION_NAME}`

  console.log('Creating cloud functions package.json file...')

  const src = readFileSync(join(ROOT_PATH, 'package.json')).toString()
  const packageJson = JSON.parse(src)

  // Get unused dependencies
  const { dependencies: unusedDependencies } = await depcheck(DIST_PROJECT_PATH, {
    package: {
      dependencies: packageJson.dependencies,
    },
  })

  // Filter dependencies
  const requiredDependencies = Object.entries(packageJson.dependencies as { [key: string]: string })
    ?.filter(([key, _value]) => !unusedDependencies?.includes(key))
    ?.reduce<{ [key: string]: string }>((previousValue, [key, value]) => {
      previousValue[key] = value
      return previousValue
    }, {})

  console.log(`Unused dependencies count: ${unusedDependencies?.length}`)
  console.log(`Required dependencies count: ${Object.values(requiredDependencies)?.length}`)

  // Write custom package.json to the dist directory
  await fs.promises.mkdir(path.dirname(DIST_PROJECT_PATH), { recursive: true })
  await fs.promises.writeFile(
    `${DIST_PROJECT_PATH}/package.json`,
    JSON.stringify(
      {
        ...PACKAGE_JSON_TEMPLATE,
        dependencies: requiredDependencies,
      },
      undefined,
      2
    )
  )

  console.log(`Written successfully: ${DIST_PROJECT_PATH}/package.json`)
}

main().then()

3,4行目の@ts-ignoreはimport時に以下のようなtsエラーが発生するため、

サッと回避するために使用

can only be default-imported using the 'esModuleInterop' flag



これで改めてデプロイを実行してみると

$ npx nx run backend:deploy

> nx run backend:deploy
・・・
i  functions: updating Node.js 16 function helloWorld(us-central1)...
✔  functions[helloWorld(us-central1)] Successful update operation.
Function URL (helloWorld(us-central1)): https://us-central1-<プロジェクトID>.cloudfunctions.net/helloWorld
i  functions: cleaning up build files...

✔  Deploy complete!

デプロイが成功しました!



Firebaseのページを見てもデプロイされている事が確認出来ます。





余談: 書き出されたpackage.jsonについて

tools/scripts/generate-api-package-json.ts変更後の書き出されたpackage.jsonは



変更前

dist/apps/backend/package.json
{
    "version": "0.1.0",
    "license": "MIT",
    "private": true,
    "dependencies": {
        "@apollo/client": "^3.6.9",
        "@emotion/react": "^11.10.4",
    ・・・
    },
    "main": "main.js",
    "engines": {
        "node": "16"
    }
}

変更後

dist/apps/backend/package.json
{
  "engines": {
    "node": "16"
  },
  "main": "main.js",
  "dependencies": {
    "firebase-functions": "^3.23.0"
  }
}

とてもシンプルでスッキリしました!

おすすめ