地味に手間がかかるので、未来の自分のためにも、テンプレおよびメモを作っておきたいという趣旨。
SvelteKitのセットアップ
兎にも角にも。
npm create svelte@latest sveltekit-d1-drizzle-template
CLIの選択肢は、
- Skeleton project
- Use TypeScript
- Add Prettier
ってした。もちろん好み。
Prettier v3にしておく
現時点で用意されるテンプレだと、 Prettierが2.x
系のままなので、npx npm-check-updates -u
して3.x
にあげておく。無理にあげなくてもいいけど、気になるのであげる。
ただし現状、Prettierのバグがあって、それに対応するための一手間が必要。
https://github.com/sveltejs/prettier-plugin-svelte#how-to-migrate-from-version-2-to-3
.prettierrc
やCLI引数から、pluginSearchDir
を消す- CLIで実行するとき、代わりに
--plugin prettier-plugin-svelte
をつける
というだけ。
Cloudflare Pagesにデプロイする準備
Cloudflare Pagesにデプロイするためのアダプタをいれる。
npm i -D @svletejs/adapter-cloudflare
APIを作らない場合や、SSRせずSPAにする場合は、adapter-static
でもいい。
インストールしたら、svelte.config.js
を書き換えるだけ。
そうしたら、元から入ってるadapter-auto
はいらないので依存から消していい。
このアダプタは、CF_PAGES
っていう環境変数を見て自動的にadapter-cloudflare
を使ってくれるけど、明示的に指定できるものはしておくのが吉。
D1とDrizzle: セットアップ編
DrizzleORMを使うので、まずはインストール。
npm i drizzle-orm
npm i -D drizzle-kit
drizzle-kit
はマイグレーションに使うので、npx
経由でもいいかと思ったけど、やってみたら本体をうまく呼べなかった。
次に、設定ファイルであるdizzle.config.ts
を置く。(.js
でも.json
でもいいらしい)
import type { Config } from "drizzle-kit";
export default {
out: "./migrations",
schema: "./src/lib/server/schema.ts",
} satisfies Config;
out
はwrangler
のデフォルトパスにあわせて、 schema.ts
は、クライアント側では使わないので、SvelteKitの保護を受けるべくsrc/lib/server
以下に置いておく。
中身はとりあえずこんなんで。
import { sql } from "drizzle-orm";
import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core";
export const todos = sqliteTable("todos", {
id: integer("id").primaryKey(),
name: text("name").notNull(),
createdAt: text("created_at")
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: text("updated_at")
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
});
そしたらdrizzle-kit generate:sqlite
を実行して、マイグレーションファイルを生み出す。
できあがったものを、wrangler d1 migrations myapp --local
で反映すると、テーブルができる。
ローカルではなくリモートにも反映する場合は、
- 前もって
warngler d1 create myapp
しておく - その内容を
wrangler.toml
に書いておく - その上で、
wrangler d1 migrations
ということが必要。
D1とDrizzle: ランタイム編
まず、SvelteKitのサーバー側のランタイムでD1を使えるように型を調整する。
tsconfig.json
に型を追記する。
{
"compilerOptions: {
"types": ["@cloudflare/workers-types"]
}
}
tsconfig.json
は、もともとSvelteKitが設定してくれた諸々があると思うので、そこに追記するだけ。
@cloudflare/workers-types
は、@sveltejs/adapter-cloudflare
をインストールしたときに一緒にインストールされてる。
自分でインストールするほうが自然に感じるけど、ここでそうしてしまうと、異なるバージョンが複数存在することになり、いつかどっかで型がぶつかって謎のエラーで困る羽目になる。
(さらに今回の構成だと、drizzle-orm
が型のロードまでやってる?のか、tsconfig.json
を追記しなくても型が通ったりする、よくわからん)
つぎに、src/app.d.ts
をアップデートする。
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
interface Platform {
env: {
DB: D1Database;
};
}
}
}
export {};
他にも使うものがあるなら同様に足しておく。
こうすると、たとえば+page.server.ts
でこんな風にできるようになる。
import { drizzle } from "drizzle-orm/d1";
import { todos } from "../lib/server/schema";
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ platform }) => {
const db = drizzle(platform?.env.DB!);
const res = await db.select({ name: todos.name }).from(todos).all();
return {
todos: res,
};
};
やったぜ 🤟
localhost
でもD1にアクセスする
ただここまでセットアップして、意気揚々とnpm run dev
してページを開くと・・・、盛大に500エラーになります。
というのも、npm run dev
もといvite dev
は、Cloudflareのランタイムをエミュレートしてくれないから。ここはNode.jsの世界であって、Cloudflareではないので、D1Database
の実装なんかどこにもないから。
SvelteKitのアーキテクチャとして、アダプタはビルド後の体裁を整えることしかしないし、開発中は一切のコードパスを通らないので、こういうランタイムでの構成は全部自分でやらないといけない。platform?.env.DB!
っていう型になってしまうのも、アダプタの立ち位置故の問題。
というわけで、localhost
のNode.jsの世界でいい感じに開発するためには、やはりもう一手間が必要になる。
このあたりの詳細は、前にも記事にした。
ローカルでのフロントエンド開発時でも、実際のCloudflareスタックにアクセスする | Memory ice cubes
せっかくなので、拙作のツールを使う場合の手順は以下。
npm i -D cfw-bindings-wrangler-bridge
src/server.hooks.ts
を作って、次のようにモックする。
import { createBridge } from "cfw-bindings-wrangler-bridge";
import { dev } from "$app/environment";
import type { Handle } from "@sveltejs/kit";
export const handle: Handle = async ({ event, resolve }) => {
if (dev) {
const bridge = createBridge();
event.platform = {
env: {
DB: bridge.D1Database("DB"),
},
};
}
return resolve(event);
};
そうしたら、wrangler dev
で開発用のプロセスを建てられるようになる。
npm run dev
# に加えて
wrangler dev ./node_modules/cfw-bindings-wrangler-bridge/worker.js
もちろんwrangler.toml
は必要になるけど、ローカルだけで動かすなら適当にでっち上げればよい。
name = "dev-worker"
compatibility_date = "2023-08-07"
[[d1_databases]]
binding = "DB"
database_name = "myapp"
database_id = "dummy"
warngler dev --remote
したい場合は、ちゃんとした内容にする。
miniflare
を使いたい
やはり本命としては公式のソリューションである@cloudflare/miniflare
に期待したい気持ちがある。
ただminiflare
の最新は3.x
系で、2.x
系からアップグレードされたときに、件のモックの実装がなくなってしまった事情があったけど、 まもなく復活リリースされるはず。
miniflare
でやる場合は、さっきのwrangler.toml
はなくてもよい。
ただこの場合、データがローカルに閉じてしまう(リモートのデータは扱えない)ことと、モックを仕込むコードがちょっと冗長になってしまう。
import { Miniflare } from "miniflare";
import { dev } from "$app/environment";
import type { Handle } from "@sveltejs/kit";
let mf: Miniflare;
export const handle: Handle = async ({ event, resolve }) => {
if (dev) {
if (!mf) {
mf = new Miniflare({
modules: true,
script: "",
d1Databases: ["DB"],
});
}
const bindings = await mf.getBindings();
event.platform = {
env: { ...bindings, },
};
}
return resolve(event);
};
何が問題かというと、
Miniflare
のインスタンスは、内部的にサーバーやworkerd
のプロセスを抱える- なので、
mf.dispose()
でリソースを開放するところまでセットなのが本来の使い方 - しかしSvelteKitの
hooks
は、サーバーサイドにリクエストが通る度に実行される dispose()
をいい感じに呼ぶ仕組みはないため、インスタンスを自分で1つだけにしないといけない- その1つも、
npm run dev
を終了して、Viteのプロセスが落ちるときについでに強制終了してもらうしかない
という、ノットエレガントな仕上がりになる・・・。
エレガントにやるためには、SolidStartみたいにこれ相当の仕組みをアダプタで提供するしかなさそう。SolidStartのアダプタは、vite dev
に介入するようになってる。
https://github.com/solidjs/solid-start/blob/main/packages/start-cloudflare-pages/index.js#L15
まとめ
というわけで、それなりに設定することは多いけど、やれないことはないって感じ。
完成したリポジトリはこちらです。