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

## 概要
フルスタックPWAの環境構築を見直していたら、@nrwl/nxに出会ったので使ってみた。

15分くらいで環境構築からデプロイまでたどり着けるようになったのでワークフローを共有します。Schematics書けばもっと早くなりそう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/420158/33192283-487e-c1aa-8636-b27a67b3c390.png)

## 前提
- firebase-toolsの初期設定が完了している

## ワークフロー
- ① Nx Workspaceの作成(3分くらい)
- ② @angular/pwaのインストール(30秒くらい)
- ③ Firebaseプロジェクトの作成(30秒くらい)
- ④ @angular/fireのインストール(2分)
- ⑤ NestJSをCloud Functions for Firebaseに対応(4分くらい)
- ⑥ Firebaseのプラン変更(30秒くらい)
- ⑦ デプロイ(4分くらい)

## ① Nx Workspaceの作成
はじめに`create-nx-workspace`コマンドでNxのWorkspaceを作成する。
基本的にコマンドは`npx`を用いていく。

```sh
$ npx create-nx-workspace@latest pwa-sample \
--preset angular-nest --app-name ng-pwa-sample \
--style scss --linter eslint --nx-cloud false
```

作成が完了したら出来上がった`pwa-sample`ディレクトリに移動する。

```sh
$ cd pwa-sample
```


## ② @angular/pwaのインストール
`ng add`と同じように`nx add`を用いるとSchematicsが動くので、PWA対応のために`@angular/pwa`をインストールする。

```sh
$ npx nx add @angular/pwa
```

## ③ Firebaseプロジェクトの作成
`@angular/fire`をインストールする前に、Firebaseのプロジェクトを作っておく。

```sh
$ PROJECT_NAME=sample-pwa
$ PROJECT_ID=$PROJECT_NAME-$(printf %04d $(($RANDOM % 10000)))
// sample-pwa-0021
$ npx firebase projects:create $PROJECT_ID --display-name $PROJECT_NAME
```

## ④ @angular/fireのインストール
Firebaseプロジェクトを作成したら、`@angular/fire`をインストールする。

```sh
$ npx nx add @angular/fire
```

インストール時に使用するFirebaseのプロジェクトを聞かれるので先程作成したプロジェクトを選択する。

## ⑤ NestJSをCloud Functions for Firebaseに対応
### .firebasercの設定

Cloud Functionsをデプロイする際に必要になってくるので、下記コマンドで`.firebaserc`にデフォルトのプロジェクトを設定する

```sh
$ npx firebase use $PROJECT_ID --alias default
```

### 必要なパッケージのインストール
また、Cloud Functions for Firebaseに必要なパッケージをインストールする

```sh
$ npm install firebase-functions firebase-admin express
```

### Cloud FunctionsからNestJSを呼ぶ設定
`apps/api/src`に`main.prod.ts`を作成し、下記の内容を記載する。

```apps/api/src/main.prod.ts
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import * as express from 'express';
import * as functions from 'firebase-functions';
import { AppModule } from './app/app.module';

export async function bootstrap(expressInstance) {
  const app = await NestFactory.create(
    AppModule, 
    new ExpressAdapter(expressInstance)
  );
  const globalPrefix = 'api';
  app.setGlobalPrefix(globalPrefix);
  await app.init();
}

const expressServer = express();

export const api = functions.region('us-central1')
.https.onRequest(async (request, response) => {
    await bootstrap(expressServer);
    expressServer(request, response);
});
```

### デプロイ用の設定①(angular.jsonの編集)
`angular.json`に下記の通り追記する。
1つ目の追記は本番環境で`main.ts`の代わりに、`main.prod.ts`を用いる設定で、2つ目はapi(NestJS)のデプロイコマンドの設定。

```angular.json
{
  "version": 1,
  "projects": {
    ...,
    "api": {
      ...,
      "architect": {
        "build": {
          "configurations": {
            "production": {
              ...,
              "fileReplacements": [
                {
                  "replace": "apps/api/src/environments/environment.ts",
                  "with": "apps/api/src/environments/environment.prod.ts"
                },
+               {
+                 "replace": "apps/api/src/main.ts",
+                 "with": "apps/api/src/main.prod.ts"
+               }
              ]
            }
          }
        },
+       "deploy": {
+         "builder": "@nrwl/workspace:run-commands",
+         "options": {
+           "commands": [
+             {
+                 "command": "npx firebase deploy --only functions"
+             }
+           ]
+         }
+       }
      }
    }
  }
}
```

### デプロイの設定②(package.jsonの変換スクリプトの用意)

```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, '..', '..');
    console.debug('BASE_PATH:', BASE_PATH);
    const srcDir = BASE_PATH;
    const distDir = join(BASE_PATH, 'dist/apps/api');

    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': '12'};
    return JSON.stringify(packageJson, null , '\t');
}

main();
```

### デプロイの設定③(firebase.jsonの変換スクリプトの用意)

```firebase.json
{
  "hosting": {
     ...,
     "rewrites": [
+      {
+         "source": "/api/**",
+         "function": "api"
+      },
       {
         "source": "**",
         "destination": "/index.html"
       }
     ]
  },
+ "functions": {
+   "predeploy": [
+     "npx nx build api --prod",
+     "npx ts-node ./tools/scripts/generate-api-package-json.ts"
+   ],
+   "source": "dist/apps/api"
+ }
}
```
※ rewritesは上から処理されるので順番に注意！

1つ目の`hosting.rewrites`は`https://<projectId>.web.app/api/**`へのリクエストをCloudFunctionsの`api`関数宛のリクエストに書き換える設定。(サンプルでは`https://<projectId>.web.app/api/hello`でNestJSがJSONを返してくれる。)
2つ目の`functions`はCloud Functionsをデプロイするときの設定。

# ⑥ Firebaseのプラン変更
Cloud Functions for Firebaseを使うためにはBlazeプランにアップグレードしないと行けないので、Firebase Consoleの「Functions」タブにいって有効化する。

[Firebase Console](https://console.firebase.google.com/u/0/project/)


# ⑦ デプロイ
`nx`コマンドを使うとangular.jsonに書かれた設定に基づいてデプロイしてくれる。

```sh
// Deploy NestJS App
$ npx nx deploy api
// Deploy Angular App
$ npx nx deploy ng-pwa-sample 
```

# ⑧ ブラウザから確認
デプロイが終わるとFirebase Hostingのurlが表示されますのでアクセスして確認しましょう。
「Message」のところに`{"message": "Welcome to api!"}`と表示されていればAPIとの通信が成功しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/420158/0b0522e7-1462-bc66-74e9-edccdd8dd9f2.png)

# 今後の改善点
ワークフローの問題点としてはCloud FunctionsとNestJSをつなぐためだけにExpressを使っていることと、⑤の工程が複雑なことが挙げられます。前者はFastifyへの以降、後者はSchematicsでの自動化で対応できればと思います。たぶん⑤を自動化できれば、10分くらいで環境構築終わるようになるでしょう。

# あとがき
最初はNxでAngularのSchematicsが動くか心配でしたが、蓋を開けてみればNx自体がschematicsの塊でした。NestJSも全く触れたことがなかったですがAngularライクな感じですごく効率が捗りそうです。

Angular好きな人は、ぜひこの環境を試してみてください！