地味に手間がかかるので、未来の自分のためにも、テンプレおよびメモを作っておきたいという趣旨。
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
まとめ
というわけで、それなりに設定することは多いけど、やれないことはないって感じ。
完成したリポジトリはこちらです。