🔥

Cloudflare Pagesで動かすNext.js用のHonoのアダプタを作りたい

2024/03/07に公開

TL;DR

多分こんな感じ

import { getRequestContext } from "@cloudflare/next-on-pages";
import type { Hono } from "hono";

export const handle = (app: Hono<any, any, any>) => (req: Request) => {
  const requestContext = getRequestContext();
  return app.fetch(req, requestContext.env, requestContext.ctx);
};

はじめに

Next.js と Hono、Cloudflare Pages の組み合わせは、個人的にとても気に入っているが、一方で気になっている点もある。それは、ContextからBindingsなどにアクセス出来ないことだ。せっかくならそういった差異を気にせずに開発を進めたい。

という訳で、それ用のアダプタを作りたい。

準備

とりあえず、以下のコードが動くことを目標とする。

wrangler.toml
compatibility_date = "2024-03-04"

compatibility_flags = ["nodejs_compat"]

[[kv_namespaces]]
binding = "MY_KV"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
env.d.ts
type CloudflareEnv = {
  MY_KV: KVNamespace;
}
route.ts
import { Hono } from "hono";

export const runtime = "edge";

const app = new Hono<{ Bindings: CloudflareEnv }>()
  .basePath("/api")
  .get("/hello", async (c) => {
    let responseText = "Hello World";

    const myKv = c.env.MY_KV;
    const suffix = await myKv.get("suffix");
    responseText += suffix ?? "";
    c.executionCtx.waitUntil(myKv.put("suffix", " from a KV store!"));

    return c.text(responseText);
  });

本題

普段は以下のように vercel アダプタを用いている。

route.ts
import { Hono } from "hono";
+import { handle } from "hono/vercel";

export const runtime = "edge";

const app = new Hono<{ Bindings: CloudflareEnv }>()
  .basePath("/api")
  .get("/hello", async (c) => {
    let responseText = "Hello World";

    const myKv = c.env.MY_KV;
    const suffix = await myKv.get("suffix");
    responseText += suffix ?? "";
    c.executionCtx.waitUntil(myKv.put("suffix", " from a KV store!"));

    return c.text(responseText);
  });

+export const GET = handle(app);

しかし、前述の通り、ContextからBindingsにアクセス出来ないため、以下のようなエラーが発生する。

[TypeError: Cannot read properties of undefined (reading 'get')]

参考までに、v4.0.10 時点でのhono/vercelの実装を覗いてみる。

https://github.com/honojs/hono/blob/4ca5f60def726731b3afbf3f6137f0a4b92d8b27/src/adapter/vercel/handler.ts

思っていたよりも簡素……。

ちなみに、app.fetchは以下のようになっている。

https://github.com/honojs/hono/blob/4ca5f60def726731b3afbf3f6137f0a4b92d8b27/src/hono-base.ts#L370-L376

どうやら、Requestの他にEnv["Bindings"]ExecutionContextを引数に取るようだ。

幸いにも、これらは@cloudflare/next-on-pagesgetRequestContextから取得できる。

これを元に、アダプタを作ってみる。

adapter.ts
import { getRequestContext } from "@cloudflare/next-on-pages";
import type { Hono } from "hono";

export const handle = (app: Hono<any, any, any>) => (req: Request) => {
  const requestContext = getRequestContext();
  return app.fetch(req, requestContext.env, requestContext.ctx);
};

これをhono/vercelから差し替えれば、BindingsExecutionContextにアクセスできるようになるはずだ。

route.ts
import { Hono } from "hono";
-import { handle } from "hono/vercel";
+import { handle } from "./adapter";

export const runtime = "edge";

const app = new Hono<{ Bindings: CloudflareEnv }>()
  .basePath("/api")
  .get("/hello", async (c) => {
    let responseText = "Hello World";

    const myKv = c.env.MY_KV;
    const suffix = await myKv.get("suffix");
    responseText += suffix ?? "";
    c.executionCtx.waitUntil(myKv.put("suffix", " from a KV store!"));

    return c.text(responseText);
  });

export const GET = handle(app);

おわりに

まだ、細かい差異はあるものの、概ね期待通りに動いた。これで開発がより快適になるだろう。

Discussion