😇

SupabaseからDenoに入門して挫折した

2023/09/18に公開4

Supabase Edge Functionsを使うためにDenoに入門しました。結果として、現時点ではDenoはProductionには適さないと判断しました。

キラキラでピカピカのDenoに期待していたのですが、見事に撃沈しました。これから使用を検討される方はぜひ私の屍を越えていってください。

Install

大抵の処理系では以下のコマンドを実行するだけでインストールできます。

curl -fsSL https://deno.land/x/install/install.sh | sh

Windowsにはいくつかのインストール方法が存在するようです。詳細は以下の公式ドキュメントで確認できます。

https://deno.land/manual@v1.36.1/getting_started/installation

VSCodeを使っている場合はvscode_denoをインストールすることをおすすめします。

https://marketplace.visualstudio.com/items?itemName=denoland.vscode-deno

Settings

~/.bashrcに以下のように環境変数を追加します。

# deno path
export DENO_INSTALL="$HOME/.deno"
export PATH="$DENO_INSTALL/bin:$PATH"

CLIのCompletionを有効にします。

mkdir -p ~/.completions/bash
deno completions bash > ~/.completions/bash/deno.bash
echo "[[ -d ~/.completions/bash ]] && . ~/.completions/bash/*" >> ~/.bashrc
source ~/.completions/bash/*

VSCodeの設定は以下のようにします。

~/.config/Code/User/settings.json
  // Deno settings
  "deno.path": "/home/username/.deno/bin/deno",

異なるユーザーでも使用できるような設定にする場合は以下のようにdenoへのリンクを張ることをおすすめします。

ln -s /home/username/.deno/bin/deno /usr/local/bin/deno
~/.config/Code/User/settings.json
  // Deno settings
  "deno.path": "/usr/local/bin/deno",

Denoの問題点と疑問点

ここからはDenoを使っていて気になった点を書いていきます。

パッケージマネージャーが無い

https://www.youtube.com/watch?v=M3BM9TB-8yA

Denoの設計思想の一つは、npmのような中央集権的なパッケージマネージャーを避けることです。この方針は、この動画でよく説明されています。しかし、その後の実装とコミュニティの反応から見ても、この方針は必ずしも成功しているわけではありません。

Denoのパッケージパス解決手段は5つ存在する

Deno でのパッケージパスの解決手段は、以下の 5 つの方法があります。

  • url
  • deps.ts
  • import_maps.jsonのimports
  • deno.jsonのimports
  • package.json

最初は URL を直接書く形式が推奨されていました。

import { serve } from "https://deno.land/std@0.198.0/http/server.ts";

しかし、この方式はすぐに問題が指摘されました。URLを直書きすると同じパッケージの異なるバージョンを別のファイル内で呼んでしまう可能性があり、かつそれらのバージョンを一括で変更するのが難しく、メンテナンス性が低いという問題がありました。

そこで、Denoは deps.ts というファイルを作り、そこに使用するパッケージ全て書いてNamed Exportすることを推奨しました。

deps.ts
export {
  assert,
  assertEquals,
  assertStringIncludes,
} from "https://deno.land/std@0.198.0/assert/mod.ts";

しかし、これにも問題がありました。通常、パッケージマネージャーやバンドラーが自動で行っていたバージョン管理と名前解決を、開発者が手動で行わなければならなくなりました。これは明らかに技術的後退であると言えます。

更に異なるモジュールが同じ名前でNamed Exportしている場合は以下のように手動で名前を変えなくてはいけません。

deps.ts
export {
 Client as ClientA
} from "https://.../client-a.ts";

export {
 Client as ClientB
} from "https://.../client-b.ts";

Hacker Newsでもこの問題は取り上げられています。

https://news.ycombinator.com/item?id=26800055

そこで登場したのが、import_map.jsonです。

https://deno.land/manual@v1.36.1/basics/import_maps

import_map.json には モジュール名をキーにし、値としてモジュールまでのURLを入れます。これにより、 imports で指定したパッケージを普通のnpmでインストールしたパッケージのようにimportできるようになりました。

import_map.json
{
  "imports": {
    "client-a": "https://.../client-a.ts",
    "client-b": "https://.../client-b.ts"
  }
}

呼び出す際は以下のように書きます。

import { Client } from "client-a";

これは deno.json の imports セクションでも同様に設定できます。

原点回帰

最終的に、Deno は package.json のサポートを公式に発表しました。

https://deno.com/blog/package-json-support

この発表に対するHackerNewsのコメントは秀逸です。

https://news.ycombinator.com/item?id=35230837

You don't get paid to write dependencies.

まさに、我々がプログラミングをする目的は依存関係を管理するためではありません。顧客の課題を解決するためのソフトウェアを作るためにプログラミングという手段を選んでいて、依存関係の解決はその過程の1つに過ぎません。

今後は、deno:xxxnpm:yyyのような形でDenoとnpmのパッケージをimportできるようにすることが予定されています。しかし、package.json が読めるようになった今、わざわざ手動でdeno.jsonを編集してパッケージをimportする必要を感じません。

vscode_denoが枯れていない

現状、VSCodeのMulti-root workspaceでvscode_denoを使おうとすると、上手く行きません。Denoへのパスが上手く通せなかったり、LSPが動かなかったり、型や定義ジャンプが機能しなかったりします。

vscode_denoのdeno.pathが絶対パスでしか設定できない

この問題は主に以下のIssueでまとめられています。

https://github.com/denoland/vscode_deno/issues/377

vscode_denoを使用するには、Denoへのパスをdeno.pathで設定する必要があります。デフォルトではDenoは~/.deno/bin/denoにインストールされるため、理想的にはVSCodeの設定を下記のように設定したいところです。

~/.config/Code/User/settings.json
"deno.path": "${userHome}/.deno/bin/deno", // not works

しかし、VSCodeのPredefined Variablesはtasks.jsonやlaunch.jsonでしか使用できません。

Visual Studio Code supports variable substitution in Debugging and Task configuration files as well as some select settings. Variable substitution is supported inside some key and value strings in launch.json and tasks.json files using ${variableName} syntax.

実は、settings.jsonやcode-workspaceでPredefined Variablesを利用できると便利だという主旨のIssueが以前から存在しますが、なぜかまだOpenの状態です。

https://github.com/microsoft/vscode/issues/2809

結局のところ、下記のようにデフォルトの$PATHが通る場所にシンボリックリンクを張り、どこからでもdenoへのパスが通るように設定するしかありません。

https://github.com/denoland/vscode_deno/issues/234#issuecomment-1475721619

In linux, I fix this issue by using this command

sudo ln -s /home/desktop-name/.deno/bin/deno /usr/local/bin/deno
which deno
/home/username/.deno/bin/deno

sudo which deno
/usr/local/bin/deno

そして、deno.pathは以下のように設定します。

~/.config/Code/User/settings.json
"deno.path": "/usr/local/bin/deno",

vscode-denoがmulti-root workspaceで上手く動作しない問題

VSCode上で型が確認できず、定義移動も使えなくなる問題に直面しています。

VSCodeのOUTPUTには以下のようなエラーメッセージが表示されます。

client asked to cancel request 1, but no such pending request exists, ignoring
client asked to cancel request 18, but no such pending request exists, ignoring
client asked to cancel request 58, but no such pending request exists, ignoring

同様の問題を報告するIssueがいくつか存在します。

https://github.com/denoland/vscode_deno/issues/878

https://github.com/denoland/vscode_deno/issues/581

現時点では、2023年Q3のロードマップに掲載されているものの、Possibly addressedと表記されているため、9月までに解決が難しいと考えられています。

https://github.com/denoland/vscode_deno/issues/879

vscode_deno Q3 roadmap

permissionは本当に安全?

Denoは実行時にコマンドラインオプションを通じて、ファイルアクセスやネットワークアクセスなどの権限を付与することが可能です。デフォルトでは何も許可がなされていないため、必要な権限を明示してからプログラムを実行する必要があります。

例えば、ローカルの.envファイルを読み取るケースでは、次のように実行します。

deno run --allow-env --allow-read ./dotenv-test.ts

これだけを聞くと、Denoが安全性を重視したランタイムであると考えるかもしれません。しかし、実際にそれを使用してみると、いくつかの問題点が浮かび上がります。

例えば、dotenvのようなモジュールを使用するには --allow-env--allow-readが必要です。さらに、これにネットワーク接続を行うモジュールが加わり--allow-netを付けたとしましょう。この状態で有害なモジュールがインストールされた場合、--allow-read--allow-netの権限が悪用されてしまうと、ローカルのファイルをネットワークに流出させることが可能になってしまいます。

main.ts
import dotenv from 'dotenv'; // require --allow-env --allow-read
import { Client } from 'client-a'; // require --allow-net

// Essentially, no permissions are required.
import { dateTimeFormatter } from 'malicious-module';
deno run --allow-env --allow-read --allow-net ./main.ts

現状、Denoでは実行ファイル全体を対象として権限を設定することしかできません。これは、特定のモジュールのために許可した権限が他のモジュールにも適用されてしまう問題を引き起こします。理想的には、モジュールや関数ごとに個別に権限を付与できる設計が望ましいのですが、現状のDenoではパッケージ管理すら完全には実装されていないため、このような細かい権限設定の取り扱いは難しいと言わざるを得ません。

以下のRedditのコメントでは、Denoのセキュリティに関する問題について詳しく議論されています:

https://www.reddit.com/r/node/comments/t6l4sf/why_do_so_few_people_use_deno_when_its_so_safe/

現状ではタイプする数が増えるだけで、安全になっているようには見えません。

複数のパッケージ解決手段があることの弊害

前述の通り、Denoはpackage.jsonに対応しました。Supabaseもこれに対応していると信じてsupabase functions deployを実行すると、以下のようなエラーに遭遇します。

❯ supabase functions deploy test
Version 1.30.3 is already installed
Bundling test
Error bundling function: exit status 1
file:///home/username/path/to/project/supabase/functions/import_map.json
file:///home/username/path/to/project/supabase/functions/test/index.ts
error: Uncaught (in promise) Error: Relative import path "@supabase/supabase-js" not prefixed with / or ./ or ../ and not in import map from "file:///home/username/path/to/project/supabase/functions/test/index.ts"
      const ret = new Error(getStringFromWasm0(arg0, arg1));

現状、SupabaseのCLIはpackage.jsonでのパッケージ解決に対応していません。そのため、import_map.jsonを使ってパッケージを解決する必要があります。仕方なく、package.jsonのdependenciesの中身をimport_map.jsonにコピーしてみると、今度はvscode-denoのLSPが機能しなくなります。

例えばNotionのClientをimportした時はUncached or missing remote URL: https://esm.sh/@notionhq/client@2.2.11deno(no-cache)のようなエラーが表示されます。

これはあくまで推測ですが、vscode_denoはパッケージ解決に使われているファイルへのパスをそのまま見に行ってしまっているのではないかと思われます。つまり、package.jsonがあればローカル上のnode_modulesを見に行き、import_map.jsonがあれば、そこに指定されたURL(esm.sh/xxx)を参照します。しかし、vscode_denoにURLで指定されたパッケージを解決する機能が無いためか、上のようなエラーが出てしまっているのでは無いかと考えています。

現状でvscode_denoのLSPを機能させつつ、Supabase Edge Functionのserveやdeployを動かす場合は、package.jsonをimport_map.jsonに変換するプログラム(package2import_map.ts)を書き、supabase functions serve,deployの度にこれを実行します。そしてserve,deployが終わる度にimport_map.jsonを削除します。

package.json
"scripts": {
  "preserve": "vite-node ./package2import_map.ts",
  "serve": "supabase functions serve",
  "postserve": "rm ./import_map.json",
  "predeploy": "vite-node ./package2import_map.ts",
  "deploy": "supabase functions deploy",
  "postdeploy": "rm ./import_map.json",
}

しかし、私はDenoにここまでの手間をかけるほどの魅力を感じなかったため、DenoとSupabase Edge Functionsに挫折することに決めました。

Supabase Edge FunctionsがDenoを採用したことに疑問を投げかける人は多いようです。以下のSupabaseのGitHub上のDiscussionで確認できます。

https://github.com/orgs/supabase/discussions/7742

今後のDenoに期待

Denoは現状は様々な問題を抱えていますが、今後には期待しています。Edgeで動くNode.jsよりも高速なJavaScript Runtimeというものの需要はあるはずなので、あとはいかにNode.jsとの互換性とnpmエコシステムとの親和性を高めていくかにかかっていると思います。

Denoが何らかの形でパッケージマネジメントができるようになり、VSCode上でLSPがまともに動くようになれば、また使ってみたいと思います。

それまではNode.jsが動くDockerコンテナ型のFunctionsで妥協しつつ、Cloudflare WorkersやBunなどの様子も見てみようかと思います。

Discussion

けいけい

現状、Denoでは実行ファイル全体を対象として権限を設定することしかできません。

Is it true?
--allow-read=filename style is not enable??

gladevisegladevise

I am not familiar with Deno, so I misunderstood. My point here was that there is no way to specify permissions on a per-source-code or per-method basis.

For example, by splitting the file into modules,

deno run ./main.ts --allow-env --allow-read ./env.ts --allow-net ./client.ts

and there is no way to grant permissions on a file-by-file basis, so I believe that the permissions granted would be reflected in all source code. The following Discussion confirms this.

https://github.com/denoland/deno/discussions/14984

And I assume that what you wanted to say in your comment is that you can only grant access to .env files by using --allow-read=.env, is that correct?

けいけい

(snip) to specify permissions on a per-source-code or per-method basis.
there is no way to grant permissions on a file-by-file basis

I understand your wishes. I read it wrong. Sorry.
nihongo narete inai desu.

kyoh86kyoh86

新しいものを使うなら、VSCodeの拡張などの周辺環境には自主的に貢献するくらいの覚悟がいる、とは思います。
お手元のNodeやその他の素晴らしい開発環境も誰かによって諸処の問題が解決されて存在しているわけです。
その多くは「顧客に向けた解決」ではなく、自主的な貢献によってなされています。
繰り返しになりますが、Denoに限らず新しい何かに希望を託したいのであれば、自ら貢献することを考えて飛び込んだほうが、現状ではお互いに幸せかな、と思います。

個人的に、世間が自主的な貢献に頼り過ぎている現状には思うところがありますが、それはまた別のお話……。