CloudFlareにデプロイするためのRemixプロジェクトを作成する（D1にアクセスしてデータを取得・表示する）

## 概要

CloudFlareにデプロイするためのRemixプロジェクトを作成する方法をまとめる。
なお、ローカル開発環境ではPrismaを用いてSQLiteを、CloudFlareでのデプロイ環境ではD1を用いる。

## やること

- CloudFlareにデプロイするRemixプロジェクトを作成
- CloudFlareへデプロイできるようにする
- DBはCloudFlareのD1を使う

## 前提

- 筆者はMacのローカルのNode.jsのバージョン管理にvoltaを用いている。
- Macのローカルで`npm`コマンドが実行できること。
- CloudFlareのアカウントはすでに持っておりブラウザからログインする事ができること。
- CloudFlare上でD1のDBが作成されていること。

## 方法

### Remixプロジェクトの作成

:::note warn
CloudFrontにデプロイするRemixのプロジェクトを作成する場合、作成時にCloudFlareのテンプレートを指定して作成したほうが良いです。
「一旦何もテンプレートは指定せずプロジェクト作ってあとからCloudFlare用の設定をすればいいや〜」はかなり大変です。
:::

1. 下記を実行して「todo-cloudflare」というRemixのプロジェクトを作成する。（作成中にCloudFlareへのログインを求められる事がある。）

    ```shellscript
    npm create cloudflare@latest todo-cloudflare-d1 -- --framework=remix
    ```

1. プロジェクト作成中の分岐は下記の様に選択する。
    1. Initialize a new git repository?（ローカルGitリポジトリの初期化、および初期ファイルのコミット） → Yes
    1. Install dependencies with npm?（npmの初期化） → Yes
    1. Do you want to deploy your application?（今すぐデプロイするかどうか）　→ Yes （この前後でログインを求められる事がある。）

1. 実行が完了するとデプロイされた画面がブラウザで開く。（おそらくURLのパスは同じプロジェクト名に設定しても異なる。）

    ![CleanShot 2024-06-11 at 00.39.19@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/8448b2d3-9925-314c-87aa-6dd344fcda9e.png)

1. 下記を実行してプロジェクトのディレクトリ直下のNode.jsのバージョンを20.11.1に固定しておく。

    ```shellscript
    cd todo-cloudflare-d1
    volta install node@20.11.1
    volta pin node@20.11.1
    ```

### ローカルリポジトリのGitの設定

1. 下記をそれぞれ実行してGitのコミッターの設定をしておく。

    ```shellscript
    git config --local user.name '任意のコミッター名'
    git config --local user.email '任意のコミッターのメールアドレス'
    ```

1. もしデフォルトブランチがmasterのままならmainに変えておくことをおすすめする。参考記事を下記に記載する。

https://qiita.com/miriwo/items/f9e85c3a436b25b388c5

### DBの設定

1. 下記を実行してPrismaを用意する。

    ```shellscript
    npm install prisma
    npm install @prisma/client
    npm install @prisma/adapter-d1
    npx prisma init
    ```

1. prisma/schema.prismaを下記のように編集する。

    ```prisma/schema.prisma
    // This is your Prisma schema file,
    // learn more about it in the docs: https://pris.ly/d/prisma-schema
    
    // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
    // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
    
    generator client {
      provider = "prisma-client-js"
      previewFeatures = ["driverAdapters"]
    }
    
    datasource db {
      provider = "sqlite"
      url      = env("DATABASE_URL")
    }
    
    // userテーブルの追加のために下記を追記
    model users {
      id    Int     @id @default(autoincrement())
      email String  @unique
      name  String
    }
    ```

1. プロジェクトルートに.envを用意し、下記のように記載する。（ローカル開発環境ではD1のローカル用のDBを使うため不要かも知れないが、設定されていない状態で`env("DATABASE_URL")`がどのような挙動になるか不安なので設定しておく。）

    ```.env
    DATABASE_URL="file:./dev.db"
    ```

1. .envの例を作っておく。

    ```shellscript
    cp .env .env.example
    ```

1. プロジェクトルート直下のwrangler.tomlを開きまずは下記の記載をコメントアウトする。

    ```wrangler.toml
    # [[d1_databases]]
    # binding = "MY_DB"
    # database_name = "my-database"
    # database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    ```

1. 更に下記の様に記載する。

    ```wrangler.toml
    [[d1_databases]]
    binding = "RemixのコードからDBにアクセスするときに使う任意の文字列"
    database_name = "CloudFlare上のD1のデータベース名"
    database_id = "CloudFlare上のD1のデータベースID"
    ```

1. 下記を実行してマイグレーションのクエリを記載するファイルを作成する。

    ```shellscript
    npx wrangler d1 migrations create wrangler.tomlに記載したdatabase_name create_users_table
    ```

1. 下記を実行してマイグレーションのクエリを追記する。

    ```shellscript
    npx prisma migrate diff --from-empty --to-schema-datamodel ./prisma/schema.prisma --script --output migrations/0001_create_users_table.sql
    ```

1. 下記を実行してローカル開発環境用D1にマイグレーションを実行する。

    ```shellscript
    npx wrangler d1 migrations apply wrangler.tomlに記載したdatabase_name --local
    ```

1. 実行すると下記のようになる。

    ```
    $ npx wrangler d1 migrations apply todo-cloudflare-d1 --local
     ⛅️ wrangler 3.57.1 (update available 3.60.2)
    -------------------------------------------------------
    Migrations to be applied:
    ┌─────────────────────────────┐
    │ name                        │
    ├─────────────────────────────┤
    │ 0001_create_users_table.sql │
    └─────────────────────────────┘
    ✔ About to apply 1 migration(s)
    Your database may not be available to serve requests during the migration, continue? … yes
    🌀 Executing on local database todo-cloudflare-d1 (4b6eada0-7ef7-4717-87ad-074b0fd04ccc) from .wrangler/state/v3/d1:
    🌀 To execute on your remote database, add a --remote flag to your wrangler command.
    ┌─────────────────────────────┬────────┐
    │ name                        │ status │
    ├─────────────────────────────┼────────┤
    │ 0001_create_users_table.sql │ ✅       │
    └─────────────────────────────┴────────┘
    ```

1. 下記を実行してCloudFlare上のD1にマイグレーションを実行する。

    ```shellscript
    npx wrangler d1 migrations apply wrangler.tomlに記載したdatabase_name --remote
    ```

1. 下記を実行してローカル開発環境用D1にテストデータを格納する。

    ```shellscript
    npx wrangler d1 execute wrangler.tomlに記載したdatabase_name --command "INSERT INTO  \"users\" (\"email\", \"name\") VALUES('hoge@example.com', 'Foo Bar (Local)');" --local
    ```

1. 下記を実行してCloudFlare上のD1にテストデータを格納する。

    ```shellscript
    npx wrangler d1 execute wrangler.tomlに記載したdatabase_name --command "INSERT INTO  \"users\" (\"email\", \"name\") VALUES('hoge@example.com', 'Foo Bar (Remote)');" --remote
    ```

1. ローカル開発環境用D1にTablePlusなどでアクセスする場合、SQLiteの接続設定で`.wrangler/state/v3/d1/miniflare-D1DatabaseObject`ディレクトリ直下の.sqliteファイルを指定すれば開くことができる。
1. 下記を実行してPrismaのクライアントを作成する。

    ```shellscript
    npx prisma generate
    ```

1. 下記を実行してwarangler.tomlから型ファイルを生成する。（実行するとプロジェクトルートにworker-configration.d.tsというファイルができる。）

    ```shellscript
    npx wrangler types
    ```

1. 下記を実行してディレクトリとファイルの作成を行う。

    ```shellscript
    mkdir app/database
    touch app/database/client.ts
    ```

1. `app/database/client.ts`に下記の内容を記載する。

    ```app/database/client.ts
    import { PrismaClient } from '@prisma/client'
    import { PrismaD1 } from '@prisma/adapter-d1'
    
    export const connection = async (db: D1Database) => {
      const adapter = new PrismaD1(db)
      return new PrismaClient({ adapter })
    }
    ```

1. プロジェクトルートのload-context.tsを下記のように編集する。

    ```load-context.ts
    import { type AppLoadContext } from '@remix-run/cloudflare'
    import { type PlatformProxy } from "wrangler";
    import { connection } from './app/database/client'
    
    type Cloudflare = Omit<PlatformProxy<Env>, "dispose">;
    
    declare module "@remix-run/cloudflare" {
      interface AppLoadContext {
        cloudflare: Cloudflare;
        db: Awaited<ReturnType<typeof connection>>;
      }
    }
    
    type args = {
      request: Request,
      context: {
        cloudflare: Cloudflare
      }
    }
    type GetLoadContext = (args: args) => Promise<AppLoadContext>
    
    export const getLoadContext: GetLoadContext = async ({ context }) => {
      return {
        ...context,
        db: await connection(
          context.cloudflare.env.DB, // 末尾のDBはwrangler.tomlのbindingの値
        ),
      }
    }
    ```

1. プロジェクトルートのvite.config.tsを下記のように編集する。

    ```vite.config.ts
    import {
      vitePlugin as remix,
      cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
    } from "@remix-run/dev";
    import { defineConfig } from "vite";
    import tsconfigPaths from "vite-tsconfig-paths";
    import { getLoadContext } from "./load-context";
    
    export default defineConfig({
      plugins: [
        remixCloudflareDevProxy({ getLoadContext }),
        remix({
          future: {
            v3_fetcherPersist: true,
            v3_relativeSplatPath: true,
            v3_throwAbortReason: true,
          },
        }),
        tsconfigPaths(),
      ],
    });
    ```

1. `functions/[[path]].ts`を開き下記のように編集する。

    ```functions/[[path]].ts
    import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";
    
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore - the server build file is generated by `remix vite:build`
    // eslint-disable-next-line import/no-unresolved
    import * as build from "../build/server";
    import { getLoadContext } from "../load-context";
    
    export const onRequest = createPagesFunctionHandler({ build, getLoadContext });
    ```

1. `npm run dev`を実行して下記のような画面が現段階で問題なく開くことを確認する。

    ![New_Remix_App.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/b5d0c604-245c-5abd-8e55-8782bd858845.png)

1. `app/routes`直下にloader.tsを作成し、下記のように記載する。

    ```app/routes/loader.ts
    import type { LoaderFunctionArgs} from "@remix-run/cloudflare";
    
    export const loader = async ({ context }: LoaderFunctionArgs) => {
      try {
        return await context.db.users.findMany();
      } catch (error) {
        console.error("Failed to load users:", error);
        throw new Response("Internal Server Error", { status: 500 });
      }
    }
    ```

1. `app/routes/_index.tsx`を開き下記のように修正する。

    ```app/routes/_index.tsx
    import type { MetaFunction } from "@remix-run/cloudflare";
    import { loader } from "./loader";
    export { loader };
    import { useLoaderData } from "@remix-run/react";
    
    export const meta: MetaFunction = () => {
      return [
        { title: "New Remix App" },
        {
          name: "description",
          content: "Welcome to Remix on Cloudflare!",
        },
      ];
    };
    
    interface User {
      id: string;
      name: string;
      email: string;
    }
    
    export default function Index() {
      const users = useLoaderData<User[]>();
      return (
        <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
          <h1>Welcome to Remix on Cloudflare</h1>
          <ul>
            <li>
              <a target="_blank" href="https://remix.run/docs" rel="noreferrer">
                Remix Docs
              </a>
            </li>
            <li>
              <a
                target="_blank"
                href="https://developers.cloudflare.com/pages/framework-guides/deploy-a-remix-site/"
                rel="noreferrer"
              >
                Cloudflare Pages Docs - Remix guide
              </a>
            </li>
          </ul>
          <div>
            <h2>Users</h2>
            <ul>
              {users.map((user: User) => (
                <li key={user.id}>
                  <div>ID: {user.id}</div>
                  <div>NAME: {user.name}</div>
                  <div>EMAIL: {user.email}</div>
                </li>
              ))}
            </ul>
          </div>
        </div>
      );
    }
    ```

1. 下記のようにローカルの情報が表示されれば問題ない。

    ![New_Remix_App.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/46fbf701-5695-890d-b64e-f29bdb1d6822.png)

1. また、下記のようにすることでjsonを画面に表示する事ができる。

    ```app/routes/_index.tsx
    import type { MetaFunction } from "@remix-run/cloudflare";
    import { loader } from "./loader";
    export { loader };
    import { useLoaderData } from "@remix-run/react";
    
    export const meta: MetaFunction = () => {
      return [
        { title: "New Remix App" },
        {
          name: "description",
          content: "Welcome to Remix on Cloudflare!",
        },
      ];
    };
    
    interface User {
      id: string;
      name: string;
      email: string;
    }
    
    export default function Index() {
      const users = useLoaderData<User[]>();
      return (
        <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
          <h1>Welcome to Remix on Cloudflare</h1>
          <ul>
            <li>
              <a target="_blank" href="https://remix.run/docs" rel="noreferrer">
                Remix Docs
              </a>
            </li>
            <li>
              <a
                target="_blank"
                href="https://developers.cloudflare.com/pages/framework-guides/deploy-a-remix-site/"
                rel="noreferrer"
              >
                Cloudflare Pages Docs - Remix guide
              </a>
            </li>
          </ul>
          <div>
            <pre>{JSON.stringify(users, null, 2)}</pre>
          </div>
          <div>
            <h2>Users</h2>
            <ul>
              {users.map((user: User) => (
                <li key={user.id}>
                  <div>ID: {user.id}</div>
                  <div>NAME: {user.name}</div>
                  <div>EMAIL: {user.email}</div>
                </li>
              ))}
            </ul>
          </div>
        </div>
      );
    }
    ```

1. 一旦コミットしておく。（この次`npm run deploy`を実行するがこれ自体はコミットされていようがいまいが関係なく今のローカル環境のコードをビルドしてデプロイする。）
1. 下記を実行してデプロイを行う。（フリープランだと容量が大きいのでデプロイできない可能性がある。CloudFlare上で確認しなくて良いならこれはやらなくていい。）

    ```shellscript
    npm run deploy
    ```

1. 上記コマンドをCloudFlareに5ドル課金してでもデプロイを確認したい方はこちら → [Remix CloudFlareにデプロイを行ったら容量系のエラーが出た](https://qiita.com/miriwo/items/6bfec95fbb9eff11b2b8)

1. CloudFlareでデプロイされているURLにアクセスし、トップ画面を確認し「下記を実行してCloudFlare上のD1にテストデータを格納する。」の手順で格納した（Remote）がついているユーザーが表示されていることを確認する。

    ![New_Remix_App_と_wrangler_toml_—_todo-cloudflare-d1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/8180dc6e-c0ce-0eaf-c948-dcd0a1fab247.png)


## 自分用メモ

- CloudFlare CLIツールログイン

    ```shellscript
    npx wrangler login
    ```

- CloudFlare CLIツールログアウト

    ```shellscript
    npx wrangler logout
    ```

## 参考文献

https://developers.cloudflare.com/pages/framework-guides/deploy-a-remix-site/

https://developers.cloudflare.com/workers/wrangler/commands/

https://qiita.com/miriwo/items/d9cf143bfef0ed740e46