Closed11

SvelteKit + Cloudflare Pages / D1 でウェブサイト作成

cannorincannorin

Cloudflare D1 の正式版が出たらしいので,やっていきましょうかね

Next.js を Vercel 以外で動かすことに多大な苦しみを覚えるので,SvelteKit を採用することに……


まずは普通に SvelteKit でプロジェクト作成

https://kit.svelte.jp/docs/creating-a-project

$ npm create svelte@latest project
$ cd project
$ npm i

本筋とは関係ないが,自分は yarn の方が好きなのでそうする(npm run が長いのが主な理由)

$ corepack enable
$ yarn set version berry --yarn-path
$ yarn
$ rm package-lock.json

SvelteKit の .gitignore には yarn 周りが入ってないので追記

.gitignore
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

pnp も global cache も動かないらしいので .yarnrc.yml で切っておく
https://kit.svelte.jp/docs/faq#how-do-i-use-x-with-sveltekit-how-do-i-use-with-yarn-3

.yarnrc.yml
  yarnPath: .yarn/releases/yarn-x.x.x.cjs
+ nodeLinker: node-modules
+ enableGlobalCache: false
cannorincannorin

とりあえず Cloudflare Pages にデプロイしましょうか

https://kit.svelte.jp/docs/adapter-cloudflare

$ yarn add -D @sveltejs/adapter-cloudflare wrangler 
$ yarn remove @sveltejs/adapter-auto 
svelte.config.js
- import adapter from "@sveltejs/adapter-auto";
+ import adapter from "@sveltejs/adapter-cloudflare";

const config = {
  kit: {
-    adapter: adapter()
+    adapter: adapter({
+      routes: {
+        include: ["/*"],
+        exclude: ["<all>"]
+      }
+    })
  }
}

ここまでやって GitHub に push


GitHub → CF Pages のデプロイを設定していく
https://developers.cloudflare.com/pages/get-started/git-integration/

CF Dashboard から Connect to Git で repository を選択

Framework preset を SvelteKit に変更することだけ注意(自分は yarn にしたので Build command も yarn build に変更)

これだけやって Save and Deploy すればok

はい

cannorincannorin

Svelte から D1 に接続するための諸々をやっていく
https://developers.cloudflare.com/d1/examples/d1-and-sveltekit/

$ yarn add -D @cloudflare/workers-types 
src/app.d.ts
+ import type { D1Database } from "@cloudflare/workers-types";

  declare global {
    namespace App {
+     interface Platform {
+       env: {
+         DB: D1Database;
+       };
+       context: {
+         waitUntil(promise: Promise<unknown>): void;
+       };
+       caches: CacheStorage & { default: Cache };
+     }
+   }
  }

これでデプロイすれば,上で設定した D1 に接続できるはずだが,ローカルテストはどうするのか?

cannorincannorin

環境変数をアレする必要が出てくるっぽいので,Doppler をセットアップする
https://www.doppler.com/

  1. 適当に Project を作る
  • 自動で dev, stg, prd の config ができるはず
  1. GitHub repo への Integration を追加する
  • 先だって repo に staging と production の environment を用意しておく
  • staging に stg,production に prd を指定してあげる
  1. Cloudflare Pages への Integration を追加する
  • 現状はまだ必要ないが,アプリを作りこむにつれていずれ必要になると思うので,ついでに設定
  • APIトークンを発行したり色々必要だが,指示に従ってればできる
  • Preview に stg,Production に prd を指定してあげる


とりあえず環境変数として以下を用意してあげる

  • D1_DATABASE_NAME, D1_DATABASE_ID
    • stg, prd で別々にする(もう一個 D1 Database を作る)
  • CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN
    • stg, prd 共用
    • CLOUDFLARE_ACCOUNT_IDhttps://dash.cloudflare.com/ の URL 欄から拾うとかいう原始時代みたいな手段で取得する
    • CLOUDFLARE_API_TOKEN は右上の My Profile → 左サイドバーの API Tokens から
    • Account - D1 - Edit の Permission があればよいと思う.これで動かなければ後で治します


ローカルにも D1_DATABASE_NAMED1_DATABASE_ID を用意してやる.

$ yarn add -D wrangler

wrangler は同じ Database ID でもローカルに保存したりリモートでアクセスしたりできるっぽいので,とりあえず staging 用の NAME/ID を使いまわすことにする

普通は wrangler.toml.gitignore せずに使うらしい(このファイルで CF Dashboard を触らずに全てを設定できるらしい)が,CF Pages 用と CF Workers 用で仕様が異なるらしい(???)上にドキュメントがよくわからんので,.gitignore に入れつつ純粋にローカルテスト用途のみに使うこととする(このへんが CF Pages でやってる人が少ない理由か…)

あと .wrangler は wrangler の作業フォルダで,これも .gitignore に追加

.gitignore
+ .wrangler
+ wrangler.toml
wrangler.toml
name = "REPLACEME"
compatibility_date = "CHAN-GE-ME"

[[d1_databases]]
binding = "DB"
database_name="<name>"
database_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

最後に wrangler-proxy を使って,Svelte 側からローカルの D1 を触れるようにしてあげる

$ yarn add -D wrangler-proxy npm-run-all
package.json
-     "dev": "vite dev",
+     "dev": "run-p -r dev:vite dev:wrangler",
+     "dev:vite": "vite dev",
+     "dev:wrangler": "wrangler dev node_modules/wrangler-proxy/dist/worker.js",

wrangler-proxy は cache API を提供してくれないので optional にしておく(?)[1]

src/app.d.ts
-      caches: CacheStorage & { default: Cache };
+      caches?: CacheStorage & { default: Cache };

ローカルでのみ諸々を wrangler-proxy に差し替えてあげる

src/hooks.server.ts
import { dev } from "$app/environment";
import type { Handle } from "@sveltejs/kit";

export const handle = (async ({ event, resolve }) => {
  if (dev) {
    const { connectD1, waitUntil } = await import("wrangler-proxy");
    event.platform = {
      env: { DB: connectD1("DB") },
      context: { waitUntil }
    };
  }
  return resolve(event);
}) satisfies Handle;

これで行ける気がする

脚注
  1. おれはまだ Web Worker のことをよく理解していない ↩︎

cannorincannorin

今後の様々を見越して,wrangler.toml を自動生成できるようにする

こちらがテンプレートとなるファイル

wrangler.base.toml
name = "REPLACEME"
compatibility_date = "CHAN-GE-ME"

[[d1_databases]]
binding = "DB"
database_name = "${D1_DATABASE_NAME}"
database_id = "${D1_DATABASE_ID}"

元々の wrangler.toml に書いてたやつを .env に移植

.env
D1_DATABASE_NAME="<name>"
D1_DATABASE_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

コマンド一発で生成できるようにしようと思ったら便利そうなやつがあった
https://www.npmjs.com/package/envsub

$ yarn add -D envsub
package.json
  "scripts": {
+   "gen:wrangler.toml": "envsub --all --env-file .env wrangler.base.toml wrangler.toml",
  },

これで yarn gen:wrangler.toml と打てば,環境変数または .env から必要な変数を読み取り,それらが代入された wrangler.toml が生成されるようになった

これの用途は二つ:

  1. .env を読み取り,ローカルテスト用の wrangler.toml を生成する
  2. システムの環境変数を読み取り,GitHub Actions 上で D1 のマイグレーションをかけるための wrangler.toml を生成する
cannorincannorin

ORM を入れましょう

現状 Drizzle が一番いいっぽい.mizchi先生いつもありがとうございます…
https://zenn.dev/mizchi/articles/d1-drizzle-orm

$ yarn add drizzle-orm 
$ yarn add -D drizzle-kit better-sqlite3
drizzle.config.json
{
  "schema": "./src/schema.ts",
  "out": "./migrations"
}

とりあえず動作確認したいだけなんで,後からぶっ飛ばす前提で適当にテーブルを作る

src/schema.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";

export const posts = sqliteTable("posts", {
  id: integer("id").primaryKey().notNull(),
  content: text("content")
});

各種 npm script をセットアップ

$ yarn add -D dotenv-cli cross-var
package.json
  "scripts": {
+   "migrate:remote": "dotenv cross-var 'wrangler d1 migrations apply $D1_DATABASE_NAME --remote'",
+   "migrate:local": "dotenv cross-var 'wrangler d1 migrations apply $D1_DATABASE_NAME --local'",
+   "gen:migration": "drizzle-kit generate:sqlite",
  },
$ yarn gen:migration
drizzle-kit: v0.20.17
drizzle-orm: v0.30.10

1 tables
posts 2 columns 0 indexes 0 fks

[] Your SQL migration file ➜ migrations/0000_omniscient_loki.sql 🚀

$ yarn migrate:local
 ⛅️ wrangler 3.53.0
-------------------
Migrations to be applied:
┌──────────────────────────┐
│ name                     │
├──────────────────────────┤
│ 0000_omniscient_loki.sql │
└──────────────────────────┘
? About to apply 1 migration(s)
Your database may not be available to serve requests during the migration, continue?
🤖 Using fallback value in non-interactive context: yes
🌀 Mapping SQL input into an array of statements
🌀 Executing on local database <name> (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) from .wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
┌──────────────────────────┬────────┐
│ name                     │ status │
├──────────────────────────┼────────┤
│ 0000_omniscient_loki.sql │ ✅       │
└──────────────────────────┴────────┘

いいですね~

cannorincannorin

GitHub Actions での自動マイグレーションをやっていく

.github/workflow/migrate.yml
name: Migrate

on:
  push:
    branches:
      - main
      - production
  workflow_dispatch:
    inputs:
      environment:
        required: true
        type: choice
        options:
          - "staging"
          - "production"

concurrency:
  group: ${{ github.workflow }}-${{ github.ref_name }}

jobs:
  migrate:
    name: Migrate

    runs-on: ubuntu-latest

    environment: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment || (github.ref == 'refs/heads/main' && 'staging' || 'production') }}

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
          check-latest: true
          cache: yarn

      - run: yarn install --immutable

      - name: Check if migrations are up to date
        run: |
          yarn gen:migration
          git status --porcelain | grep 'migrations/' && echo 'Unstaged migration detected!' && exit 1 || echo 'Migrations are up to date.'

      - name: Generate wrangler.toml
        run: |
          touch .env
          yarn gen:wrangler.toml
        env:
          D1_DATABASE_NAME: ${{ secrets.D1_DATABASE_NAME }}
          D1_DATABASE_ID: ${{ secrets.D1_DATABASE_ID }}

      - name: Perform migration
        run: yarn wrangler d1 migrations apply ${{ secrets.D1_DATABASE_NAME }} --remote
        env:
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

非自明な行だけ軽く説明

  • environment: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment || (github.ref == 'refs/heads/main' && 'staging' || 'production') }}

    • これは workflow_dispatch の場合は input で指定した environment を使い,push の場合はブランチ名に応じて environment を決めている(自分は main ブランチをステージング,production ブランチをプロダクションに使っているが,この辺はお好みで)
    • workflow_dispatch を使わない場合は最初の条件式を落としてよい
  • git status --porcelain | grep 'migrations/' && echo 'Unstaged migration detected!' && exit 1 || echo 'Migrations are up to date.'

    • これは yarn gen:migration 忘れてコミットした時に殺すための行で,yarn gen:migration 後に git で unstaged なファイルが migrations/ 以下に生えた時に落ちるようにしてある
  • touch .env

    • これは .env ファイルがない状態で dotenv 使うと怒るのでなだめている(それくらい無視してくれよ……)

とりあえず動いたんでok

cannorincannorin

適当に DB に読み書きするコードを追加した結果,ローカルでも CF Pages 上でも動いてることを確認できた

(以下はマジで適当なコードなので,みなさんはやらないほうがよい)

src/routes/posts/+page.server.ts
import type { PageServerLoad } from "./$types";
import { posts } from "../../schema";
import { drizzle } from "drizzle-orm/d1";

export const load = (async ({ platform }) => {
  if (!platform) return { posts: [] };
  const db = drizzle(platform.env.DB);
  const now = Date.now();
  await db.insert(posts).values({ id: now, content: `${new Date(now).toString()}` });
  const data = await db.select().from(posts).all();
  return { posts: data };
}) satisfies PageServerLoad;

フォーム周りを作るのがだるかったのでリロードするたびに insert が走るようになっている(最悪)

src/routes/+page.svelte
<script lang="ts">
  import type { PageData } from "./$types";

  export let data: PageData;
</script>

<h1>Posts</h1>

<ul>
  {#each data.posts as item}
    <li>
      [{item.id}] {item.content}
    </li>
  {/each}
</ul>

出すだけ

はい


というわけで SvelteKit + Cloudflare Pages / D1 が整いました

よかったですね

cannorincannorin

とりあえずトラブルシューティング的な何か

  • CLOUDFLARE_ACCOUNT_ID を設定してないと「GitHub Actions 上で」「適用すべきマイグレーションがないときのみ」謎の死を遂げるのできちんと設定しましょう
  • マイグレーション SQL に複数行コメントが入っていると,ローカルでは通るのに GitHub Actions では通らないので,コケたら drizzle の出力ファイルをチェックしましょう
cannorincannorin

その後……

  • Cloudflare KV + unstorage + Auth.js でログイン環境を構築した
  • MailChannels でメールを送れるようにした

などがあったが,まぁこのへんはネット見ながらサクッとできる方だと思う(気が向いたら OR 需要があったら書くかも)

このスクラップは2024/05/04にクローズされました