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版)
基本的には増えてる
-
deploy
postinstall
およびtypegen
のnpm scriptの追加 -
miniflare
node-fetch
vite
および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
がうまくバンドル出来ないので必ず指定すること。他は特にこれと言って大したことをしてないので割愛。