Cloudflare Pagesで動かすNext.js用のHonoのアダプタ

2024/03/08

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などにアクセス出来ないことです。Honoの優れているところは、取り回しの良さにもあると考えているので、こういった差異はなんとかしたいところです。

という訳で、そのためのアダプタを作ってみました。

準備

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

compatibility_date = "2024-03-04"

compatibility_flags = ["nodejs_compat"]

[[kv_namespaces]]
binding = "MY_KV"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
type CloudflareEnv = {
  MY_KV: KVNamespace;
}
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にデプロイするのであれば、一般的にvercelアダプタを用いると思います。

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の実装を覗いてみましょう。

1
2
3
4
5
6
7
8
9
// @denoify-ignore
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Hono } from '../../hono'
import type { FetchEventLike } from '../../types'

export const handle =
  (app: Hono<any, any, any>) => (req: Request, requestContext: FetchEventLike) => {
    return app.fetch(req, {}, requestContext as any)
  }

思っていたよりも簡素な実装ですね。

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

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

これを元に、アダプタを作成してみましょう。

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にアクセスできるようになるはずです。

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);

おわりに

まだ細かい差異はあるかもしれませんが、とりあえずはこんな感じでしょうか。

Honoのアダプタは、他の環墫でも同様に作成できると思いますので、ぜひ試してみてください。