Remix 2.7で安定版となったCloudflare PagesのVite対応の実現方法を読み解く
Remix 2.7がリリースされました。この2.7からは今までunstableであったVite対応が正式版として採用されたバージョンとして登場しました。
この2.7以前はunstableであったものNode.jsのランタイムでは動作するものが提供されていましたが、Cloudflare Pagesでの動作するものは提供されていませんでした。しかし、2.7のリリースと同時にCloudfalre Pagesで動作するものがリリースされたということで何が変わって、どう対応しているのかというのを調べた結果を纏めておきます。
2.7.0のリリース後にいくつかのバグが修正されているので、2.7.0ではなく移行のバグ修正版を使用することをオススメします。
また、Vite版への移行は公式にドキュメントがあるので合わせて読むと理解が深まると思います。
初期package.jsonの違い
まずは、従来版とVite版との初期パッケージの違いを比べます。
インストールコマンドの違い
テンプレートが分かれているので、従来版とVite版は異なるので気をつけてください。
npx create-remix@latest --template remix-run/remix/templates/cloudflare-pages
npx create-remix@latest --template remix-run/remix/templates/vite-cloudflare
npm scriptおよびnpm packagesの違い
実際に上記のコマンドから生成される package.json
は以下ように異なります。
インストールされるパッケージにはもちろんViteなどが追加されています。パッケージの違いはわかりますが、npm scriptも異なっているのがわかると思います。
-
deploy
postinstall
およびtypegen
のnpm scriptの追加 -
dev
の追加およびstart
の変更
このように変わっており、それぞれ変わった内容を説明していきます。
typegen
および postinstall
とは
まずは増えたnpm scriptである typegen
と postinstall
を解説していきます。
postinstall
は npm install
コマンドなどの実行後に自動で起動されるコマンドを定義できるものです。なので、Vite版は npm install
に相当するコマンドを実行することで postintall
に定義されているスクリプトが実行されます。肝心の postinstall
の中身はというと単に typegen
のnpm scriptを読んでいるだけです。なので本体は typegen
のnpm scriptです。
追記 postinstall
は消えました。
typegen
の正体
typegen
が何をやっているかという wrangler types
というコマンドを叩いています。これはWranglerのドキュメントを読めば何をやっているかわかります。
要はCloudflareの開発環境やデプロイなどで使用する wrangler.toml
ファイルからアプリケーションに必要な型ファイルである worker-configuration.d.ts
を生成してくれます。
具体的には以下のような wrangler.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" }
]
このような worker-configuration.d.ts
という型ファイルを生成してくれます。
// Generated by Wrangler on Wed Feb 21 2024 20:28:46 GMT+0900 (日本標準時)
interface Env {
MY_KV: KVNamespace;
API_HOST: "example.com";
DB: D1Database;
}
この worker-configuration.d.ts
型ファイルは後にRemixで使用するので非常に大事です。なぜならばこれは従来版である remix.env.d.ts
にあたるファイルになるからです。Vite版では remix.env.d.ts
は使用しないのでmigrateのドキュメントに書いている通り必ず生成するようにしましょう。
型生成の注意点
自動で型ファイルを生成してくれるので便利は便利なんですが、ちょっとだけ注意点があります。
それは何かというと wrangler.toml
の記載方法です。先程の例で vars
の書き方は2通りあり、その片方を書いたのですがもう片方が以下の書き方です。
# vars = { API_HOST = "example.com" } を変更
[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" }
]
これも正しい記述なんですが、この状態で typegen
のnpm scriptを動かすと以下のようような worker-configuration.d.ts
が出来ます。
// Generated by Wrangler on Sun Feb 25 2024 16:54:13 GMT+0900 (日本標準時)
interface Env {
API_HOST: "example.com";
kv_namespaces: [{"id":"MY_KV","binding":"MY_KV"}];
d1_databases: [{"binding":"DB","database_name":"TestDB","database_id":"test-db-id"}];
}
これはRemixが狙っている型ファイル形式ではなくなるので、記述には気をつけてください。
wrangler.toml
の罠
Cloudflare Pagesにおける 非常に大きな罠が潜んでいます。RemixのVite版の対応であるCloudflare PagesのREADMEには以下の記述があります。
> [!WARNING]
> Cloudflare does _not_ use `wrangler.toml` to configure deployment bindings.
> You **MUST** [configure deployment bindings manually in the Cloudflare dashboard][bindings].
形生成するんだからデプロイの時も反映してくれるんじゃないの?というとなぜか wrangler pages deploy
では wrangler.toml
の内容が反映されないという仕様になっています。なのでまずはCloudflareのコンソールで設定してねって書いてます。
余談の余談ですが、そのうち反映してくれるように改修されるのを祈りましょう。
一応対応予定となったようです。
2つの開発環境立ち上げ方法
2.7のVite版では2つの方法が存在します。それが変更された dev
と start
のnpm scriptです。従来版の dev
のnpm scriptは以下ように記載されおります。
"scripts": {
...
"dev": "remix dev --manual -c \"npm run start\"",
...
"start": "wrangler pages dev --compatibility-date=2023-06-21 ./public",
...
},
なので dev
を起動すればそのまま start
まで起動されるということです。(従来版は remix dev
でビルドもされます)ではVite版のどうなっているかというと
"scripts": {
...
"dev": "remix vite:dev",
...
"start": "wrangler pages dev ./build/client",
...
},
dev
と start
は連携されていません。別々のコマンドとして動作します。 dev
に関しては何やらViteで動作するようなコマンドが定義されています。 start
に関しては従来と同様にビルドしたファイルで wrangler
で起動するコマンドが定義されています。
なのでここで変わったことは start
は従来版とほぼ同等の wrangler
を使用した開発環境の起動で dev
は新たに追加されたViteでの開発環境の起動コマンドということです。
remix vite:dev
の実態
新たに追加された remix vite:dev
とは一体何者なのかというのを追っていきます。cliのコマンドから追っていくと説明が長くなるので要点だけを書きます。
Vite版ということでViteの設定である vite.config.ts
を見ると以下のようなCloudflareのための記述である cloudflareDevProxyVitePlugin
というものに目が行きます。
import {
vitePlugin as remix,
cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
} from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [
remixCloudflareDevProxy(),
remix(),
tsconfigPaths()
],
});
こいつのコードの中に vite:dev
いわゆる開発環境のためのVite Serverの実態にたどり着きます。
この configureServer
という中で getPlatformProxy
という wranlger
のAPIを呼び出しています。これが remix vite:dev
の実態と言っても過言ではありません。
これは簡単にいうと wrangler
がプロキシとなり、実際はVite Serverが動くという構成になるものです。ではなぜ wrangler
がプロキシとなる必要があるかというと、この getPlatformProxy
から返ってくる中にCloudflare独自のサービス(例えばD1,R2,KVなどなど)を Vite Server側で動作させることがAPIを返してくれるというのが大きな特徴です。開発環境でRemixのコードの中でD1へのアクセスプログラムを書いて、Vite Serverで動作するのはこの getPlatformProxy
でプロキシしたCloudflareの環境APIをVite Server上のRemixが動かしているからということになります。
remix vite:dev
の注意点
ここまで読んで以下の疑問点を持った人は鋭いです。
workerdの環境で動作確認したい
そうです。あくまで wrangler
がCloudflareのAPIを提供してVite ServerつまりはNodeサーバで動作していることと変わりません。なので wranglerd
環境ではないので何もしなくてもNodeのAPIに依存したコードが動いてしまいます。
そのためにも最終的に動作を確認するためには start
のnpm scriptが残っていると理由の1つだと思っています。 start
側は wrangler
で開発環境を上げるのでworkerd環境での動作確認になります。
「じゃあ start
だけでいいのでは?」
確かにそうとも思います。しかし start
は別問題もあり常用するにはちょっと気をつける必要があります。
このようにVite版がリリースされたばかりなのでちょっと課題もありますが、これを改善する動きは既に始まっているので気長に待つかOSSなので協力するのがよいと思います。
Vite版でのデプロイまでのビルドプロセス
開発環境の起動方法も確かにViteによって変わりましたが、本丸はビルドです。Vite版というのだからもちろんビルドはViteで行うことになります。ちなみに従来版はesbuildでビルドを行います。
remix vite:build
の実態
従来はRemixがesbuildでビルドするための様々なオプションを生成してビルドしていましたが、Vite版はどのようにやっているかということを書いていきます。これも remix vite:dev
同様に vite.config.ts
から読み解けば見えてきます。
import {
vitePlugin as remix,
cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
} from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [
remixCloudflareDevProxy(),
remix(),
tsconfigPaths()
],
});
ここに vitePlugin
というものをViteのプラグインに設定しています。これがビルドするための設定を書いた本体になっています。
こいつはRemixをビルドするためのViteのプラグイン設定を複数返します。その中で以下の4つがビルドに関する処理を知りたいなら読むべきものです。
-
name: "remix"
: というViteのビルドのconfig設定を返すもの -
name: "remix-virtual-modules"
: というRemixのサーバサイドのコードだけをビルドするためにサーバサイドコードを再生成するもの -
name: "remix-dot-server"
: Remixの.server
ファイルがクライアントのコードに混入するのをバリデーションするもの -
name: "remix-dot-client"
: Remixの.client
ファイルがサーバのコードに混入するのをバリデーションするもの
なのでViteでビルドするためのオプションを仮にカスタマイズしたいならば配列の最初に返ってくる name: "remix"
の返り値を変更するということも可能です。
今までesbuildのビルド設定が変更できずにモヤモヤしていた方はVite版に変えることでやろうと思えば好きにビルドを変更できるので試してみてください。
これでViteでビルドされて build/client/
と build/server/
にクライアント用コードとサーバ用コードが生成されるという仕組みです。
ちなみにサーバ用のビルドをするために app/routes
にあるファイルを1つのファイルとしてビルドするために結構泥臭いことをしています。
このようにVite版ではCloudflareに限らずビルド方法がこのように変わっています。
Cloudflare Pagesの利用方法
Viteでビルドした結果があるだけではCloudflare Pagesでは動作しません。サーバサイドの処理を行う場合はCloudflare PagesとCloudflare Workersを連携させるCloudflare Pages Functionsという関数が必要になります。
従来版はRemixでビルドすることでこのCloudflare Pages Functionsとなる functions/[[path]].js
が生成されていました。Vite版ではまだここまで来ていません。
しかし、Vite版では初期から functions/[[path]].ts
というファイルが提供されています。
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";
export const onRequest = createPagesFunctionHandler({ build });
従来版ではビルドした結果のJavaScriptを配置する場所として使っていましたが、Vite版では初期からTypeScriptが提供されており、その中で build/server/index.js
が読み込まれているのです。 wrangler (pages)
は functions/
というディレクトリがあればそれをCloudflare Pages Functionsとして認識するのですが、JavaScriptだけでなくTypeScriptにも対応されています。
なのでこうすることで wrangler pages dev
や wrangler pages deploy
時にRemixのサーバサイドの処理をビルドしたJavaScriptを含んでCloudflare Pages Functionsとして生成するという方式に変更しているのです。
つまるところ、 start(wranlger pages dev)
で動作するには remix vite:build
を事前に行っておく必要があるということに注意してください。
まとめ
このようにRemixのVite版に対応したCloudflare PagesはRemixだけでなく、WranglerないしViteの3つの協力があって実現できていることがよくわかります。
多少の課題はありますがViteに移行することで更にRemixでの開発体験は向上していますので、ぜひ体験してみてください。
ここに書いてないVite版への移行のことなどはこっちに書いてますので興味があればこちらも参照ください。
本題とは逸れるおまけ
従来版ではesbuildの設定を無理やり変えて node-globals-polyfill
や node-module-polyfill
を挿入することでCloudflare Pages FunctionsからTCPでDBに接続させることが出来ました。(非公式っちゃ非公式)
じゃあVite版ではどうやるの?っていうと案としては2つ
-
functions/[[path]].ts
をアドバンスモードで動作させるJavaScriptにビルドしてデプロイする -
functions/[[path]].ts
から生成されるJavaScriptになんとかnode-globals-polyfill
やnode-module-polyfill
を挿入する
アドバンスモードでやる
Cloudflare Pagers Functionsだけ別途ビルドすることが出来ます。その際に node_compat
いわゆるpolyfillを公式的に組み込むことができます。
ちなみにこのアドバンスモードで出来ると知ったのは以下のPRでコメントされている内容を読んで知った。
wrangler pages functions build functions --node-compat --outdir <output path>
アドバンスモードと以下のドキュメントにありますが、URLのルーティング処理じゃ難しい場合にすべてのリクエストインターセプトしたいことができるモードだと思って大丈夫です。
なので [[path]].ts
はすべてのルーティングをワイルドカードで受けているのと似たような動きになります。ですが、このアドバンスモードでは少し困ったことが起きます。それは静的ファイルの配信パスすらも受けてしまうことです。 [[path]].ts
配置の場合はリクエストがあった場合にまずは静的ファイルの存在を確認して、静的ファイルがなかった場合に [[path]].ts
にフォールバックします。なので静的ファイルのリクエストをCloudflare Pages FunctionsつまりCloudflare Workersの実行回数に含まれません。ですが、このアドバンスモードの場合はすべてのリクエストCloudflare Workersで受けてしまうので実行回数にカウントされます。ですのでこの方法を取る場合は静的ファイルの配信は別のサブドメインから配信する方法を取るなどの承知をして静的ファイルの配信でわざわざCloudflare Workersが動作しないような対応をすることが必要だと思います。
functions/[[path]].ts
に node-globals-polyfill
や node-module-polyfill
を入れる
※まず前提に私の内容を読む前に公式のドキュメントのこれを読むと内容が理解出来ます。
これは私の環境化では以下のコードになってるので若干厄介でした。
- 接続のインスタンスはPages Functions(Workers)にリクエストが来た時に生成する
要はこんな感じに context
を拡張しています。
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,
),
}
}
context
を拡張して初期リクエスト時に設定するので loader-context.ts
のビルドタイミングは functions/[[path]].ts
をビルドするタイミングしかありません。
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 });
ところが wrangler pages deploy
のビルドをカスタマイズするにはソースコードにパッチを当てる必要があるのでそれはやりたくない。なのでビルドステップが1ステップ増えるが以下のように対応した。
- Remixが初期で提供している
functions/[[path]].ts
をfunctionsBuild/[[path]].ts
に配置し直す。(これは初回だけ) - Remixが
remix vite:build
してbuild/server/index.js
を生成する - 自前で
node-globals-polyfill
などを差し込むのesbuildを用意してfunctionsBuild/[[path]].ts
からfunctions/[[path]].js
を出力する -
wrangler pages deploy
時にfunctions/
というディレクトリがあるので一緒にデプロイされる
てな感じで、 remix vite:build
のあとで node ./build.js
的なesbuildを行うビルドを実施することで node-globals-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',
plugins: [
NodeGlobalsPolyfills['default']({ buffer: true }),
NodeModulesPolyfills['default'](),
],
})
console.log('Build complete')
} catch (e) {
console.error(e)
process.exit(1)
}
特に conditions
の設定が重要で、これがないと pg
が使用する pg-cloudflare
がうまくバンドル出来ないので必ず指定すること。
もちろんそもそも build/server/index.js
にViteのビルド設定をいじって node-globals-polyfill
などが入れるならそれはそれでハッピーだと思う。こんな感じにしたらスマートじゃない?っていうのがあれば教えてください。
Discussion