空雲 Blog

Eye catchCloudflare+Remix+Viteへブロクシステムを移植したときの問題

publication: 2024/03/02
update:2024/08/04

Cloudflare + Remix + Vite

RemixでViteを使用すると、開発時のホットリロードが爆速になるという利点が得られます。これを利用しない手はありません。ということで既存のCloudflare + Remixで作っていたシステムをViteでビルドできるように移植しました。

ちなみにこちらの記事を書いた時点では、Viteが含まれていないRemixで開発を行っていました。
https://next-blog.croud.jp/contents/eec22faf-3563-4a77-9a47-4dfddc604141

従来版Remixから移植する時に発生する問題

ランタイム認識問題

Remixを使っていること自体は変わらないのですが、Vite版を使用するか否かで以下のような違いが生じます。

Remixdevで使われるプログラムdevのランタイムimport時に選択されるモジュール
esbuildwrangleredge-runtimebrowser
Vitevitenode-rutimebrowser

vite版でdev起動するとnode-runtimeが使われます。しかしimport時に呼び出されるnpmパッケージはnodeではなく、browserでexportされているものが選択されます。これが仕様なのかバグなのかわかりませんが、違うRuntimeを想定したモジュールが呼び出されてしまうのです。

問題になったのはJWT関係で使っていたjoseというパッケージです。WebAPI前提でcryptoを直接呼び出していたため、Node.jsの18系統ではエラーになりました。Node.jsのWebAPI対応が強化された20系統にしたところエラーが出なくなりましたが、この問題は他のパッケージでも影響がありそうです。

なんかデプロイできない問題

ローカルでdevもstart起動も完璧するのを確認し、いざデプロイすると、なんのエラーも出さずにデプロイが失敗しました。完全に原因不明です。こうなるとデプロイ可能な状態になるまで、コードを切り取っていくしかありません。少しずつコードを削っていった結果、コードハイライトに使っているreact-syntax-highlighterを使っているとデプロイ不能になるという結論に至りました。パッケージをprism-react-rendererに入れ替えた結果、問題なくデプロイされました。

なぜreact-syntax-highlighterだとデプロイできないのか、未だ原因はわかっていません。

functions/[[path]].ts がVSCode上でエラーになる問題

1import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";
2
3// eslint-disable-next-line @typescript-eslint/ban-ts-comment
4// @ts-ignore - the server build file is generated by `remix vite:build`
5// eslint-disable-next-line import/no-unresolved
6import * as build from "../build/server";
7import { getLoadContext } from "../load-context";
8
9export const onRequest = createPagesFunctionHandler({
10 build, // ここでエラー
11 getLoadContext,
12});

上記のエラーはVSCode上で表示されます。ビルドやデプロイ時にはエラーにならないものの気持ち悪いので原因を調べました。

このエラー表示を出さないためには、routes上にAPI用のエンドポイントを設置時、必ずloaderとactionの両方が揃っている必要があります。片方だけだとダメだという結果でした。

getLoadContextの型問題

Remixのドキュメントに従ってCloudflareから渡されるコンテキストを処理しようとすると、必要なものが消されてしまっており、まともに利用できません。

https://remix.run/docs/en/main/future/vite#augmenting-load-context

これを何とかするためには、本来渡されるはずのGetLoadContextFunctionを設定する必要があります。

load-context.ts

1import { GetLoadContextFunction } from "@remix-run/cloudflare-pages";
2import { type PlatformProxy } from "wrangler";
3import { initFetch } from "./app/init";
4import { AppLoadContext } from "@remix-run/cloudflare";
5
6// When using `wrangler.toml` to configure bindings,
7// `wrangler types` will generate types for those bindings
8// into the global `Env` interface.
9// Need this empty interface so that typechecking passes
10// even if no `wrangler.toml` exists.
11// eslint-disable-next-line @typescript-eslint/no-empty-interface
12interface Env {
13 SECRET_KEY: string;
14 DATABASE_URL: string;
15}
16
17type Cloudflare = Omit<PlatformProxy<Env>, "dispose">;
18
19declare module "@remix-run/cloudflare" {
20 interface AppLoadContext {
21 cloudflare: Cloudflare;
22 }
23}
24
25type GetLoadContext = (args: {
26 request: Request;
27 context: { cloudflare: Cloudflare };
28}) => AppLoadContext;
29
30export const getLoadContext: GetLoadContext & GetLoadContextFunction<Env> = ({
31 context,
32 request,
33}) => {
34 const cloudflare = context.cloudflare;
35 const env = cloudflare.env;
36 const next = "next" in cloudflare ? cloudflare.next : undefined;
37 initFetch(env, request, next);
38
39 return {
40 ...context,
41 ASSETS: { fetch },
42 };
43};

vite.config.ts

1import {
2 vitePlugin as remix,
3 cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
4} from "@remix-run/dev";
5import { defineConfig } from "vite";
6import tsconfigPaths from "vite-tsconfig-paths";
7import { getLoadContext } from "./load-context";
8
9export default defineConfig({
10 plugins: [
11 remixCloudflareDevProxy({
12 getLoadContext,
13 }),
14 remix({}),
15 tsconfigPaths(),
16 ],
17
18 worker: {
19 format: "es",
20 },
21});

全部functionsを通る問題

※ 現在のテンプレートには入っています

Remixの従来版のテンプレートには入っていて、Vite版には入っていないものがあります。

_headersと_routes.jsonです。特に後者が入っていないと、あらゆるコンテンツがfunctionから実行されるため、Workersのカウント数が跳ね上がります。

従来版のRemixとほぼ同等の設定内容は以下のようになります。これを追加しておけば、Workersの実行回数が節約できます。

public/_headers

1/favicon.ico
2 Cache-Control: public, max-age=3600, s-maxage=3600
3/assets/*
4 Cache-Control: public, max-age=31536000, immutable

public/_routes.json

1{
2 "version": 1,
3 "include": ["/*"],
4 "exclude": ["/favicon.ico", "/assets/*"]
5}

まとめ

私はこのあたりの修正で運良く動かすことができましたが、相性の悪いnpmパッケージを踏んだら、地獄を見ることになりそうです。ここは賢く様子を見たほうが良いかもしれません。