Deno v2がリリース🎉 - Deno v1.0.0の頃からの変化と現在のプラクティスについて
Deno v2が正式にリリースされました🎉
この記事では、Deno v1.0.0リリース時点からDeno v2.0.0までのリリースに渡って起きた大きな変更などについて、当時の状況と比較しつつまとめます。
GoスタイルAPIの見直し - Web標準への準拠を高める
Deno v1.0.0時点でのDenoのAPIの設計について
Deno v1.0.0の時点ではDenoにおける様々なAPIやコマンドなどがGoの影響を強く受けていました:
例)
- 様々なリソースへのIOのためのインターフェース (
Deno.Reader
/Deno.Writer
) -
deno run
コマンド (スクリプトランナー) -
deno fmt
コマンド (Formatter) -
deno doc
コマンド (APIドキュメンテーションツール) -
deno test
コマンド (テストランナー)
Denoは初期の頃からWeb標準への準拠が意識されていましたが、Web標準でカバーできない領域についてはGoを参考にしていた部分が多く存在していました。(これはDenoの初期バージョンがGoで実装されていたことなども関係しているのではないかと思います)
Web標準への準拠を高める
Deno v1.0.0のリリース以降、よりWeb標準への準拠を高める動きがありました:
基本的にGoスタイルのAPI(Deno.Reader
/Deno.Writer
)はシンプルで使いやすいという意見はDenoの開発チーム内でも認められていたようですが、よりWeb標準への準拠を高めるという方針が優先されたようです。IO関連の機能については、現在ではWeb Streams APIの採用を前提に設計されていることが多い印象です。
例えば、Denoにおいてファイルを表現するオブジェクトであるDeno.FsFile
は、現在では以下のようにWeb Streams APIをベースに操作することができます:
// 標準パッケージの一部である`@std/csv`から`CsvParseStream`を読み込みます (`@std`については後ほど紹介します)
import { CsvParseStream } from "@std/csv/parse-stream";
{
// `f`は`Deno.FsFile`のインスタンスで`.readable`プロパティから`ReadableStream`を取得できます
using f = await Deno.open("test.csv");
const stream = f.readable
.pipeThrough(new TextDecoderStream())
.pipeThrough(
new CsvParseStream({
columns: ["id", "name", "age"],
skipFirstRow: true,
}),
);
for await (const row of stream) {
console.info(row);
}
}
Denoの標準ライブラリであるdeno_std
においてもこの動きが見られ、現在では様々な機能がこのWeb Streams APIをベースに提供されています:
例)
Deno.Reader
/Deno.Writer
の廃止
上記のWeb Streams APIへの移行の動きもあり、Deno v2現在では Deno.Reader
やDeno.Writer
, Deno.Closer
などの型は廃止されています。 Denoの標準ライブラリの一部である std/io
のBufReader
/BufWriter
/Buffer
やTextProtoReader
(std/textproto
)などのGoに影響を受けたAPIもすでに削除されています。
ただし、用途によってはWeb Streams APIよりもGoベースのAPIの方が使いやすい場面もあるかと思います。Deno.Reader
やDeno.Writer
が提供していた低レベルなIOインターフェースは、例えばTCPをベースにプロトコルを実装したい場合などはWeb Streams APIよりも使い勝手が良いのではないかと思います。
そういった事情もあり、Deno.Reader
やDeno.Writer
についてはDeno本体から型は削除されているものの、Deno.TcpConn
やDeno.FsFile
などのオブジェクトでは依然としてread()
やwrite()
メソッドなどを実装しています。現在では、Denoの標準ライブラリであるstd/io
からReader
やWriter
などのインターフェースが提供されています。(std/io/types.ts)
全体的にGoに影響を受けたスタイルからの脱却が見られますが、deno doc
コマンドやdeno fmt
コマンド, deno test
コマンドの存在など、Goに影響を受けていた名残は今でも一部に残っていたりします。
deno.json
)
設定ファイルの導入 (Denoは当初から特に設定ファイルなど(package.json
/tsconfig.json
/.prettierrc
など)を用意せずに利用することができるという特徴がありました。TypeScriptやフォーマッター(deno fmt
)などに関して、あらかじめDenoがデフォルトで多くのユーザーに対して妥当と思われる設定を前提に動作することで、設定なしでも利用できるよう設計されています。
Deno v1.0.0時点からの大きな変更点として、現在ではdeno.json
という設定ファイルが導入されています。deno.json
はJSONC形式もサポートされており、ファイル名をdeno.jsonc
にしておくとコメントなどを記述することができます。
このファイルでは以下のような内容をカスタマイズできます:
compilerOptions
の設定
TypeScriptの{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
deno fmt
やdeno lint
コマンドなどの振る舞いの調整
{
"lint": {
"rules": {
"include": [
// `no-console`ルールを有効化します
"no-console"
]
}
}
}
有効化したいunstable APIの管理
Deno v1.0.0のころは--unstable
オプションで全てのunstable APIの有効化のみがサポートされていましたが、現在はより詳細に有効化したいAPIを制御できます:
{
// Deno KV関連のunstable APIを有効化します
"unstable": ["kv"]
}
設定なしではDenoを利用できないの?
deno.json
という設定ファイルが導入されたものの、このファイルの作成は任意であって必須ではありません。必要がなければ、設定ファイルなしでもTypeScriptなどを利用出来るという特徴は今でも変わっていません。
依存管理
Deno v1.0.0の時点での依存管理について
まず前提として、Denoでは任意のホストからTypeScriptモジュールをimport
することができます:
import { BufReader } from "https://deno.land/std@0.50.0/io/bufio.ts";
これは手軽に外部ライブラリを利用できる便利な機能ではあるのですが、以下のように複数のモジュールで同じライブラリが利用されている場合、そのライブラリのアップデート時にすべてのモジュールでURLを書き換える必要が出てきます (以下の場合、a.ts
とb.ts
の両方を更新する必要があります)
// a.ts
import { BufReader } from "https://deno.land/std@0.50.0/io/bufio.ts";
// b.ts
import { BufReader } from "https://deno.land/std@0.50.0/io/bufio.ts";
Deno v1.0.0リリース時点では、まだこの課題を解決するためのプラクティスは固まっていなかったと思います。そこでこの課題を解決するための方法としてコミュニティなどからいくつかの方法が考案されました。
deps.ts
まずdeps.ts
というファイルを用意しておき、そこに全ての依存モジュールをまとめておきます:
// deps.ts
export { BufReader } from "https://deno.land/std@0.50.0/io/bufio.ts";
export * as path from "https://deno.land/std@0.50.0/path/mod.ts";
各モジュールからライブラリを利用する際は、このdeps.ts
経由でimport
します:
// a.ts
import { BufReader } from "./deps.ts";
// b.ts
import { BufReader, path } from "./deps.ts";
こうすることで、あるライブラリのバージョンをアップデートしたい場合、deps.ts
の編集のみで済みます。
この方法は特定の外部ツールなどを必要とせずとてもシンプルであることがメリットで、今現在でも採用しているプロジェクトがあったりします。
deno-udd
これはプロジェクトが依存している依存モジュールのアップデートを自動化してくれるツールです。もし複数のモジュールが同じ外部モジュールに依存していたとしても、deno-udd
がそれらのモジュールの更新を自動で行ってくれるため、手間が軽減されます。
deps.ts
を使わずに、このdeno-udd
を使っていた方もおそらく多いのではないかと思います。
dem
/dlink
/Trex
など)
専用の依存管理ツール (deno-udd
とは少しアプローチは異なりますが、専用の依存管理ツールも登場しました。
dem
とdlink
は、ユーザーが一定のディレクトリ構造に従う必要はありますが、deps.ts
よりも柔軟に依存管理できる方法を提供してくれます。
また、Trex
については後述するImport mapsの運用を効率化してくれるツールです。
--import-map
)
Import mapsを使う (Deno v1.0.0においてはImport mapsを使って依存関係を管理する方法もありました。例えば、import_map.json
というファイルを用意して、そこにプロジェクトで依存している依存関係をまとめておきます:
{
"imports": {
"$std/": "https://deno.land/std@0.50.0/",
"react": "https://esm.sh/react@18"
}
}
このimport_map.json
をDenoを実行する際に--import-map
オプションによって指定することで、アプリケーションでは以下のように依存モジュールを読み込むことができます:
// `https://deno.land/std@0.50.0/io/bufio.ts`が読み込まれます
import { BufReader } from "$std/io/bufio.ts";
// `https://deno.land/std@0.50.0/path/mod.ts`が読み込まれます
import * as path from "$std/path/mod.ts";
// `https://esm.sh/react@18`が読み込まれます
import React from "react";
もし依存パッケージのバージョンを更新したい場合、import_map.json
を書き換えるだけで済みます。
この方法はアプリケーションコードにおいてimport
文の記述を簡略化できることや先程紹介したTrexなどのツールによって管理を効率化できるなどのメリットがありますが、deno.land/x向けに公開するパッケージなどを開発する際は採用が難しいという欠点がありました。(Deno v1.0.0時点ではImport mapsはプロセスごとに一つしか指定ができず、外部ライブラリではImport mapsを使用することが難しい)
まとめ
Deno v1.0.0の時点ではまだ依存管理に関するプラクティスが定まっておらず、用途や好みなどに応じて手法が使い分けられることが多かったのではないかと思います:
-
deno.land/x向けのパッケージを開発する場合
deps.ts
deno-udd
-
dem
やdlink
- アプリケーションを開発する場合
- Import maps (+
Trex
など)
- Import maps (+
Deno v2における依存管理
Deno v2では、Deno本体にパッケージマネージャーが組み込まれています。まずは、このパッケージマネージャーが想定している依存管理手法である deno.json
でImport mapsを定義する 方法をDeno v2においては採用すると良いと思います。
パッケージマネージャーについて
現在ではDenoの本体にパッケージマネージャーが組み込まれており、具体的には以下のパッケージを管理することができます:
- jsrパッケージ
- npmパッケージ
jsrパッケージ
まず、JSRというDeno社によって開発されたパッケージレジストリがあります。OSSとして開発されており、以下のリポジトリでソースコードが公開されています:
Deno v1.0.0時点でも存在したdeno.land/xとの違いとしては、semverに基づいた依存解決の仕組みが提供されていること[1]や、Deno以外にもNode.jsやBunなどの様々なランタイムもサポートすることが挙げられます。(jsr-npmを使うことでNode.jsやBunからもjsrパッケージを利用できます)
DenoにはこのJSRと連携してパッケージを管理するための仕組みが本体に組み込まれています。JSRからパッケージを利用する際は、以下のようにjsr:@<scope>/<package>@<version>
形式のURLを記述します。
import { CsvParseStream } from "jsr:@std/csv@^1/parse-stream";
このように記述しておくと、スクリプトを実行する際にDenoはJSRと連携してsemverに基づいたバージョンの解決を行い、自動で必要なパッケージのダウンロードを行ってくれます。(基本的にはURLインポートと同じような感覚で使えると思います)
現在では標準ライブラリであるdeno_std
もこのJSRで公開されています (スコープは@std
)
Import mapsを併用することで、import
の記述を簡略化することも可能です。現在ではImport mapsはdeno.json
で定義することが可能で、deno.json
に書いておけば--import-map
オプションなしでも自動で適用されます:
{
"imports": {
"@std/csv": "jsr:@std/csv@^1"
}
}
deno.json
でこのように設定しておけば、アプリケーションからは以下のようにimport
ができます:
import { CsvParseStream } from "@std/csv/parse-stream";
このdeno.json
でのImport mapsの運用を簡略化するために、deno add
/deno install
/deno remove
などのコマンドが提供されています。
まず、パッケージを追加するにはdeno add
もしくはdeno install
コマンドを使う必要があります。例えば、以下はJSRからHonoをインストールする例です:
$ deno add jsr:@hono/hono@^4
# または
$ deno install jsr:@hono/hono@^4
すると、Denoが自動でdeno.json
のImport mapsとdeno.lock
※を更新してくれます (※deno.lock
について後述します):
{
"imports": {
"@hono/hono": "jsr:@hono/hono@^4.6.4"
}
}
deno add
またはdeno install
でインストールしたjsrパッケージを削除したい場合は、deno remove
が利用できます:
$ deno remove @hono/hono
このコマンドを実行すると、deno.json
のImport mapsから依存を削除してくれます。
補足: `deno install`について
deno install
は元々、Denoで書かれたスクリプトやCLIツールをインストールするためのコマンドでした (Goにおけるgo install
に近いイメージです)
現在ではdeno install
はnpmなどのツールを意識した挙動に変更されています (jsrまたはnpmパッケージをインストールして、deno.json
に書き込むコマンド)
Deno v2ではdeno install
コマンドは基本的にdeno add
コマンドと同様の振る舞いをします。
もしdeno install
コマンドにDeno v1.0.0と同様の振る舞いをさせたい場合は、--global
オプションを指定する必要があります。
ここで紹介したdeno_std
以外にも、Fresh/Hono/Oak/daxなどのDenoで人気のあるライブラリも少しずつJSRへ移行しつつあります。今後、Denoのライブラリを探すときは、まずはJSRが主流になる可能性もありそうです。
npmパッケージ
詳細については後述しますが、現在のDenoではNode.js互換性が重要視されており、Denoから直接npmパッケージを利用することができます。
Denoからnpmパッケージを利用する際は、基本的にnpm:<package>@<version>
の形式でimport
する必要があります。
import pc from "npm:picocolors@1";
console.info(pc.bold(pc.blue("Deno")));
npmパッケージに依存したスクリプトは通常通り、deno run
コマンドで実行できます (通常のDenoスクリプトの実行と同様に、パーミッションフラグの指定が必要です):
$ deno run --allow-env=NO_COLOR,FORCE_COLOR,TERM main.ts
Deno
jsrパッケージの場合と同様に、もし該当のnpmパッケージがまだダウンロードされていなければ、デフォルトでDenoのグローバルキャッシュ(DENO_DIR
)へ自動でそのパッケージがダウンロードされます。
deno add
またはdeno install
コマンドでもインストールが可能です:
$ deno add npm:picocolors
上記を実行すると、以下のようにdeno.json
のImport mapsとdeno.lock
が更新されます:
{
"imports": {
"picocolors": "npm:picocolors@^1.1.0"
}
}
これにより、以下のようにnpmパッケージを読み込めます:
import pc from "picocolors";
まとめ
Deno v2においては、基本的にはdeno.json
+deno add
などのコマンドの組み合わせで依存関係を管理すると良いと思います。Denoが提供する標準の方法であり、おそらくこの方法がすぐに廃れてしまう可能性も低いと思われます。
また、この依存管理手法はJSRパッケージを開発する際にも採用できます。DenoでJSRパッケージを公開する(deno publish
コマンド)際に、Denoはdeno.json
に記述されたImport mapsの内容を元に、公開されるパッケージに含まれる各モジュールのimport
文を自動で書き換えてくれます。
基本的に今までのdeps.ts
などを使用した依存管理手法は、deno.json
+deno add
を用いた依存管理が難しい場合に採用を検討すると良いと思います (例: GitHubのプライベートレポジトリで社内向けの共有ライブラリを開発したい場合など)
deno.lock
Deno v1.0.0において
v1.0.0の時点でもDenoにはLockfileの仕組みは存在していました。ただし、このLockfileの仕組みはnpmやpnpmなどにおけるものとは少し異なり、依存関係のバージョンを固定するというよりは、どちらかというとインテグリティチェックのための仕組みでした。(Denoは不特定多数の任意のホストからモジュールをimport
することができるため、どちらかというとLockfileはセキュリティを担保するための仕組みという位置づけだったと思います)
Deno v1.0.0時点においてはLockfileをバージョン管理に含めずに運用されるケースも比較的多かったのではないかと思います。
Deno v2.0.0において
Deno v2ではjsrやnpmパッケージなどのサポートに伴い、今までのインテグリティチェックのための仕組みに加えて、npmやpnpmなどと同様に依存関係のバージョンを固定するための仕組みとしても利用できます。それに伴い、Denoはデフォルトでdeno.lock
という名前でLockfileを生成するよう振る舞いが変更されています。
今後は可能な限り、deno.lock
もバージョン管理に含めるとよいと思います。
ワークスペース
Deno v2.0.0ではワークスペースがサポートされています。ワークスペースを利用する際は、プロジェクトのルートディレクトリの直下に配置するdeno.json
のworkspace
フィールドで設定を行います:
{
"workspace": {
"members": [
"./packages/backend",
"./packages/frontend",
"./packages/shared"
]
},
"imports": {
"@david/dax": "jsr:@david/dax@0.42.0"
},
"lint": {
"rules": {
"include": [
"no-console"
]
}
}
}
./packages/backend
, ./packages/frontend
及び./packages/shared
のそれぞれのディレクトリにもdeno.json
を配置し、そこでname
とversion
及びexports
フィールドを定義します:
{
"name": "@uki00a/shared",
"version": "0.1.0",
"exports": {
".": "./mod.ts"
},
"imports": {
"@std/csv": "jsr:@std/csv@^1"
}
}
ワークスペース機能の利点として、このようにdeno.json
を複数定義することができ、Import mapsについても各deno.json
ごとに定義することができます。これにより、Deno v1.0.0におけるプロセスごとに一つしかImport mapsを持つことができない問題が部分的に解消されています。
各ワークスペースメンバー間ではdeno.json
のname
属性で指定した名前を使って参照することができます。
// `packages/backend`から`packages/share`を参照する
import { groupBy } from "@uki00a/shared";
また、ワークスペースを利用したプロジェクトでdeno publish
コマンドを実行すると、全ワークスペースメンバーがまとめてJSRへ公開されます。
ワークスペース機能については便利ではあるものの、Denoの中では比較的新しい機能であり、まだ周辺ツールなどはNode.jsと比較すると少なめな印象ではあるため、現時点では無理には採用しようとはせず、必要になったら採用するぐらいの温度間でもよさそうな気はしています。
deno_std
について
Denoの標準ライブラリDeno v1.0.0
Deno v1.0.0時点では、deno_std
はDeno本体と同一リポジトリで開発されていました:
そのため、Deno本体とdeno_std
のリリースタイミングも揃えられていました。
また、deno_std
はdeno.land/stdで公開されており、そこから必要なモジュールをimport
することが想定されていました:
import { BufReader } from "https://deno.land/std@0.50.0/io/bufio.ts";
Deno v2.0.0現在
先程紹介したいように、deno_std
は現在はJSRで公開されています:
deno_std
のリポジトリもDeno本体のリポジトリからは分離され、リリースタイミングもDenoの本体とは完全に独立しています。
JSRを利用することにより、各モジュールごとに独立したパッケージとして公開が行えるようになりました。
例えば、@std/csvについてはすでにv1がリリースされています:
import { CsvParseStream, CsvStringifyStream } from "jsr:@std/csv@^1";
パッケージによってはまだv1がリリースされていないものもあるため、その場合はバージョンを固定して利用した方が安全かもしれません:
import * as log from "jsr:@std/log@0.224.9";
また、現在では各APIごとに独立したエントリポイントを設けるよう設計されており、特定のAPIのみを読み込むことができます:
import { CsvParseStream } from "jsr:@std/csv@^1/parse-stream";
Node.js互換性
v1.0.0時点での方針や状況について
Deno v1.0.0の時点ではNode.jsとの互換性は基本的に維持しない方針でした。そのため、npmパッケージについては基本的にそのままでは動かないケースがほとんどであり、使用には多少の工夫が必要であった記憶があります。
もし互換性が必要な際は、互換レイヤーとしてstd/nodeがDenoの標準ライブラリから提供されていたので、基本的にはそちらを利用するケースが多かったのではないかと思います。特にesm.shではnpmパッケージをDenoから使えるようにするためにstd/node
が活用されていました。
v2.0.0ではどうなるのか?
先程npmパッケージのサポートについて紹介しましたが、v2.0時点ではDenoではNode.jsとの互換性を重視する方針へと変わっています。
具体的にはnpmパッケージの読み込みを実現するために、v1の時点でstd/node
として実装されていた互換レイヤーは現在ではDenoのランタイム本体に組み込まれており、現在ではNode.jsの組み込みパッケージの多くがDenoで利用することができます。
// DenoからNode.jsの組み込みパッケージを読み込むことができます (ただし、デフォルトでは`node:`の指定が必須です)
import { EventEmitter } from "node:events";
const emitter = new EventEmitter();
また、Denoではpackage.json
のサポートも行われており、もしpackage.json
が存在する場合にdeno install
コマンドを実行すると、package.json
に書かれた依存パッケージがnode_modules
にダウンロードされます:
$ deno install
この場合もnpm:
なしでnpmパッケージを読み込むことができます:
import pc from "picocolors";
console.info(pc.green("Deno"));
また、package.json
がある場合はdeno install
ではなくnpm install
によってインストールした場合でも、Denoで実行することが可能です。
$ npm install
# npmでインストールしたパッケージをDenoで実行できます
$ deno run --allow-env=NO_COLOR,FORCE_COLOR,TERM main.ts
npmパッケージとpackage.json
のサポート以外にも、Node.jsと同様のモジュール解決(ディレクトリ内のindex.js
の自動検出など)を有効化する--unstable-sloppy-imports
やnode:
なしでのNode.js組み込みモジュールの読み込みを許可する--unstable-bare-node-builtins
などの機能があります。もし、Node.jsプロジェクトを変更せずにそのままDenoで試してみたい場合は、これらの機能の使用も検討するとよいかもしれません。
型チェック
Deno v1.0.0において
deno run
でTypeScriptファイルを実行する際に自動的に型チェックが行われていました。
# `deno run`の実行のたびにファイルに変更があれば型チェックが行われます
$ deno run main.ts
Deno v2.0.0において
現在においては、deno run
でTypeScriptファイルを実行する際はデフォルトで型チェックが行われません。 (※TypeScriptコードの実行は今でもできます)
deno run
コマンドはTypeScriptファイルのトランスパイルと実行のみを行い、デフォルトで型チェックは行いません。
その代わり、deno check
という型チェックのためのコマンドが導入されています。このコマンドを使うと対象のファイルの型チェックが可能です:
$ deno check mod.ts
このように変更された背景としては、Deno本体にLSP(deno lsp
)が実装されたこともあり、開発時にエディター上でユーザーが型エラーに気づくであろうことと、手元で型チェックをしていなかったとしてもCIでdeno check
を実行していれば問題は防げるということなどが背景にあると思われます。
ユースケースの拡充
Deno v1.0.0時点ではDenoだけでは実現できなかったことに関して紹介します。
永続化
現在では、DenoにlocalStorage
が実装されています。localStorage
はSQLiteをベースに実装されており、基本的にはブラウザーと同様の感覚で利用できます。
また、Deno KVという独自のKVSがDeno本体に組み込まれています。
また、このDeno KVはバックエンドを柔軟に切り替えることができるよう設計されています。具体的には、DenoのCLIとDeno Deploy[2]ではそれぞれ異なるバックエンドが採用されています:
- Deno (CLI): SQLiteベースのバックエンド (denoland/denokv)
- Deno Deploy: Foundation DBベースのバックエンド
FFI
Deno v1.0.0の頃は、プラグインシステム(Deno.openPlugin
)があり、この仕組みによってRustを使ってDenoの機能を拡張することができました。例として、このプラグインシステムはv0.13.0までのdeno_mongo
などにおいて使われていました。
現在ではこのプラグインシステムは廃止され、代わりにFFIが導入されています。Deno.dlopen
というAPIが追加されており、これによってダイナミックライブラリを読み込み、Denoから利用することができます (FFIを利用する際は--allow-ffi
の指定が必要です)
また、このFFI機能においては独自にJitやV8 Fast APIなどによる最適化の仕組みが実装されており、一定の条件を満たす場合に自動で適用されます (初期はTinyCCをベースに実装されていましたが、現在ではDeno独自の仕組みが導入されています)
このFFI機能を利用して、様々なライブラリが開発されています:
WebGPU API
Deno本体ではWebGPU APIが実装されています。
またDenoではこのWebGPU APIをベースにBYOWという独自の機能が実装されています。
正直、BYOWは使ったことはなくてあまり理解しきれてはいないのですが、WebGPU APIを活用することでプラットフォームを問わずウィンドウのレンダリングを高速化するための機能と認識しています。
WebGPU APIはDenoのコミュニティにおいては機械学習やGUIなどの領域で使用されている印象があります。
Jupyterとの統合
Deno本体にdeno jupyter
というコマンドが実装されています。Denoのランタイム内にJupyter Kernelが実装されており、Jupyter Notebookなどから通信ができます。
セキュリティ
パーミッションプロンプト
セキュリティについてはv1.0.0からv2.0.0にかけて大きく考えや方針が大きく変わったところは他に比べて少ないと思いますが、唯一大きく変わった点としては、パーミッションプロンプトというものが導入された点が挙げられます。
例えば、Deno v1.0.0においては権限を与えずにある処理を実行しようとした場合、以下のようにエラーが発生します:
$ deno run main.ts
error: Uncaught (in promise) PermissionDenied: Requires read access to "./deno.json", run again with the --allow-read flag
Deno v2.0.0現在では、以下のようにプロンプトが表示されます:
$ deno run main.ts
┏ ⚠️ Deno requests read access to "/home/uki00a/ghq/github.com/uki00a/deno-sandbox/deno.json".
┠─ Requested by `Deno.readFile()` API.
┠─ Learn more at: https://docs.deno.com/go/--allow-read
┠─ Run again with --allow-read to bypass this prompt.
┗ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions) >
ここでy
もしくはA
と入力すると、該当の処理に対する権限が与えられます。
ただし、deno test
などのコマンドを実行する際はDeno v1.0.0と同様に、パーミッションプロンプトは表示されません。明示的に権限を与える必要があります。
--allow-import
Deno v2.0.0では--allow-import
という新しいパーミッションフラグが導入されています。これはリモートホストからのモジュールのimport
を制限するためのもので、デフォルトでは以下のホストからのimport
が許可されています:
上記以外のホストからモジュールをimport
したい場合、明示的に権限を付与する必要があります:
import ky from "https://unpkg.com/ky@1.7.2";
const res = await ky.get("https://api.github.com/repos/uki00a/deno-weekly").json();
例えば、上記のようにunpkg.com
からモジュールを読み込みたい場合は、--allow-import
で許可する必要があります:
$ deno run --allow-net --allow-import=unpkg.com main.js
Deno.errors.NotCapable
の導入
Denoにおけるパーミッションエラー(--allow-*
の指定漏れ)とOSから報告されるパーミッションエラーは、両方ともDeno.errors.PermissionDenied
で表現されていました。
しかし、これでは区別が難しいということで、Deno v2.0.0からはOSによる権限エラーはDeno.errors.PermissionDenied
、Denoによる権限エラーはDeno.errors.NotCapable
で表現されるよう変更されています。
--deny-*
フラグの導入
--deny-*
フラグは--allow-*
フラグによって付与したパーミッションを部分的に取り消すための機能です。
# ファイルシステムへの書き込み以外を許可する
$ deno run --allow-all --deny-write main.js
# README.md以外のファイルの読み込みのみを許可します
$ deno run --allow-read --deny-read=README.md main.js
また、この--deny-*
の導入により、--allow-run
のallow-listに指定された各実行可能ファイルが--deny-write
に自動で設定されるようにする修正が実施されています。(--allow-run=deno
を指定すると、--deny-write=$(which deno)
も自動で指定されるイメージです)
組み込みのHTTPサーバーの導入
Deno v1.0.0当時の状況
Deno v1.0.0においてはHTTPサーバーやWebSocketサーバーの実装は標準ライブラリであるstd/http
から提供されていました
import { serve } from "https://deno.land/std@0.50.0/http/server.ts";
const body = "Hello, Deno!";
const server = serve({ port: 8000 });
for await (const req of server) {
req.respond({ body });
}
AsyncIterator
の利用など、よりJavaScriptの先進的な機能の採用をしようとしていたことが伺えます。
また、当時はServestのように完全に一から実装されたHTTPサーバーの実装なども存在していました。
Deno v2.0.0について
現在ではDenoのラインタイム内にHTTPサーバーやWebSocketサーバーのRustベースの実装が組み込まれています。Deno.serve
によって、Deno本体に組み込まれたHTTPサーバーを起動することができます:
const server = Deno.serve({
port: 3000,
}, (req) => {
// `Request`を受け取り`Response | Promise<Respones>`を返す関数を指定します
return new Response("OK");
});
また、WebSocket
サーバーはDeno.upgradeWebSocket
というAPI経由で建てることができます。
std/httpからは現在ではHTTPサーバーの実装は削除されており、HTTP関連のユーティリティーという位置づけに変更されています。
Deno v1.0.0時点でも人気のあったOakは現在でも開発やメンテナンスが継続され続けています。
また、HonoもDeno.serve
をベースに利用することができます。
テストランナーの改善
テストランナー(deno test
)に関しては、Deno v1.0.0と比較して大きく考え方や方針などは変わっていませんが、いくつか便利な機能が導入されています。
ドキュメンテーションテスト
Deno v2.0.0で導入された便利な機能として、deno test --doc
でJSDocコメントやMarkdownファイル中のコードスニペットの実行がサポートされています。(Deno v1.xでも型チェックは可能でしたが、コードの実行はつい最近サポートされました)
/**
* ```typescript
* import { assertEquals } from "jsr:@std/assert@1/equals";
* assertEquals(sum(), 0);
* assertEquals(sum(2, 3, 4), 9);
* assertEquals(sum(1), 1);
* ```
*/
export function sum(...values) {
return values.reduce((x, y) => x + y, 0);
}
例えば、deno test --doc sum.js
を実行すると、JSDocコメントに記述された以下のコードブロックのテストと型チェックが実行されます:
import { assertEquals } from "jsr:@std/assert@1/equals";
assertEquals(sum(), 0);
assertEquals(sum(2, 3, 4), 9);
assertEquals(sum(1), 1);
BDD形式でのテストの記述
現在では、標準ライブラリであるstd/testingを活用することでBDD形式でのテストの記述が可能です:
import { describe, it } from "jsr:@std/testing@1/bdd";
import { expect } from "jsr:@std/expect@1"
import { sum } from "./sum.js"
describe("sum", () => {
it("should return sum of given numbers", () => {
expect(sum(2, 3, 4)).toBe(9);
});
it("should return 0 if no arguments are given", () => {
expect(sum()).toBe(0);
});
});
@std/testing/bdd
を使用して記述されたテストは、通常通りdeno test
コマンドで実行することができます。
タスクランナー
Deno v1.0.0
Deno v1.0.0時点では標準のタスク管理の仕組みがありませんでした。そのため、Velociraptor
やmake
などが使用されるケースが多かったのではないかと思います。
特にDeno v1.0.0時点ではGoが意識されていたこともあってか比較的make
を使ったプロジェクトも多かった印象があります。
Deno v2.0.0
Deno本体からdeno task
というコマンドが提供されています。使い方としてはnpm scriptsによく似ていて、まずdeno.json
のtasks
内でタスクを定義します。
{
"tasks": {
"lint": "deno fmt --check && deno lint && deno check 'packages/**/*.ts' && deno doc --lint 'packages/**/*.ts'"
}
}
この状態でdeno task lint
を実行すると、deno.json
のtasks.lint
に記述したスクリプトが実行されます:
$ deno task lint
deno task
コマンドの内部では、daxなどでも採用されているdeno_task_shellがシェルとして利用されており、各プラットフォームごとの差異を吸収してくれます。また、deno task
コマンドはpackage.json
のscripts
に記述されたスクリプトを実行することも可能です。
おわりに
Deno v1のリリースからDeno v2のリリースまでおよそ4年半程が経過しましたが、こうしてまとめてみると、思いの外大きな変更や方針転換などもあり驚きました。
Deno本体もそうですが、標準ライブラリについてもかなり安定性や使い勝手が当時と比較して改善していると思います。
JSRについては今後、どれくらい使われるようになるかは正直まだわからないのですが、個人的に使ってみた限り、パッケージの公開についてはかなり体験が良さそうなので、今後も色々試してみたいと思っています。
Discussion