SvelteKit + Cloudflare Pages / D1 でウェブサイト作成
Cloudflare D1 の正式版が出たらしいので,やっていきましょうかね
Next.js を Vercel 以外で動かすことに多大な苦しみを覚えるので,SvelteKit を採用することに……
まずは普通に SvelteKit でプロジェクト作成
$ 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 周りが入ってないので追記
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
pnp も global cache も動かないらしいので .yarnrc.yml
で切っておく
yarnPath: .yarn/releases/yarn-x.x.x.cjs
+ nodeLinker: node-modules
+ enableGlobalCache: false
とりあえず Cloudflare Pages にデプロイしましょうか
$ yarn add -D @sveltejs/adapter-cloudflare wrangler
$ yarn remove @sveltejs/adapter-auto
- 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 のデプロイを設定していく
CF Dashboard から Connect to Git で repository を選択
Framework preset を SvelteKit に変更することだけ注意(自分は yarn にしたので Build command も yarn build
に変更)
これだけやって Save and Deploy すればok
はい
D1 をセットアップしていく
CF Pages のプロジェクトページで Settings → Functions → D1 database bindings → Add binding
(D1 database が存在していない場合は先に作成する必要がある)
Variable name は DB
とする
Svelte から D1 に接続するための諸々をやっていく
$ yarn add -D @cloudflare/workers-types
+ 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 に接続できるはずだが,ローカルテストはどうするのか?
環境変数をアレする必要が出てくるっぽいので,Doppler をセットアップする
- 適当に Project を作る
- 自動で dev, stg, prd の config ができるはず
- GitHub repo への Integration を追加する
- 先だって repo に staging と production の environment を用意しておく
- staging に stg,production に prd を指定してあげる
- 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_ID
は https://dash.cloudflare.com/ の URL 欄から拾うとかいう原始時代みたいな手段で取得する -
CLOUDFLARE_API_TOKEN
は右上の My Profile → 左サイドバーの API Tokens から -
Account - D1 - Edit
の Permission があればよいと思う.これで動かなければ後で治します
ローカルにも D1_DATABASE_NAME
と D1_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
に追加
+ .wrangler
+ 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
- "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]
- caches: CacheStorage & { default: Cache };
+ caches?: CacheStorage & { default: Cache };
ローカルでのみ諸々を wrangler-proxy
に差し替えてあげる
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;
これで行ける気がする
-
おれはまだ Web Worker のことをよく理解していない ↩︎
今後の様々を見越して,wrangler.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
に移植
D1_DATABASE_NAME="<name>"
D1_DATABASE_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
コマンド一発で生成できるようにしようと思ったら便利そうなやつがあった
$ yarn add -D envsub
"scripts": {
+ "gen:wrangler.toml": "envsub --all --env-file .env wrangler.base.toml wrangler.toml",
},
これで yarn gen:wrangler.toml
と打てば,環境変数または .env
から必要な変数を読み取り,それらが代入された wrangler.toml
が生成されるようになった
これの用途は二つ:
-
.env
を読み取り,ローカルテスト用のwrangler.toml
を生成する - システムの環境変数を読み取り,GitHub Actions 上で D1 のマイグレーションをかけるための
wrangler.toml
を生成する
ORM を入れましょう
現状 Drizzle が一番いいっぽい.mizchi先生いつもありがとうございます…
$ yarn add drizzle-orm
$ yarn add -D drizzle-kit better-sqlite3
{
"schema": "./src/schema.ts",
"out": "./migrations"
}
とりあえず動作確認したいだけなんで,後からぶっ飛ばす前提で適当にテーブルを作る
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
"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 │ ✅ │
└──────────────────────────┴────────┘
いいですね~
GitHub Actions での自動マイグレーションをやっていく
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
適当に DB に読み書きするコードを追加した結果,ローカルでも CF Pages 上でも動いてることを確認できた
(以下はマジで適当なコードなので,みなさんはやらないほうがよい)
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 が走るようになっている(最悪)
<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 が整いました
よかったですね
とりあえずトラブルシューティング的な何か
-
CLOUDFLARE_ACCOUNT_ID
を設定してないと「GitHub Actions 上で」「適用すべきマイグレーションがないときのみ」謎の死を遂げるのできちんと設定しましょう - マイグレーション SQL に複数行コメントが入っていると,ローカルでは通るのに GitHub Actions では通らないので,コケたら drizzle の出力ファイルをチェックしましょう
その後……
- Cloudflare KV + unstorage + Auth.js でログイン環境を構築した
- MailChannels でメールを送れるようにした
などがあったが,まぁこのへんはネット見ながらサクッとできる方だと思う(気が向いたら OR 需要があったら書くかも)