SolidJSを使ってFirebase HostingでSSGを試す

> 本記事に記載されている内容は、古いバージョンのSolid Startに基づいていますので、ご注意ください。

Firebase HostingにデプロイしたページにSSGしてみます。
Firebase HostingはSWR(stale-while-revalidate)が使えるようなので、Cloud Functions for Firebaseで得られる値をCDNにキャッシュしてみます。

- SolidJSとSolid Startを使用します。
- パッケージマネージャーにはpnpmを使います。

# プロジェクトを作成する
## プロジェクトのディレクトリを作成

```shell
$ mkdir ssg-firebase-solidjs
$ cd ssg-firebase-solidjs
```

## 準備

### Blazeプランにする

あとで実際にデプロイして動かしてみます。
Cloud Functions for FirebaseはBlazeプランでなければ実行できないのでBlazeプランにしておく必要があります。

### firebase tools をインストール

```shell
$ npm install -g firebase-tools
```

### Firebase ログイン

CLIからもfirebaseにログインしていなければ先に進めませんので、以下のコマンドでログインしておきましょう。

```shell
$ firebase login
```

## Firebaseを初期化

FunctionsとHostingを有効にして、TypeScriptを使えるようにしましょう。

```shell
$ firebase init
```

# 動的なデータを生成するバックエンド処理を作成する

## Functionsで関数を作成する

### モジュールをインストール

```shell
$ pnpm -C functions install
```

@firebase/app-typesがないというエラー(`ERR_PNPM_PEER_DEP_ISSUES  Unmet peer dependencies`)が発生した場合は以下でインストールしましょう。

```shell
$ pnpm -C functions add @firebase/app-types
```

### 関数を作成

`functions/src/index.ts`を開いて以下のように書き換えます。
CDNのキャッシュが効いているかどうかをわかりやすくするため、日時を返すようにしています。

```typescript:functions/src/index.ts
import * as functions from "firebase-functions"

export const lastUpdatedAt = functions.https.onRequest((_, response) => {
  const dateTime = new Date()
  const formattedDate = `${dateTime.getFullYear()}-${dateTime.getMonth() + 1}-${dateTime.getDate()}`
  const formattedTime = `${dateTime.getHours()}:${dateTime.getMinutes()}:${dateTime.getSeconds()}`
  const json = {
    lastUpdatedAt: `${formattedDate} ${formattedTime}`,
  }

  response.send(json)
})
```

### デプロイする

#### ビルド

```shell
$ pnpm -C functions build
```

#### デプロイ

```shell
$ firebase deploy --only functions
```

### 実行

デプロイが成功すると``のようなURLが表示されるので、Webブラウザーでアクセスしてみましょう。
以下のような表示になるはずです。

![スクリーンショット 2022-10-10 0.22.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64294/0b39347d-e766-9e8e-d437-4a8dd423b047.png)

### ローカルで起動

後ほど、Webサイトをビルドするときにここで作成した関数を実行して結果をWebページに埋め込みます。
ビルド時にローカルで実行できるようにするために、`firebase serve`コマンドを使用してローカルでサーバーを起動しておきます。

```shell
$ firebase serve -only functions
✔  functions: Using node@16 from host.
i  functions: Watching "/path/to/ssg-firebase-solidjs/functions" for Cloud Functions...
✔  functions: Loaded functions definitions from source: lastUpdatedAt.
✔  functions[asia-northeast1-lastUpdatedAt]: http function initialized (http://localhost:5000/ssg-firebase-solidjs/us-central1/lastUpdatedAt).
i  functions: Beginning execution of "lastUpdatedAt"
i  functions: Finished "lastUpdatedAt" in ~1s
```

上記ログに表示されている通り、`curl http://localhost:5000/ssg-firebase-solidjs/asia-northeast1/lastUpdatedAt`を実行すると、以下のような結果が返ります。

```shell
$ curl http://localhost:5000/ssg-firebase-solidjs/us-central1/lastUpdatedAt
{"lastUpdatedAt":"2022-10-9 14:3:26"}    
```

コマンドはCtrl+Cするまで起動したままになるので、これ以降の作業を続けるには別のターミナルを開いてください。

# SolidJSでスケルトンコードを作成する

## SolidJSの環境を作成

firebase initで作成されたpublicディレクトリは作り直すので、いったん削除します。

```shell
$ rm -rf public
```

続いて同名のディレクトリを作成して、SolidJSのプロジェクトを作成していきます。

```shell
$ mkdir public
$ cd public
$ pnpm create solid
```

テンプレートの選択を要求されます。今回は深く考えず`bare`を選択します。

```shell
? Which template do you want to use? › - Use arrow-keys. Return to submit.
❯   bare
    durable-objects-websocket
    hackernews
    todomvc
    with-auth
    with-mdx
    with-solid-styled
    with-tailwindcss
    with-vitest
```

SSRかどうかと聞かれるのですが、デフォルトでよいのでEnterを押します。

```shell
? Server Side Rendering? › (Y/n)
```

TypeScriptを使うか問われるのでYを押します。

```shell
? Use TypeScript? › (y/N)
```

ディレクトリをプロジェクトのルートに戻しておきます。

```shell
$ cd ..
```

モジュールをインストールします。

```shell
$ pnpm -C public install
```

## 公開ディレクトリを変更する

ビルドすると、`public/build/public`に結果が格納されるのでfirebase.jsonの`hosting.public`の設定を以下のように変更しておきます。

```json:firebase.json(抜粋)
  "hosting": {
    "public": "public/dist/public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  }
```

# SSGのアダプターを適用する

solid-start-nodeは不要なのでアンインストールします。

```
$ pnpm -C public uninstall solid-start-node
```

代わりに solid-start-static をインストールします。これはSSGを実現するアダプターです。

```
$ pnpm -C public install -D solid-start-static
```

# CDNにキャッシュさせる設定を行う

## Functionsの関数の実行にCDNを通す

rewrites設定で、Firebase Hostingを経由させることでCDNを通すようにし、headersで、キャッシュの設定を行っています。

キャッシュする時間は、あとで実験しやすくするために60秒とし、stale-while-revalidateの時間も60秒としています。(つまり120秒間キャッシュの値を返す設定になっている)

```json:firebase.json(抜粋)
  "hosting": {
    "public": "public/dist/public",
    "rewrites": [{
      "source": "/lastUpdatedAt",
      "function": "lastUpdatedAt",
      "region": "us-central1"
    }],
    "headers": [ {
      "source": "/lastUpdatedAt",
      "headers": [ {
        "key": "Cache-Control",
        "value": "public, max-age=60, stale-while-revalidate=60"
      } ]
    }],
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  }
```

# 静的サイトを作成する

## 404.htmlのファイルの修正

SSGのために`solid-start-static`アダプターを使うと、ビルドしたときにエラーが発生してしまうので、`public/src/routes/[...404].tsx`の以下をコメントアウトします。

- `import { HttpStatusCode } from "solid-start/server";`
- `<HttpStatusCode code={404} />`

参考： https://pullanswer.com/questions/solid-start-static-adapter-throws-err_invalid_arg_type

## index.tsxを書き換える

ビルド時にアクセスするAPIのURLと、実行時にアクセスするAPIのURLを切り替えるようにしていますが、動的に変更したいデータがなければビルド時のAPIだけで良いでしょう。
下記のビルド時のAPIのURLは、ビルド時に実行可能でなければなりません。

onMountはブラウザでしか動作しないのでビルドタイムとランタイムでAPIを分けることができます。

```tsx:public/src/routes/index.tsx
import { createResource, createEffect, onMount } from "solid-js";

type LastUpdatedAtType = {
  lastUpdatedAt: string;
}

const BUILD_TIME_API = "http://localhost:5000/ssg-firebase-solidjs/us-central1/lastUpdatedAt";
const RUN_TIME_API = "/lastUpdatedAt";

export default function Home() {
  const [api, setApi] = createSignal<string>(BUILD_TIME_API);
  const [data, { refetch }] = createResource<LastUpdatedAtType, string>(api, async (url) => (await fetch(url)).json());

  onMount(() => setApi(RUN_TIME_API));

  return (
    <main>
      <p>Last updated at: {data()?.lastUpdatedAt || "-"}</p>
      <button onClick={() => refetch()}>Refetch!</button>
    </main>
  );
}
```

# デプロイする

先に hosting のコンテンツをビルドしてから、firebaseにデプロイを行う。

```shell
$ pnpm -C public build
$ firebase deploy --only hosting
```

## 動作確認

Webブラウザーで表示すると以下のような表示になります。
SSGのため、ビルド時にlastUpdatedAtを実行した結果が埋め込まれた状態になるので、日時が表示されます。

![スクリーンショット 2022-10-10 16.38.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64294/1330e81a-78af-64f5-58e3-921dba51e5a1.png)

### SWRを確認してみる

ビルドしてから60秒以上経っている前提ですが、画面を表示して`Refresh!`ボタンを押します。
それから60秒経過するまでの間、なんど`Refresh!`ボタンを押しても日時は変わりません。
Cloud Functions for Firebaseの関数は実行されず結果をCDNのキャッシュから返しているためです。

60秒経過して、120秒経過するまでに`Refresh!`ボタンを押しても日時に変更はありません。

そして、120秒経ってもう一度`Refresh!`ボタンを押すと日時は変わります。しかし、このときに表示される日時はボタンを押した日時ではありません。**60秒経過して、120秒経過するまでに`Refresh!`ボタンを押した時の日時**が表示されます。

上記の通りになればSWRによって日時がフェッチされたことが確認できたと言っていいと思います。

# 最後に

いかがでしたでしょうか。Firebase HostingでSolidJSを使いSSGを実現してみただけでなく、SWRによるCDNのキャッシュの動きも確認してみました。

なかなか前衛的なモジュールを使っての試みだったので、いつまで本ページの内容が実験可能であるか保証はできませんが、興味のある方はトライしてみてください。
一応、package.jsonの中身も記載しておきます。

## functions

```json:package.json
{
  "name": "functions",
  "scripts": {
    "lint": "eslint --ext .js,.ts .",
    "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",
  "dependencies": {
    "@firebase/app-types": "^0.8.0",
    "firebase-admin": "^10.0.2",
    "firebase-functions": "^3.18.0"
  },
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^5.12.0",
    "@typescript-eslint/parser": "^5.12.0",
    "eslint": "^8.9.0",
    "eslint-config-google": "^0.14.0",
    "eslint-plugin-import": "^2.25.4",
    "firebase-functions-test": "^0.2.0",
    "typescript": "^4.5.4"
  },
  "private": true
}
```

## hosting

```json:package.json
{
  "name": "public",
  "scripts": {
    "dev": "solid-start dev",
    "build": "solid-start build",
    "start": "solid-start start"
  },
  "type": "module",
  "devDependencies": {
    "solid-start-static": "^0.1.5",
    "typescript": "^4.8.3",
    "vite": "^3.1.0"
  },
  "dependencies": {
    "@solidjs/meta": "^0.28.0",
    "@solidjs/router": "^0.5.0",
    "solid-js": "^1.5.7",
    "solid-start": "^0.1.0",
    "undici": "^5.10.0"
  },
  "engines": {
    "node": ">=16"
  }
}
```

以上です〜！