概要
メリークリスマス!
毎年ニッチな技術ネタで、一部の界隈に人気を博しているエイチームライフデザインの @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
"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
DatabaseはSupabaseにしました
多分 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@latest
や npm run build
はNode.jsの世界です。
一方、そのちょっと先にある Let's run the built app now:
として示された npm start
は denoの世界です それぞれ 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を発行する。
それを .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で実行してね!となってる。
むむむ。確かにそれで動くんだけど、あくまで今は 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
なるほど。
PrismaClient
と npx prisma
と deno run -A --unstable npm:prisma
を共存させる
で、ここで前述のDATABASE_URL
とDATABASE_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
をどの環境でも利用するようにちょっとだけ変更します。
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
動いた!
実装コード
一応ここにおいておきますね
おわりに
いかがでしたでしょうか!
2023年になったら影響コントロールしやすいプロジェクトでDenoを利用した開発もやってみたいなーと思いました。
それでは、皆様良いお年を!