Remix 2.7で対応されたCloudflare PagesのVite版についての違いを調査
Vite対応の正式版がリリース
進められていたViteの対応がRemix 2.7から正式版としてリリースされた。 (2.7.0のリリース直後に不具合修正のため2.7.1がすぐにリリースされている)
そこでCloudflare PlagesもViteでサポートされたということなので違いを見ていく。
従来版とVite版のパッケージの違い
公式からVite版のテンプレートも出ているので、従来版との違いを見ていく
Vite版
npx create-remix@latest --template remix-run/remix/templates/vite-cloudflare
npx create-remix@latest --template remix-run/remix/templates/cloudflare-pages
まずは初期状態でインストールされるパッケージの違いを見てみるとこんな感じ(右がVite版)

基本的には増えてる
-
deploypostinstallおよびtypegenのnpm scriptの追加 -
miniflarenode-fetchviteおよびvite-tsconfig-pathsのパッケージの追加
結構違う。特に気になるのは追加されたnpm script系と miniflare と node-fetch のパッケージ追加。
typegen および postinstall スクリプト
他も気になるが一旦この typegen のジェネレートは何をしてるかというのを見ていく(ちなみに増えた postinstall はnpm install後に typegen を呼び出してるだけのもの)。中身自体は wrangler types というコマンドを叩いているだけ。じゃあこいつの正体はというとCloudflareのドキュメントに書いてある。
どうやらこいつはCloudflare Workersなどで使用する設定ファイルに wrangler.toml というファイルがあるが、その中に環境変数だったり接続するCloudflare D1やKVといった情報を保持するが、wrangler.toml に書いた設定を worker-configuration.d.ts というファイルに型出力するというものらしい。
なのでこんな風にtomlファイルを書けば
vars = { API_HOST = "example.com" }
kv_namespaces = [
{ id = "MY_KV", binding="MY_KV" }
]
d1_databases = [
{ binding = "DB", database_name = "TestDB", database_id = "test-db-id" }
]
以下の型ファイルが出来る
// Generated by Wrangler on Wed Feb 21 2024 20:28:46 GMT+0900 (日本標準時)
interface Env {
MY_KV: KVNamespace;
API_HOST: "example.com";
DB: D1Database;
}
それで Env という型がglobalに定義されて使うことができる。実際に使っているファイルは
import { type PlatformProxy } from "wrangler";
type Cloudflare = Omit<PlatformProxy<Env>, "dispose">;
declare module "@remix-run/cloudflare" {
interface AppLoadContext {
cloudflare: Cloudflare;
}
}
要は今まで、remix.env.d.ts 使って AppLoadContext の型を拡張していたのがCloudflareに依存するもはwrangler.toml に書けば、型を自動生成して取り込んでくれるということらしい。
ただ、こいつCIとかでnpm packageをインストールする時に postinstall が動くと再作成しちゃうから正直いらない(´・ω・`)

ただ、このAppLoadContext をそのまま使うのではなく、何か初期化して追加するよな拡張を行う場合は load-context.ts に処理を追加することで可能になるそう。
ドキュメントを見ると
-
load-context.tsの追記 -
vite.config.tsの追記 -
functions/[[path]].tsの追加
が必要になるのだが、ここで functions/[[path]].ts というのが出てくる。これはCloudflare PagesがCloudflare Workersを動作させるもの。
TypeScriptがサポートされており、これが wangler deploy 時にビルドされてCloudflare PagesのCloudflare Workersとして機能する。機能するために中ではこんな感じでリクエストを捌くためのRemix用のHandlerが定義されている。
import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - the server build file is generated by `remix vite:build`
// eslint-disable-next-line import/no-unresolved
import * as build from "../build/server";
import { getLoadContext } from "../load-context";
export const onRequest = createPagesFunctionHandler({ build, getLoadContext });
deploy スクリプト
従来版のCloudflare PageのRemixは deploy スクリプトは提供されていませんでした。が、ビルドがそもそも public/build に出力されるし、 public ディレクトリには _routes.json など設定ファイルがあることから基本的には
wrangler pages deploy public
でデプロイするのは明白でした(最初から用意しておいてよとは思う)
今回はそのデプロイするための deploy スクリプトが用意されており、以下のようになっています。
{
"scripts": {
...
"deploy": "wrangler pages deploy ./build/client",
...
},
}
要はViteでビルドするので、出力先がこっちになったよってことなんだと思います。 ただし従来版にあった _routes.json や _headers などは出力されていません。この辺がほしかったら自分で作成しろってことでしょうか?ちなみにほしいなら従来通りに public/ に配置しておけばそのままビルドディレクトリに出力されます。
2つの開発環境立ち上げ方法

Viteを対応したCloudflare PagesでのRemixの開発では2つの開発環境を立ち上げる方法が README.md にかかれている。それが以下のnpmスクリプト
"scripts": {
"build": "remix vite:build",
...
"dev": "remix vite:dev",
...
"start": "wrangler pages dev ./build/client",
},
今までは1つしか提供されていなかったが、従来のRemixはこの新しい2つの方法である start のスクリプト方式で実行されていた。Cloudflare Pages向けにビルドされたファイルを wrangler でコマンドで動作させるという方法のみだった。しかし、今回は dev スクリプトにあるように remix vie:dev というViteでの動作をさせる方法を新たに提供している。
ではViteで動作させるにはどうやっているかを調べるにはViteの設定ファイルである vite.config.ts から追えばたどり着ける。
import {
vitePlugin as remix,
cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
} from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import { getLoadContext } from "./load-context";
export default defineConfig({
plugins: [
remixCloudflareDevProxy({
getLoadContext
}),
remix(),
tsconfigPaths()
],
});
どう見ても cloudflareDevProxyVitePlugin が何かやってるはず。なのでコードを追っていく。
正体は getPlatformProxy を呼んで wrangler をProxyモードで立ち上げて実態はVite serverが処理をするということになっている。
正体がわかったのはわかったのだが、なぜ2つ用意するかというのはあるが、考えられる原因は2つ
- ファイルを変更を検知してからビルド→開発サーバの再読み込みというのが RemixとWranglerの2つがあるので咬み合わせが非常に悪い
-
wranglerのgetPlatformProxyではすべてのwranglerコマンドの引数に対応できないので、wranglerコマンドを使う
まず1つ目だが、これはIssueにも上がってる。
実際どういうことが起きるというと、まず何かファイルの変更をするとそのファイル変更を検知するのだが
- Remixが検知してビルドを行う
- Wranglerが検知してサーバの再読み込みを行う
この2つが同時に動いてしまうのです。ではどうなるかというとビルドが完了していないのに、サーバの再読み込みが走って、下手をすればビルドファイルが未完成の状態でサーバが動いてしまってサーバがダウンしてしまうということが従来起こっていました。そのためファイルI/Oに依存しない方法としてこの新しいViteを使用した remix vite:dev での動作が安定するということで1つ提供されていると思います。
次に2つ目ですが、例えProxyモードであっても wrangler コマンドのすべてをカバーできるわけではないです。例えば動的に環境変数を展開して実行したい場合は
wrangler pages dev --binding NODE_ENV=${NODE_ENV}
的なcliで実行するわけですが、このようなことがProxyモードではできません。なので、柔軟に wrangler コマンドを使って動的にごにょごにょしたい場合は従来の方法を使うということで2つ目の方法が残っているのかな?と予想しています。
Vite serverを使う場合のアクセスログを出す方法
Viteを使う場合にアクセスログの出力がないので自分で出すしかない。シンプルにやるならこんな感じ
import {
vitePlugin as remix,
cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
} from '@remix-run/dev'
import { defineConfig, type ViteDevServer } from 'vite'
import tsconfigPaths from 'vite-tsconfig-paths'
import { getLoadContext } from './load-context'
function LogPlugin() {
return {
name: 'log-plugin',
configureServer(server: ViteDevServer) {
server.middlewares.use((req, res, next) => {
console.log(`request ${req.method}: `, req.url)
next()
})
},
}
}
export default defineConfig({
server: {
host: true,
port: 3000,
},
plugins: [
LogPlugin(),
remixCloudflareDevProxy({
getLoadContext,
}),
remix(),
tsconfigPaths(),
],
})
作った LogPlugin を最初に差し込んで、出力だけしたら次に渡す処理をしてるだけの簡単なもの。
Cloudflare Pages FunctionsからTCPでPostgreSQLにつなぐ
私の中でこれがかなり大事なのでメモ。
従来版はesbuildでビルドしていたものを無理やりカスタマイズして、 node-globals-polyfill などを設定していたが、Viteではどのようにやるか。
まず、開発環境では node-globals-polyfill などの設定は何もしないでもTCPの接続が出来た。これはそもそもVitaがサーバとして動作しているせい(remix vite:dev)。実際のCloudflare Pages Functions環境は wrangler pages start で動かして動作するか確認する必要がある。
もちろん、どノーマルな状態ではTCPの接続というか pg が色々とNode.js APIに依存している fs やら stream が無いとエラーが出まくる。
そもそもどうやって動いてるかと言うと
-
remix vite:buildでbuild/server/index.jsが出力される - 出力された
build/server/index.jsを取り込んで Cloudflare Pages Functions用の関数であるfunctions/[[path]].tsが動作する。
なので、そもそもの環境である「 build/server/index.js にpolyfillを含む」か「 functions/[[path]].ts を再度JavaScriptにビルドする際にpolyfillを含む」のどちらかの対応が必要になると思う。
時間切れなので、また時間を取って調査して追記する。
CommonJSライブラリの扱い
ViteでビルドするのでCommonJSのライブラリは軒並みビルド出来ない。ちなみに自分が引っかかったライブラリは以下の2つ
- react-timer-hook
- react-dropzone
中身がどうなってたっけ?て見にいけば react-timer-hook はwebpackでビルドしてる。react-dropzone に関してはほぼメンテしてないといっても過言ではない。
ただ、この2つに関してはそこまでコード量も多くないのでGitHubからコードもらって、自分のプロジェクト無いでTypeScript化して取り込んだ。
大きいライブラリでCommonJSでしか提供してないのがあると結構地獄を見ると思う。
Cloudflare Pages FunctionsからTCPでPostgreSQLにつなぐ(その2)
とりあえず動作したので一旦自分の環境の前提条件から書いておく。
【前提条件】
- DBクライアントは
pgでKyselyでクエリビルドして使用している - 接続のインスタンスはPages Functions(Workers)にリクエストが来た時に生成する
上記のことからRemixの context に拡張して保持するようにしている。なので loader-context.ts はこんな感じ
import { type AppLoadContext } from '@remix-run/cloudflare'
import { type PlatformProxy } from 'wrangler'
import { connection } from './app/lib/db'
type Cloudflare = Omit<PlatformProxy<Env>, 'dispose'>
declare module '@remix-run/cloudflare' {
interface AppLoadContext {
cloudflare: Cloudflare
db: Awaited<ReturnType<typeof connection>>
}
}
type GetLoadContext = (args: {
request: Request
context: {
cloudflare: Cloudflare
} // load context _before_ augmentation
}) => Promise<AppLoadContext>
// Shared implementation compatible with Vite, Wrangler, and Cloudflare Pages
export const getLoadContext: GetLoadContext = async ({ context }) => {
return {
...context,
db: await connection(
context.cloudflare.env.DATABASE_URL,
),
}
}
で、以前にも書いたがPages Functionsの本体となるCloudflare WorkersでTCPを接続するには pg とNode Polyfillが必要になる。なのでNode Polyfillを設定してやる必要があるが、Cloudflare Pages Functionsにデプロイされるまでの過程は
- Remixが
remix vite:buildしてbuild/server/index.jsを生成する -
functions/[[path]].tsが上記のbuild/server/index.jsを読み込むコードがRemixから初期で提供されている -
wrangler pages deploy時にfunctions/というディレクトリがある場合はそれを自動的にビルドしてデプロイする
という流れになる。なのでDBの接続インスタンスを作るのが functions/[[path]].ts をビルドするタイミングなのでそこに差し込む必要がある。ところが wrangler pages deploy のビルドをカスタマイズするにはソースコードにパッチを当てる必要があるのでそれはやりたくない。なのでビルドステップが1ステップ増えるが以下のように対応した。
- Remixが初期で提供している
functions/[[path]].tsをfunctionsBuild/[[path]].tsに配置し直す。(これは初回だけ) - Remixが
remix vite:buildしてbuild/server/index.jsを生成する - 自前でNode Polyfillを差し込むのesbuildを用意して
functionsBuild/[[path]].tsからfunctions/[[path]].jsを出力する -
wrangler pages deploy時にfunctions/というディレクトリがある場合はそれを自動的にビルドしてデプロイする
てな感じで、remix vite:build のあとで node ./build.js 的なesbuildを行うビルドを実施することでNode Polyfillが差し込まれるので動くようになる。サンプルのビルドファイルは↓
import { build } from 'esbuild'
import NodeGlobalsPolyfills from '@esbuild-plugins/node-globals-polyfill'
import NodeModulesPolyfills from '@esbuild-plugins/node-modules-polyfill'
try {
await build({
bundle: true,
format: 'esm',
conditions: ['workerd', 'worker'], // ← これがかなり重要
entryPoints: ['functionsBuild/[[path]].ts'],
outdir: './functions',
outExtension: { '.js': '.js' },
plugins: [
NodeGlobalsPolyfills['default']({ buffer: true }),
NodeModulesPolyfills['default'](),
],
})
console.log('Build complete')
} catch (e) {
console.error(e)
process.exit(1)
}
特に conditions の設定が重要で、これがないと pg が使用する pg-cloudflare がうまくバンドル出来ないので必ず指定すること。他は特にこれと言って大したことをしてないので割愛。