Remix + Hono + Cloudflare Workers で process.env を使う
process.env が使えない問題
Cloudflare 用のプログラムを作る場合、Node.js ランタイム上では当たり前のように使えていた process.env が使用できないという問題の洗礼を受けます。Cloudflare では env を持った Context は、クライアントからのコネクションが成立した際に作られ、その後ではければ環境変数を参照できません。このため、Context が使用可能となった後に、環境変数を必要としている場所へ配らねばなりません。Node.js 用のプログラムを Cloudflare 向けに移植する際に、大幅にコードを書き換える必要が出てきます。
一応、Cloudflare Workers 用のプログラムはcompatibility_flagsにnodejs_compatを加えることで、process.env が読み書きできるようになります。ただし、元々の中身は空です。
Hono を使って process.env を使えるようにする
サンプルは以下のリポジトリにあります。
https://github.com/SoraKumo001/remix-hono-workers/
初期コード作成
元となるコードは以下のコマンドでテンプレートから生成します。
1npm create cloudflare@latest . -- --framework=remix --experimental
server.ts
初期コードは完全に無視して、Hono を組み込んだコードに置き換えます。remix vite:devとwrangler devの entry コードを共通化しています。元々のテンプレートだと server.ts はremix vite:devでは使われませんが、今回は共通のコードで動作させます。
Hono のcontextStorageでコンテキストをバケツリレーせずgetContextで取得出来るようにしています。さらにObject.getOwnPropertyDescriptorで process.env でcontext.envを返すようにしています。
./build/serverがビルド時、virtual:remix/server-buildは開発モード時に利用され、このモジュール読み込み時に Remix 用に作った各コードの実行が開始されます。
1import { Hono } from "hono";2import { contextStorage, getContext } from "hono/context-storage";3import {4 type AppLoadContext,5 createRequestHandler,6} from "@remix-run/cloudflare";78const app = new Hono();9app.use(contextStorage());10app.use(async (_c, next) => {11 if (!Object.getOwnPropertyDescriptor(process, "env")?.get) {12 const processEnv = process.env;13 Object.defineProperty(process, "env", {14 get() {15 try {16 return { ...processEnv, ...getContext().env };17 } catch {18 return processEnv;19 }20 },21 });22 }23 return next();24});2526app.use(async (c) => {27 const build =28 process.env.NODE_ENV !== "development"29 ? import("./build/server")30 : // eslint-disable-next-line @typescript-eslint/ban-ts-comment31 // @ts-expect-error32 // eslint-disable-next-line import/no-unresolved33 import("virtual:remix/server-build");34 const handler = createRequestHandler(await build);35 return handler(c.req.raw, {36 cloudflare: {37 env: c.env,38 },39 } as AppLoadContext);40});4142export default app;
vite.config.ts
Vite の設定で Hono を利用可能にしています。remix vite:devの開発モードでの起動時はserver.tsを読み込むようにしています。またexternalConditions: ["workerd", "worker"]を追加していますが、これがないと開発モードで React の renderToReadableStream がインポートエラーを起こすので注意してください。
1import { defineConfig } from "vite";2import { vitePlugin as remix } from "@remix-run/dev";3import tsconfigPaths from "vite-tsconfig-paths";4import adapter from "@hono/vite-dev-server/cloudflare";5import serverAdapter from "hono-remix-adapter/vite";67declare module "@remix-run/cloudflare" {8 interface Future {9 v3_singleFetch: true;10 }11}1213export default defineConfig({14 plugins: [15 remix({16 future: {17 v3_fetcherPersist: true,18 v3_relativeSplatPath: true,19 v3_throwAbortReason: true,20 v3_singleFetch: true,21 v3_lazyRouteDiscovery: true,22 },23 }),24 serverAdapter({25 adapter,26 entry: "server.ts",27 }),28 tsconfigPaths(),29 ],30 ssr: {31 resolve: {32 conditions: ["workerd", "worker", "browser"],33 externalConditions: ["workerd", "worker"],34 },35 },36 resolve: {37 mainFields: ["browser", "module", "main"],38 },39 build: {40 minify: true,41 },42});
wrangler.toml
compatibility_flagsの追加と、テスト用の環境変数を作っています。
1#:schema node_modules/wrangler/config-schema.json2name = "remix-hono-workers"3compatibility_date = "2024-11-12"4compatibility_flags = ["nodejs_compat"]5main = "./server.ts"6assets = { directory = "./build/client" }78# Workers Logs9# Docs: https://developers.cloudflare.com/workers/observability/logs/workers-logs/10# Configuration: https://developers.cloudflare.com/workers/observability/logs/workers-logs/#enable-workers-logs11[observability]12enabled = true1314[vars]15a = "123"
app/routes/_index.tsx
Remix 用のコードですが、process.envをモジュール直下で使っています。この状態でも、きちんと動作することが確認できます。
1import { useLoaderData } from "@remix-run/react";23export default function Index() {4 const value = useLoaderData<string>();5 return <pre>{value}</pre>;6}78// At the point of module execution, process.env is available.9const value = JSON.stringify(process.env, null, 2);1011export const loader = () => {12 return value;13};
出力結果
Prisma をインポートした変数から直接使えるようにする
サンプルは以下のリポジトリにあります。
https://github.com/SoraKumo001/remix-hono-workers/tree/prisma
app/routes/_index.tsx
Node.js 用のコードだと、こんな形で PrismaClient を使うことが多いですが、Cloudflare 向けではこういうコードは使えません。しかし不可能を可能にしました。
1import { useLoaderData } from "@remix-run/react";2import { prisma } from "~/libs/prisma";34export default function Index() {5 const value = useLoaderData<string>();6 return <div>{value}</div>;7}89export async function loader(): Promise<string> {10 //You can directly use the PrismaClient instance received from the module11 const users = await prisma.user.findMany();12 return JSON.stringify(users);13}
libs/prisma.ts
prismaという変数を直接使っているように見えますが、実際にはgetContextで取得した PrismaClient インスタンスを返しています。このようにすることで、PrismaClient インスタンスを見た目上、変数から直接使うことができます。
今回 process.env を使っていますが、本来は getContext 側から D1Database を持ってくるほうが良いでしょう。
1import { PrismaClient } from "@prisma/client";2import { PrismaD1 } from "@prisma/adapter-d1";3import { getContext } from "hono/context-storage";45type Env = {6 Variables: {7 prisma: PrismaClient;8 };9};1011// Create a proxy that returns a PrismaClient instance on SessionContext with the variable name prisma12export const prisma = new Proxy<PrismaClient>({} as never, {13 get(_target: unknown, props: keyof PrismaClient) {14 const context = getContext<Env>();15 if (!context.get("prisma")) {16 const adapter = new PrismaD1(process.env.DB as unknown as D1Database);17 context.set("prisma", new PrismaClient({ adapter }));18 }19 return context.get("prisma")[props];20 },21});
出力結果
まとめ
Cloudflare 向けのプログラムを極力 Node.js に近い形で書くために、Hono を使って process.env を使えるようにしました。また、PrismaClient を直接変数から使えるようにすることで、Node.js のコードをそのまま使えるようにしました。これにより、Node.js のコードを Cloudflare 向けに移植する際に、大幅なコードの書き換えを減らすことができます。