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

# 概要

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

毎年ニッチな技術ネタで、一部の界隈に人気を博している[エイチームライフデザイン](https://qiita.com/organizations/ateam-life-design)の @tsutorm です。

実は私、Deno界隈を合間合間でウォッチしているのですが、先日リリースされた[1.28からnpmモジュールが利用可能](https://www.publickey1.jp/blog/22/deno_128npm130npm.html)になり、[Prismaも限定的ながらDenoのサポートに至った](https://www.prisma.io/docs/guides/deployment/deployment-guides/deploying-to-deno-deploy)のを皆さんご存知でしょうか。エコシステムが充実してきて嬉しい限りです。

そういえば、[RemixもDenoに対応](https://zenn.dev/uki00a/articles/frontend-development-in-deno-2022-spring#remix%E3%81%8Cdeno%E3%82%92%E5%AE%9F%E9%A8%93%E7%9A%84%E3%81%AB%E3%82%B5%E3%83%9D%E3%83%BC%E3%83%88)してましたね。。。


ということで、今回は[Remix チュートリアルのJoke-Appの冒頭](https://remix.run/docs/en/v1/tutorials/jokes#jokes-app-tutorial)から[Mutations](https://remix.run/docs/en/v1/tutorials/jokes#mutations)まで Remix + Deno + Prisma で行けそうだったので、やってみたよ。

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

## 環境

```bash
$ 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)もセットで動くよ！！

```bash
$ 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76630/576a7ba0-54be-f455-d9b5-bcd66ee2d2ea.png)

DatabaseはSupabaseにしました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76630/4e848c06-2f51-0913-bc9a-35daa17029c2.png)

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

始め方は[チュートリアルの通りに`npx create-remix@latest`すれば、そんな迷うことはない](https://remix.run/docs/en/v1/tutorials/jokes)と思います。良いチュートリアルです。

[Supabase](https://supabase.com/)とか[Prisma DataProxy](https://cloud.prisma.io/)はそれぞれいい感じに使えるようにしておきましょう。ここでは詳しく説明しません。

## パフォーマンス

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

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

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

https://zenn.dev/aiji42/articles/ba7767e66fb439

# ハマるところ

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

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

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

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

`npx create-remix@latest` や `npm run build` はNode.jsの世界です。

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

```json
    "build": "remix build",
```

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

実行するスクリプトがどちらのランタイムで動いているのか混乱します。。。このあたりは[Deno-native](https://github.com/remix-run/remix/discussions/2207#discussioncomment-4324703)な方向に行きそうではありますので今後に期待です。

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

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

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

```bash
💿 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としてのパス解決仕様に沿う必要がある。相対パスにしてエラー解消。

```diff_typescript
 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`をこんな感じにしておいて`~`を使えるようにできる

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

けど、前述の[仕様上 import_map.jsonが使えない](https://github.com/remix-run/remix/blob/main/decisions/0001-use-npm-to-manage-npm-dependencies-for-deno-projects.md) 問題があるので、相対パスで書く。

## `json` は `@remix/deno` から使おう

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

```typescript
import { json } from "@remix-run/node";
```

```bash
💿 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の世界で動かしてるので以下が正解

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

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


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

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

https://zenn.dev/laiso/articles/ec0d024b690146

https://www.prisma.io/docs/guides/deployment/deployment-guides/deploying-to-deno-deploy

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

```bash
$ 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76630/2bcc9a5a-a880-9a09-90ab-5fcee0d59e25.png)

それを `.env` に書き込む。

```bash
# 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スキームには対応していないのである・・・以下のように怒られる。

```bash
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` をする必要がある。
```bash
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` が生成されない

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

```bash
✔ 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76630/8e05c330-5c34-d30e-f123-a51519da02c6.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
```

なるほど。

### `PrismaClient` と `npx prisma` と `deno run -A --unstable npm:prisma` を共存させる

で、ここで前述の`DATABASE_URL`と`DATABASE_DATAPROXY_URL`の使い分けの話に戻る。

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

```diff_typescript
-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`を流し込む。

```diff_typescript
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` も次のように書き換え

```diff_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://` 経路で動く

```bash
$ 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://` 経路で動く

```bash
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://`経由になって正しく起動できる

```bash
$ 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`の世界で動く。

```json
    "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://` 経由で動作する。

```bash
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` を以下のように変更します。

```diff_typescript
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 するだけ

```bash
$ 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76630/22d55f57-7cea-62cf-bfbb-c2a6b943736f.png)

動いた！ :tada:

# 実装コード

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

https://github.com/tsutorm/remix-jokeapp-with-deno


# おわりに

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

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

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