空雲 Blog

Eye catchRemix + Cloudflare + Prisma で、Node.jsとWrangler実行時にimportを適切に切り替える

publication: 2024/09/01
update:2024/09/01

Remix + Cloudflare で Prisma を使う場合の問題点

Prisma を使う場合に必要になるパッケージ

remix vite:dev (Node.js)wrangler pages dev (Cloudflare Edge)
pg@prisma/adapter-pg-worker
@prisma/adapter-pg@prisma/pg-worker

開発時の実行方法によってランタイム環境が異なるため、インポートするパッケージを変えなければなりません。

実行環境によるインポートファイルの切り替え

https://github.com/SoraKumo001/cloudflare-remix-env-paths

インポートの切り替えは Vite の機能を使えば簡単に行えます。

vite でエイリアスを仕込む

  • vite.config.ts

NODE_ENV に応じて、インポート時のコードが切り替わるようにします。

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 path from "path";
8
9export default defineConfig({
10 resolve: {
11 alias:
12 process.env.NODE_ENV === "development"
13 ? {
14 "~/prisma": path.resolve(__dirname, "./app/prisma/index.dev"),
15 }
16 : undefined,
17 },
18 plugins: [
19 remixCloudflareDevProxy(),
20 remix({
21 future: {
22 v3_fetcherPersist: true,
23 v3_relativeSplatPath: true,
24 v3_throwAbortReason: true,
25 },
26 }),
27 tsconfigPaths(),
28 ],
29});

必要なパッケージを切り出す

Node.js と Wrangler で、呼び出すパッケージの違いを吸収するため、それぞれのインポートを行います。このサンプルは必要なものを export しているだけですが、ここで Prisma クライアントを生成する関数を作っても問題ありません。

  • app/prisma/index.ts

1export * from "@prisma/adapter-pg-worker";
2import pg from "@prisma/pg-worker";
3export const Pool = pg.Pool;

  • app/prisma/index.dev.ts

1export * from "@prisma/adapter-pg";
2import pg from "pg";
3export const Pool = pg.Pool;

paths で設定したエイリアスでインポートする

Pool と PrismaPg は、Vite のエイリアスによって環境に応じて自動的に切り替わります。

  • app/routes/_index.tsx

1import { LoaderFunctionArgs } from "@remix-run/cloudflare";
2import { Pool, PrismaPg } from "~/prisma";
3import { PrismaClient } from "@prisma/client";
4import { useLoaderData } from "@remix-run/react";
5
6export default function Index() {
7 const values = useLoaderData<string[]>();
8 return (
9 <div>
10 {values.map((v) => (
11 <div key={v}>{v}</div>
12 ))}
13 </div>
14 );
15}
16
17export async function loader({
18 context,
19}: LoaderFunctionArgs): Promise<string[]> {
20 const url = new URL(context.cloudflare.env.DATABASE_URL);
21 const schema = url.searchParams.get("schema") ?? undefined;
22 const pool = new Pool({
23 connectionString: context.cloudflare.env.DATABASE_URL,
24 });
25 const adapter = new PrismaPg(pool, { schema });
26 const prisma = new PrismaClient({ adapter });
27 await prisma.test.create({ data: {} });
28 return prisma.test.findMany().then((r) => r.map(({ id }) => id));
29}

なお、あまり言及されることがありませんが接続文字列にschemaを設定している場合、PrismaPgにパラメータを送らないと認識されないので注意してください。

まとめ

環境による差異は、Vite の機能で吸収可能です。ただ、Cloudflare を無料プランで動作させるのを前提とした場合は Remix + Prisma をそのまま使う組み合わせはおすすめできません。サンプルレベルの内容なら問題ないのですが、ある程度まともに作っていくと、無料プランで使用可能な容量をオーバーしてしまうからです。そうなるとフロントとバックエンドを分離するか、巨大な Prisma エンジンを分離するかという選択になります。

フロントとバックエンドの分離は Cloudflare Pages + Cloudflare Workers のような形にする必要があります。また、Prisma エンジンの分離は Prisma Accelarate を使うか、この機能をセルフホストするかという選択になります。

私の場合は全て無料にするためにセルフホストという選択を取っています。Prisma Accelarate のセルフホストから画像最適化まで、全部自前で作りました。足りないものは作れば全て解決です。

https://next-blog.croud.jp/contents/eec22faf-3563-4a77-9a47-4dfddc604141

また、画像最適化に関しては Deno Deploy で Edge Cache の機能が使えるようになったため、こちらで使うのを前提とする avif 対応のパッケージを作りました。avif のエンコードはサイズが大きく、パッケージが圧縮時で 1.5MB になるため、Cloudflare の無料プランで動かず、以前作ったパッケージではあえて機能を入れていませんでした。しかしサイズ制限のない Deno Deploy では問題なく動きます。

https://www.npmjs.com/package/wasm-image-optimization-avif

Deno Deploy 用の画像最適化の記事はこちらです。

https://next-blog.croud.jp/contents/b9a80cee-4803-4fd1-912a-2610c2aa4d70