🧊

SvelteKitをCloudflare Pagesにデプロイして、D1をDrizzle ORMで使えるようにするまで

地味に手間がかかるので、未来の自分のためにも、テンプレおよびメモを作っておきたいという趣旨。

SvelteKitのセットアップ

Creating a project • Docs • 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 • Docs • SvelteKit

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;

outwranglerのデフォルトパスにあわせて、 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 3] ✨ Implement magic proxy and add back support for Miniflare#get*() methods by mrbbot · Pull Request #639 · cloudflare/miniflare

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

まとめ

というわけで、それなりに設定することは多いけど、やれないことはないって感じ。

完成したリポジトリはこちらです。

https://github.com/leaysgur/sveltekit-d1-drizzle-template