ちょっと真面目なNext.js【Firebase Hosting編】

## 概要
Next.jsで開発した物をFirebase Hostingで公開することになったので、
公式のサンプルを参考にデプロイしました。

[Next.js](https://nextjs.org/)
[with-firebase-hosting-and-typescript (公式サンプル)](https://github.com/zeit/next.js/tree/canary/examples/with-firebase-hosting-and-typescript)

#### トピックス 
・プロジェクト設定 
・デプロイ
・カスタムサーバー設定(Express) 
・注意ポイント
・感想

## プロジェクト設定
~~~
$ firebase init
...
/* アプリをデプロイするだけなのでFunctions/HostingのみでOK */
 ◯ Database: Deploy Firebase Realtime Database Rules
 ◯ Firestore: Deploy rules and create indexes for Firestore
 ◉ Functions: Configure and deploy Cloud Functions
❯◉ Hosting: Configure and deploy Firebase Hosting sites
 ◯ Storage: Deploy Cloud Storage security rules
~~~
~~~
/* 既存のFirebaseプロジェクトを指定するか、新しくプロジェクトを作る */
? Select a default Firebase project for this directory: 
  [don't setup a default project] 
❯ next-sample-deploy (next-sample-deploy) 
  [create a new project]
~~~
~~~
/* この辺の設定はお好きにどうぞ */
? What language would you like to use to write Cloud Functions? 
? Do you want to use TSLint to catch probable bugs and enforce style? (Y/n)
? Do you want to install dependencies with npm now?
? What do you want to use as your public directory?
/* SSR化したいのでNo */
? Configure as a single-page app (rewrite all urls to /index.html)? No
~~~
設定が終わると「functions」ディレクトリーや「.firebaserc」「firebase.json」ファイルが生成されます。

以下のコマンドも入れておきましょう。
.firebasercで使用するプロジェクトを指定します。

~~~
$ firebase use default
~~~
※ 初期設定ではコンソールで設定したプロジェクトのみ存在していると思います。

```.firebaserc
{
  "projects": {
    "default": "next-sample-deploy"
  }
}
```
次にディレクトリー構造を変更します。

```sh:デフォルト
.
├── firebase.json
├── functions
│   ├── next-env.d.ts
│   ├── package-lock.json
│   ├── package.json
│   ├── src
│   │   └── index.ts
│   └── tsconfig.json
├── next-env.d.ts
├── node_modules
├── package-lock.json
├── package.json
├── pages
│   └── index.tsx
├── public
│   ├── 404.html
│   └── index.html
└── tsconfig.json
```
```sh:変更後
.
├── firebase.json
├── node_modules
├── package-lock.json
├── package.json
└── src
    ├── app
    │   ├── next-env.d.ts
    │   ├── next.config.js
    │   ├── pages
    │   │   ├── _app.tsx
    │   │   └── index.tsx
    │   └── tsconfig.json
    ├── functions
    │   ├── index.ts
    │   └── tsconfig.json
    └── public
        └── placeholder.html
```

srcディレクトリーを作成し、その中にapp/functions/publicを配置し、functions内のpackage.jsonなども削除しましょう。

**[追記]：このままだとHosting後ルートがpublic以下に設定されてしまうためsrc/app/pagesに_app.tsxも追加してください。[参考](https://github.com/deptno/next.js-typescript-starter-kit/blob/master/pages/_app.tsx)**


src/app/にnext.config.jsを作成し、出力ファイルパスを設定しましょう。

```js:next.config.js
module.exports = {
    distDir: '../../dist/functions/next'
}
```

以下ファイルも書き換えてください

```json:src/functions/tsconfig.json
{
  "compilerOptions": {
    "lib": [
      "es6",
      "dom",
      "es2016"
    ],
    "module": "commonjs",
    "strict": true,
    "outDir": "../../dist/functions",
    "sourceMap": true,
    "target": "es2017",
    "baseUrl": "./",
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "isolatedModules": true
  },
  "compileOnSave": true
}
```

```json:firebase.json
{
  "functions": {
    "source": "dist/functions",
    "predeploy": [
      "npm run typecheck-app",
      "npm run build-functions",
      "npm run build-app",
      "npm run copy-deps",
      "npm run install-deps"
    ]
  },
  "hosting": {
    "public": "dist/public",
    "rewrites": [
      {
        "source": "**/**",
        "function": "nextApp"
      }
    ],
    "predeploy": "npm run build-public"
  }
}
```
```ts:functions/src/index.ts
import * as functions from 'firebase-functions';
import next from 'next';

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev, conf: { distDir: 'next' } })
const handle = app.getRequestHandler()

export const nextApp = functions.https.onRequest((req, res) => {
    console.log('File: ' + req.originalUrl)
    return app.prepare().then(() => handle(req, res))
})
```
```package.json
...
"scripts": {
    "dev": "next src/app",
    "preserve": "npm run build-public && npm run build-functions && npm run build-app && npm run copy-deps && npm run install-deps",
    "serve": "cross-env NODE_ENV=production firebase serve",
    "deploy": "firebase deploy",
    "clean": "rimraf \"dist/functions\" && rimraf \"dist/public\"",
    "build-app": "next build \"src/app\"",
    "build-public": "cpx \"src/public/**/*.*\" \"dist/public\" -C",
    "build-functions": "tsc --project src/functions",
    "typecheck-app": "tsc --project src/app",
    "copy-deps": "cpx \"*{package.json,package-lock.json,yarn.lock}\" \"dist/functions\" -C",
    "install-deps": "cd \"dist/functions\" && npm i"
  },
...
```
足りないパッケージも追加しましょう

```sh
npm i firebase-functions
npm i -D cpx cross-env rimraf
```

ここまでできたら一旦起動してみます。
以下のコマンドでビルドとlocalhostでプロジェクトが起動します

```sh
$ npm run serve
...
✔  functions: Using node@10 from host.
i  hosting: Serving hosting files from: dist/public
✔  hosting: Local server: http://localhost:5000
✔  functions: Emulator started at http://localhost:5001
i  functions: Watching "/Users/user/next-sample-deploy/dist/functions" for Cloud Functions...
✔  functions[nextApp]: http function initialized (http://localhost:5001/next-sample-deploy/us-central1/nextApp).
```
functions[nextApp]: http function initializedのURLを開くとNextのページが表示されます。

## デプロイ
以下のコマンドでビルドとデプロイが始まります

```sh
$ npm run deploy
```

## カスタムサーバー設定
express使用時

```diff:src/function/index.ts
 import * as functions from 'firebase-functions'
 import next from 'next'
+ import express from "express";

+ const cors = require('cors');
+ const routes = require("next-routes");

+ const dev = process.env.NODE_ENV !== 'production'
+ const app = next({ dev, conf: { distDir: 'next' } })
+ const handle = routes().getRequestHandler(app);
- const handle = app.getRequestHandler()

+ const server = express();
+ server.use(cors({ origin: true }));
+ server.get("*", (req, res): any => {
+     return handle(req, res);
+ });

+ export const nextApp = functions.https.onRequest(server)
- export const nextApp = functions.https.onRequest((req, res) => {
-     console.log('File: ' + req.originalUrl)
-     return app.prepare().then(() => handle(req, res))
- })
```
handleにnextのルートを設定して
functions.https.onRequest()の引数にexpressを渡すだけ！

## 注意ポイント
・既存のプロジェクトにFirebase Hostingの設定を組み込む際は、ディレクトリー構造が公式サンプルと違ってしまうことがあるので、firebase.jsonやpackage.jsonのscripts、app/とfunctions/の出力ファイルのパスを変更してください。

・ビルド時に足りないと言われ追加したパッケージ  
※ それぞれのプロジェクトによっては、他に必要なものや不要なものがあるかもしれません。

```sh
// 初ビルド時
npm install -D @types/react-dom

// express使用時
$ npm i cors next-routes

// 既存のプロジェクトに設定時
$ $ npm i core-js
```

## 感想
自分の場合は既存のプロジェクトに組み込む必要があったので、公式を参考に新規プロジェクトから設定し、
既存のプロジェクトのデレクトリー構造に合わせ再設定、起動テスト、デプロイのテストを経て既存のプロジェクトに反映しました。

新規プロジェクトだと公式のサンプルを真似るだけですぐに設定できるので、Firebase Hostingを使用する予定があるのなら、早めに設定するのが楽なのかなと思います。