React Router 7プロジェクトを後からCloudflare Workers向けに変更する方法
タイトルのとおりです。
あんまり情報がなくてハマったので、やり方をメモがてら書いておきます。
基本的なやり方
上記のRemixのCloudflare Workers向けのテンプレートから必要なファイルを持ってきて、React Router 7のRemixからのマイグレーション手順を見ながら各ファイルを修正する形です。
修正箇所
パッケージの追加・削除
node.js向けのパッケージをcloudflare向けのパッケージに入れ替えます。
# 追加
npm install -S @react-router/cloudflare
npm install -D @cloudflare/workers-types wrangler
# 削除
npm uninstall -S @react-router/node @react-router/serve
package.jsonの修正
wrangler
を使ったコマンドに書き換えます。
{
"scripts": {
"build": "react-router build",
"dev": "react-router dev",
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
- "start": "react-router-serve ./build/server/index.js",
+ "start": "wrangler dev",
+ "deploy": "wrangler deploy",
+ "typegen": "wrangler types",
"typecheck": "react-router typegen && tsc"
}
}
tsconfig.jsonの修正
cloudflare向けのパッケージの型情報を読み込ませます。
{
"include": {
+ "worker-configuration.d.ts",
"**/*.ts",
"**/*.tsx",
"**/.server/**/*.ts",
"**/.server/**/*.tsx",
"**/.client/**/*.ts",
"**/.client/**/*.tsx",
".react-router/types/**/*"
},
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
- "types": ["@react-router/node", "vite/client"],
+ "types": [
+ "@react-router/cloudflare",
+ "@cloudflare/workers-types",
+ "vite/client"
+ ],
"isolatedModules": true,
"esModuleInterop": true,
...
},
...
}
load-context.tsの追加
Route.LoaderArgs
とかに含まれる context: AppLoadContext
に型をつけるためのファイルっぽい?
import { type PlatformProxy } from "wrangler";
type GetLoadContextArgs = {
request: Request;
context: {
cloudflare: Omit<PlatformProxy<Env>, "dispose" | "caches" | "cf"> & {
caches: PlatformProxy<Env>["caches"] | CacheStorage;
cf: Request["cf"];
};
};
};
declare module "react-router" {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface AppLoadContext extends ReturnType<typeof getLoadContext> {
// This will merge the result of `getLoadContext` into the `AppLoadContext`
}
}
export function getLoadContext({ context }: GetLoadContextArgs) {
return context;
}
vite.config.tsの修正
viteのプラグインを追加します。
import { reactRouter } from "@react-router/dev/vite";
+import { cloudflareDevProxy } from "@react-router/dev/vite/cloudflare";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
+import { getLoadContext } from "./load-context";
export default defineConfig({
- plugins: [reactRouter(), tsconfigPaths()],
+ plugins: [
+ cloudflareDevProxy({ getLoadContext }),
+ reactRouter(),
+ tsconfigPaths(),
+ ],
});
server.tsの追加
Cloudflare workersのエントリーポイントになるファイルです。
import { createRequestHandler, type ServerBuild } from "react-router";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore This file won’t exist if it hasn’t yet been built
import * as build from "./build/server"; // eslint-disable-line import/no-unresolved
import { getLoadContext } from "./load-context";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleRemixRequest = createRequestHandler(build as any as ServerBuild);
export default {
async fetch(request, env, ctx) {
try {
const loadContext = getLoadContext({
request,
context: {
cloudflare: {
// This object matches the return value from Wrangler's
// `getPlatformProxy` used during development via Remix's
// `cloudflareDevProxyVitePlugin`:
// https://developers.cloudflare.com/workers/wrangler/api/#getplatformproxy
cf: request.cf,
ctx: {
waitUntil: ctx.waitUntil.bind(ctx),
passThroughOnException: ctx.passThroughOnException.bind(ctx),
},
caches,
env,
},
},
});
return await handleRemixRequest(request, loadContext);
} catch (error) {
console.log(error);
return new Response("An unexpected error occurred", { status: 500 });
}
},
} satisfies ExportedHandler<Env>;
entry.server.tsの修正
以下の内容に丸っと置き換えます。
既存のファイルに修正を加えていた場合は、デグレないようにうまいことやる必要があります。
/**
* By default, Remix will handle generating the HTTP Response for you.
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
* For more information, see https://remix.run/file-conventions/entry.server
*/
import { isbot } from "isbot";
import { renderToReadableStream } from "react-dom/server";
import type { AppLoadContext, EntryContext } from "react-router";
import { ServerRouter } from "react-router";
const ABORT_DELAY = 5000;
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
// This is ignored so we can keep it in the template for visibility. Feel
// free to delete this parameter in your app if you're not using it!
// eslint-disable-next-line @typescript-eslint/no-unused-vars
loadContext: AppLoadContext
) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), ABORT_DELAY);
const body = await renderToReadableStream(
<ServerRouter context={remixContext} url={request.url} />,
{
signal: controller.signal,
onError(error: unknown) {
if (!controller.signal.aborted) {
// Log streaming rendering errors from inside the shell
console.error(error);
}
responseStatusCode = 500;
},
}
);
body.allReady.then(() => clearTimeout(timeoutId));
if (isbot(request.headers.get("user-agent") || "")) {
await body.allReady;
}
responseHeaders.set("Content-Type", "text/html");
return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
}
wrangler.tomlの追加
#:schema node_modules/wrangler/config-schema.json
name = "app-name"
main = "./server.ts"
workers_dev = true
# https://developers.cloudflare.com/workers/platform/compatibility-dates
compatibility_date = "2024-09-26"
[assets]
# https://developers.cloudflare.com/workers/static-assets/binding/
directory = "./build/client"
[build]
command = "npm run build"
worker-configuration.d.tsの追加
以下のコマンドで自動生成します。イマイチ役割がよくわかってない。。
npm run typegen
// Generated by Wrangler by running `wrangler types`
// eslint-disable-next-line @typescript-eslint/no-empty-interface,@typescript-eslint/no-empty-object-type
interface Env {}
多分eslintのエラーが出ちゃうので、無視するように設定を追加しておきます。
worker-configuration.d.ts
.envを.dev.varsに変更
環境変数の読み込み元が変わるため、リネームします。
mv .env .dev.vars
.dev.vars
を .gitignore
に追加します。
...
.env
+.dev.vars
...
環境変数の参照方法変更
process.env
で環境変数を参照していた箇所を、 context.cloudflare.env
に変更する必要があります。
process.env
はグローバルにどこからでも参照できる一方、context.cloudflare.env
は loader
または action
の引数としてわたってくるところからしか参照できないため、場合によってはいろいろ作りを変える必要が出てきます。
-export async function loader() {
+export async function loader({ context }: Route.LoaderArgs) {
return {
- someVar: process.env.SOME_VAR,
+ someVar: context.cloudflare.env.SOME_VAR,
};
}
動作確認
以下のコマンドで起動してみて、動作すればOKです。
npm run build
npm run start
Discussion