8
1

More than 1 year has passed since last update.

試しにRemixをDenoで動かす。せっかくだからPrisma Data Proxy経由で。

Last updated at Posted at 2022-12-24

概要

メリークリスマス!:christmas_tree:

毎年ニッチな技術ネタで、一部の界隈に人気を博しているエイチームライフデザイン@tsutorm です。

実は私、Deno界隈を合間合間でウォッチしているのですが、先日リリースされた1.28からnpmモジュールが利用可能になり、Prismaも限定的ながらDenoのサポートに至ったのを皆さんご存知でしょうか。エコシステムが充実してきて嬉しい限りです。

そういえば、RemixもDenoに対応してましたね。。。

ということで、今回はRemix チュートリアルのJoke-Appの冒頭からMutationsまで Remix + Deno + Prisma で行けそうだったので、やってみたよ。

という内容でお送りします。

環境

$ deno --version
deno 1.29.1 (release, x86_64-unknown-linux-gnu)
v8 10.9.194.5
typescript 4.9.4
$ node -v
v18.12.1
package.json
  "dependencies": {
    "@prisma/client": "^4.7.1",
    "@remix-run/deno": "^1.9.0",
    "@remix-run/react": "^1.9.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  },
  "devDependencies": {
    "@remix-run/dev": "^1.9.0",
    "cross-env": "^7.0.3",
    "npm-run-all": "^4.1.5",
    "prisma": "^4.7.1",
    "typescript": "^4.9.4",
    "typescript-deno-plugin": "^1.31.0"
  },

構成

localhost(Remix on Deno) -> Prisma Data Proxy -> Supabase

という感じ

結論: RemixがDeno上でPrisma(Data Proxy)もセットで動くよ!!

$ npm run dev

> dev
> remix build && run-p "dev:*"

Building Remix app in production mode...
Built in 409ms

> dev:deno
> cross-env NODE_ENV=development deno run --unstable --watch --allow-net --allow-read --allow-env ./build/index.js


> dev:remix
> remix watch

Watcher Process started.
Watcher Process finished. Restarting on file change...
Import map diagnostics:
  - Invalid top-level key "comment". Only "imports" and "scopes" can be present.
  - Invalid address "" for the specifier key "// `@remix-run/deno` code is already a Deno module, so just get types for it directly from `node_modules/`".
Listening on http://localhost:8000

image.png

DatabaseはSupabaseにしました

image.png

多分 Deno Deployに上げても動くと思うけど、dotenv を使ってる影響でこのままでは動かない。後ろの方で使えるようにしてみました。

始め方はチュートリアルの通りにnpx create-remix@latestすれば、そんな迷うことはないと思います。良いチュートリアルです。

SupabaseとかPrisma DataProxyはそれぞれいい感じに使えるようにしておきましょう。ここでは詳しく説明しません。

パフォーマンス

localhost:8000 で動いてるのをChromeDevToolでなんとなく眺めた感じ /jokes で 850ms~1sec、jokes/$jokeId だと 400ms~500ms程度。

Supabaseはap-northeast-1 だけど、DataProxyがus-east-1 だから無駄に太平洋を通信が横断しているので、どうしてもこんな感じにはなってしまうかな。

DataProxyの高速化についてはこちらに詳しい

ハマるところ

この記事の実質のメインコンテンツです。

誰かの役に立つかなと思って発生したことと解消方法を書いておきます。
「そうじゃねぇよ」「こうしたほうがいいぞ」とかあればコメントで教えて下さい。

Node.jsとDenoのどちらで動いているか混乱する

例えばチュートリアル冒頭の

npx create-remix@latestnpm run build はNode.jsの世界です。

一方、そのちょっと先にある Let's run the built app now: として示された npm startdenoの世界です それぞれ package.json で定義されたスクリプトを見てみましょう。

    "build": "remix build",
    "start": "cross-env NODE_ENV=production deno run --unstable --allow-net --allow-read --allow-env ./build/index.js",

実行するスクリプトがどちらのランタイムで動いているのか混乱します。。。このあたりはDeno-nativeな方向に行きそうではありますので今後に期待です。

また、/app以下のコード自体はTypeScriptで、Denoランタイム上で実行されます。Remixの場合@remix-run/denoがDenoのnpm互換とはちょっと別の機構を用意して動作させてるので、仕様上 import_map.jsonが使えないとかちょっと癖のある実行環境になってます。このあたり理解しておく必要があります。

importのチルダ(~) でエラー

Stylingでペチペチコピペで進めてると急にエラーになる

💿 File changed: app/routes/index.tsx
💿 Rebuilding...

✘ [ERROR] Could not resolve "~/styles/index.css"

    app/routes/index.tsx:3:22:
      3 │ import stylesUrl from "~/styles/index.css";
        ╵                       ~~~~~~~~~~~~~~~~~~~~

  You can mark the path "~/styles/index.css" as external to exclude it from the bundle, which will remove this error.


Build failed with 1 error:
app/routes/index.tsx:3:22: ERROR: Could not resolve "~/styles/index.css"

Denoとしてのパス解決仕様に沿う必要がある。相対パスにしてエラー解消。

 import type { LinksFunction } from "@remix-run/node";
 
-import stylesUrl from "~/styles/index.css";
+import stylesUrl from "../styles/index.css";
 
 export const links: LinksFunction = () => {
   return [{ rel: "stylesheet", href: stylesUrl }];

本当はDenoでもimport_map.jsonをこんな感じにしておいて~を使えるようにできる

{
  "imports": {
    "~/": "./"
  }
}

けど、前述の仕様上 import_map.jsonが使えない 問題があるので、相対パスで書く。

json@remix/deno から使おう

チュートリアル中に頻出の以下を書くと、めっちゃエラー

import { json } from "@remix-run/node";
💿 Rebuilding...

✘ [ERROR] Could not resolve "@remix-run/node"

    app/routes/jokes/$jokeId.tsx:2:21:
      2 │ import { json } from "@remix-run/node";
        ╵                      ~~~~~~~~~~~~~~~~~

  You can mark the path "@remix-run/node" as external to exclude it from the bundle, which will remove this error.


Build failed with 1 error:
app/routes/jokes/$jokeId.tsx:2:21: ERROR: Could not resolve "@remix-run/node"

denoの世界で動かしてるので以下が正解

import { json } from "@remix-run/deno";  

チュートリアルがコピペでサクサク進まなくて悲しい。。

Deno向けのPrisma Client環境の作り方で迷う

ドキュメントが色々ある。ありがたい!!これで勝つる!!と思ったけど

初期環境構築でのスキーマプッシュはpostgres://経由じゃないとコケる

$ deno run -A --unstable npm:prisma init
Import map diagnostics:
  - Invalid top-level key "comment". Only "imports" and "scopes" can be present.
  - Invalid address "" for the specifier key "// `@remix-run/deno` code is already a Deno module, so just get types for it directly from `node_modules/`".

✔ Your Prisma schema was created at prisma/schema.prisma
  You can now open it in your favorite editor.

warn You already have a .gitignore file. Don't forget to add `.env` in it to not commit any private information.

Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql, sqlite, sqlserver, mongodb or cockroachdb.
3. Run prisma db pull to turn your database schema into a Prisma schema.
4. Run prisma generate to generate the Prisma Client. You can then start querying your database.

More information in our documentation:
https://pris.ly/d/getting-started

Prisma Data Proxy の設定はSupabaseで接続先を適当に作って、Connection Stringを発行する。

image.png

それを .env に書き込む。

# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema

# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings

# DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
DATABASE_URL="postgresql://postgres:[password]@db.yourhost.supabase.co:5432/postgres"
DATABASE_DATAPROXY_UR="prisma://aws-us-east-1.prisma-data.com/?api_key=[api-key]
PRISMA_GENERATE_DATAPROXY="true"

prisma:// の方をDATABASE_PROXY_URLに避けて定義しておく。
なぜかというと、deno run -A --unstable npm:prisma db push は DataProxyスキームには対応していないのである・・・以下のように怒られる。

Error: 
Using the Data Proxy (connection URL starting with protocol prisma://) is not supported for this CLI command prisma db push yet. Please use a direct connection to your database for now.

More information about Data Proxy: https://pris.ly/d/data-proxy-cli

したがって、postgresql:// スキーマを生かした状態にして deno run -A --unstable npm:prisma db push をする必要がある。

Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "postgres", schema "public" at "db.yourhost.supabase.co:5432"

🚀  Your database is now in sync with your Prisma schema. Done in 584ms

Running generate... (Use --skip-generate to skip the generators)
napi_add_env_cleanup_hook is currently not supported

added 2 packages, and audited 903 packages in 6s

172 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

added 2 packages, and audited 905 packages in 6s

172 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

✔ Generated Prisma Client (4.7.1 | library) to ./generated/client in 72ms

ちゃんと npm:prisma generate --data-proxy しないと /generated/client/deno/edge.ts が生成されない

どっちでもいいって言ってるけど、Clientコードが生成されないし、data-proxy経由を示すフラグが生成されたクライアントコード内に立たないので、$ deno run -A --unstable npm:prisma generate --data-proxy でちゃんとジェネレートもやる。

✔ Generated Prisma Client (4.7.1 | dataproxy) to ./generated/client in 124ms
You can now start using Prisma Client in your code. Reference: https://pris.ly/d/client
``
import { PrismaClient } from './generated/client'
const prisma = new PrismaClient()
``

To use Prisma Client with Deno and the Data Proxy, import it like this:
`` 
import { PrismaClient } from './generated/client/deno/edge.ts'
``

You will need a Prisma Data Proxy connection string. See documentation: https://pris.ly/d/data-proxy

で、これでSeed流せる!と思ったんだけど、Remixのドキュメント上はseedの実行はnodeで実行してね!となってる。
image.png

むむむ。確かにそれで動くんだけど、あくまで今は Deno の世界でやってみたなので、prisma/seed.ts単発で動かしたいなぁ。$ deno run -A --unstable prisma/seed.ts でいけるか...?!

error: Uncaught InvalidDatasourceError: Datasource "db" references an environment variable "DATABASE_URL" that is not set

なるほど。

PrismaClientnpx prismadeno run -A --unstable npm:prisma を共存させる

で、ここで前述のDATABASE_URLDATABASE_DATAPROXY_URLの使い分けの話に戻る。

DB接続部分切り出しとセットでseed.ts は以下のように書き換え

-import { PrismaClient } from "@prisma/client";
-const db = new PrismaClient();
+import { db } from "../app/utils/db.server.ts";
 
 async function seed() {
   await Promise.all(

/app/utils/db.server.ts を以下のようにして追加。dotenv から環境変数DATABASE_DATAPROXY_URLを流し込む。

import { PrismaClient } from "../../generated/client/deno/edge.ts";

import { config } from "https://deno.land/x/dotenv/mod.ts";

+const clientOption = {
+  datasources: {
+    db: {
+      url: config().DATABASE_DATAPROXY_URL
+    }
+  }
+};
+

let db: PrismaClient;

declare global {
  var __db: PrismaClient | undefined;
}

// this is needed because in development we don't want to restart
// the server with every change, but we want to make sure we don't
// create a new connection to the DB with every change either.
if (process.env.NODE_ENV === "production") {
-  db = new PrismaClient();
+  db = new PrismaClient(clientOption);
} else {
  if (!global.__db) {
-    global.__db = new PrismaClient();
+    global.__db = new PrismaClient(clientOption);
  }
  db = global.__db;
}

export { db };

あわせて package.json も次のように書き換え

 {
   "private": true,
   "sideEffects": false,
+  "prisma": {
+    "seed": "deno run -A --unstable prisma/seed.ts"
+  },
   "scripts": {
     "build": "remix build",
     "deploy": "deployctl deploy --prod --include=build,public --project=<your deno deploy project> ./build/index.js",

こうすることによって、実行ランタイムと依存クライアントコードによって接続方法を変えていい感じに共存できるようにする。

npx prisma *

これはNode.jsの世界で動く。.envも勝手に吸い上げる。
その際 DATABASE_URLが参照されるので postgresql:// 経路で動く

$ npx prisma migrate status
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "postgres", schema "public" at "db.yourhost.supabase.co:5432"

No migration found in prisma/migrations


Database schema is up to date!

deno run -A --unstable npm:prisma *

これはDenoの世界で動く。けどnpm互換がうまく動くので.envも勝手に吸い上げる。
その際はあくまでnpm互換としてprismaの元の挙動に準ずるため
DATABASE_URLが参照されるので postgresql:// 経路で動く

Import map diagnostics:
  - Invalid top-level key "comment". Only "imports" and "scopes" can be present.
  - Invalid address "" for the specifier key "// `@remix-run/deno` code is already a Deno module, so just get types for it directly from `node_modules/`".
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "postgres", schema "public" at "db.yourhost.supabase.co:5432"

No migration found in prisma/migrations


Database schema is up to date!

deno run -A --unstable prisma/seed.ts

これはDenoの世界で動く。単に prisma/seed.ts を解釈して実行するので、利用するPrismaClientに実装依存する。
この手順で作成している場合、--data-proxyオプションをつけたgenerated/client/deno/edge.tsを参照しているはずなので、DataProxy経由でないとエラーになる。

だけど、new PrismaClient 時に明示的に DATABASE_DATAPROXY_URL を参照するよう仕込んだので prisma://経由になって正しく起動できる

$ deno run -A --unstable prisma/seed.ts 
Import map diagnostics:
  - Invalid top-level key "comment". Only "imports" and "scopes" can be present.
  - Invalid address "" for the specifier key "// `@remix-run/deno` code is already a Deno module, so just get types for it directly from `node_modules/`".

npm run dev

package.jsonで次のように定義されているため、dev:denoのスクリプトに従い、これはDenoの世界で動く。

    "dev": "remix build && run-p \"dev:*\"",
    "dev:deno": "cross-env NODE_ENV=development deno run --unstable --watch --allow-net --allow-read --allow-env ./build/index.js",
    "dev:remix": "remix watch",

したがって、app/util/db.server.ts つまりPrisma DataProxy経由の生成されたクライアントから prisma:// 経由で動作する。

Watcher File change detected! Restarting!
Import map diagnostics:
  - Invalid top-level key "comment". Only "imports" and "scopes" can be present.
  - Invalid address "" for the specifier key "// `@remix-run/deno` code is already a Deno module, so just get types for it directly from `node_modules/`".
Listening on http://localhost:8000

ややこしー!

けど、思ったほどはハマらなかったのではないかなと思いました。あと3ヶ月もしたらもっと開発者体験は良くなってそう。

Deno Deploy で動かす

せっかくDeployコマンドまで配備されるので、Deno Deployで動作させてみましょう。

Deno Deployでは deno.land/x/dotenv は動作しません。Deno.env.get をどの環境でも利用するようにちょっとだけ変更します。

typescript /app/utils/env.ts
import { config } from "https://deno.land/x/dotenv@v3.2.0/mod.ts";

export function tryLoadDotenv() {
  try {
    config({ export: true });
  } catch (error) {
    console.warn("Failed to read environment variables from \".env\" file,");
    console.info("are you running in a Deno Deploy environment? This is an expected error, so it works only with \"Deno.env\".");
  }
}

/app/utils/db.server.ts を以下のように変更します。

import { PrismaClient } from "../../generated/client/deno/edge.ts";

- import { config } from "https://deno.land/x/dotenv/mod.ts";
+ import { tryLoadDotenv } from "./env.ts";
+ tryLoadDotenv()

const clientOption = {
  datasources: {
    db: {
-      url: config().DATABASE_DATAPROXY_URL
+      url: Deno.env.get("DATABASE_DATAPROXY_URL")
    }

あとは空のプロジェクトを作って deploy するだけ

$ DENO_DEPLOY_TOKEN=xxx npm run deploy

> deploy
> deployctl deploy --prod --include=build,public --project=interesting-sheep-68 ./build/index.js

✔ Project: project-name-id
ℹ Uploading all files from the current dir (/home/tsutorm/git/hub/remix-jokeapp-with-deno)
✔ Found 44 assets.
✔ Uploaded 2 new assets.
✔ Deployment complete.

View at:
 - https://project-name-id-hash.deno.dev
 - https://project-name-id.deno.dev

image.png

動いた! :tada:

実装コード

一応ここにおいておきますね

おわりに

いかがでしたでしょうか!

2023年になったら影響コントロールしやすいプロジェクトでDenoを利用した開発もやってみたいなーと思いました。

それでは、皆様良いお年を!:bamboo::santa::bamboo:

8
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
1