🦕

Deno v2がリリース🎉 - Deno v1.0.0の頃からの変化と現在のプラクティスについて

2024/10/15に公開

Deno v2が正式にリリースされました🎉

https://deno.com/blog/v2.0

https://www.youtube.com/watch?v=d35SlRgVxT8

この記事では、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標準への準拠を高める動きがありました:

https://github.com/denoland/deno/issues/9795

https://github.com/denoland/deno/issues/13614

https://github.com/denoland/std/issues/1986

基本的にGoスタイルのAPI(Deno.Reader/Deno.Writer)はシンプルで使いやすいという意見はDenoの開発チーム内でも認められていたようですが、よりWeb標準への準拠を高めるという方針が優先されたようです。IO関連の機能については、現在ではWeb Streams APIの採用を前提に設計されていることが多い印象です。

https://developer.mozilla.org/en-US/docs/Web/API/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.ReaderDeno.Writer, Deno.Closerなどの型は廃止されています。 Denoの標準ライブラリの一部である std/ioBufReader/BufWriter/BufferTextProtoReader (std/textproto)などのGoに影響を受けたAPIもすでに削除されています。

ただし、用途によってはWeb Streams APIよりもGoベースのAPIの方が使いやすい場面もあるかと思います。Deno.ReaderDeno.Writerが提供していた低レベルなIOインターフェースは、例えばTCPをベースにプロトコルを実装したい場合などはWeb Streams APIよりも使い勝手が良いのではないかと思います。

そういった事情もあり、Deno.ReaderDeno.WriterについてはDeno本体から型は削除されているものの、Deno.TcpConnDeno.FsFileなどのオブジェクトでは依然としてread()write()メソッドなどを実装しています。現在では、Denoの標準ライブラリであるstd/ioからReaderWriterなどのインターフェースが提供されています。(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にしておくとコメントなどを記述することができます。

このファイルでは以下のような内容をカスタマイズできます:

TypeScriptのcompilerOptionsの設定

deno.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

deno fmtdeno lintコマンドなどの振る舞いの調整

deno.jsonc
{
  "lint": {
    "rules": {
      "include": [
        // `no-console`ルールを有効化します
        "no-console"
      ]
    }
  }
}

有効化したいunstable APIの管理

Deno v1.0.0のころは--unstableオプションで全てのunstable APIの有効化のみがサポートされていましたが、現在はより詳細に有効化したいAPIを制御できます:

deno.jsonc
{
  // 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.tsb.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の編集のみで済みます。

この方法は特定の外部ツールなどを必要とせずとてもシンプルであることがメリットで、今現在でも採用しているプロジェクトがあったりします。

https://github.com/oakserver/oak/blob/v17.1.0/deps.ts

deno-udd

https://github.com/hayd/deno-udd

これはプロジェクトが依存している依存モジュールのアップデートを自動化してくれるツールです。もし複数のモジュールが同じ外部モジュールに依存していたとしても、deno-uddがそれらのモジュールの更新を自動で行ってくれるため、手間が軽減されます。

deps.tsを使わずに、このdeno-uddを使っていた方もおそらく多いのではないかと思います。

専用の依存管理ツール (dem/dlink/Trexなど)

deno-uddとは少しアプローチは異なりますが、専用の依存管理ツールも登場しました。

https://github.com/syumai/dem

https://github.com/keroxp/dlink

https://github.com/crewdevio/Trex

demdlinkは、ユーザーが一定のディレクトリ構造に従う必要はありますが、deps.tsよりも柔軟に依存管理できる方法を提供してくれます。

また、Trexについては後述するImport mapsの運用を効率化してくれるツールです。

Import mapsを使う (--import-map)

Deno v1.0.0においてはImport mapsを使って依存関係を管理する方法もありました。例えば、import_map.jsonというファイルを用意して、そこにプロジェクトで依存している依存関係をまとめておきます:

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
    • demdlink
  • アプリケーションを開発する場合
    • Import maps (+Trexなど)

Deno v2における依存管理

Deno v2では、Deno本体にパッケージマネージャーが組み込まれています。まずは、このパッケージマネージャーが想定している依存管理手法である deno.jsonでImport mapsを定義する 方法をDeno v2においては採用すると良いと思います。

パッケージマネージャーについて

現在ではDenoの本体にパッケージマネージャーが組み込まれており、具体的には以下のパッケージを管理することができます:

  • jsrパッケージ
  • npmパッケージ

jsrパッケージ

まず、JSRというDeno社によって開発されたパッケージレジストリがあります。OSSとして開発されており、以下のリポジトリでソースコードが公開されています:

https://github.com/jsr-io/jsr

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)

https://jsr.io/@std

Import mapsを併用することで、importの記述を簡略化することも可能です。現在ではImport mapsはdeno.jsonで定義することが可能で、deno.jsonに書いておけば--import-mapオプションなしでも自動で適用されます:

deno.json
{
  "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について後述します):

deno.json
{
  "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が主流になる可能性もありそうです。

https://jsr.io/@fresh/core

https://jsr.io/@hono/hono

https://jsr.io/@oak/oak

https://jsr.io/@david/dax

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が更新されます:

deno.json
{
  "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.jsonworkspaceフィールドで設定を行います:

deno.json
{
  "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を配置し、そこでnameversion及びexportsフィールドを定義します:

packages/shared/deno.json
{
  "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.jsonname属性で指定した名前を使って参照することができます。

packages/backend/main.ts
// `packages/backend`から`packages/share`を参照する
import { groupBy } from "@uki00a/shared";

また、ワークスペースを利用したプロジェクトでdeno publishコマンドを実行すると、全ワークスペースメンバーがまとめてJSRへ公開されます。

ワークスペース機能については便利ではあるものの、Denoの中では比較的新しい機能であり、まだ周辺ツールなどはNode.jsと比較すると少なめな印象ではあるため、現時点では無理には採用しようとはせず、必要になったら採用するぐらいの温度間でもよさそうな気はしています。

Denoの標準ライブラリdeno_stdについて

Deno v1.0.0

Deno v1.0.0時点では、deno_stdはDeno本体と同一リポジトリで開発されていました:

https://github.com/denoland/deno/tree/v1.0.0/std

そのため、Deno本体とdeno_stdのリリースタイミングも揃えられていました。

また、deno_stddeno.land/stdで公開されており、そこから必要なモジュールをimportすることが想定されていました:

import { BufReader } from "https://deno.land/std@0.50.0/io/bufio.ts";

Deno v2.0.0現在

先程紹介したいように、deno_stdは現在はJSRで公開されています:

https://jsr.io/@std

deno_stdのリポジトリもDeno本体のリポジトリからは分離され、リリースタイミングもDenoの本体とは完全に独立しています。

https://github.com/denoland/std

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パッケージについては基本的にそのままでは動かないケースがほとんどであり、使用には多少の工夫が必要であった記憶があります。

https://zenn.dev/uki00a/articles/how-to-use-npm-packages-in-deno

もし互換性が必要な際は、互換レイヤーとして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-importsnode:なしでの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本体に組み込まれています。

https://zenn.dev/uki00a/articles/kv-store-introduced-in-deno-v1-32

また、この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機能を利用して、様々なライブラリが開発されています:

https://github.com/denodrivers/sqlite3

https://github.com/littledivy/deno_sdl2

https://github.com/webview/webview_deno

WebGPU API

Deno本体ではWebGPU APIが実装されています。

https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API

またDenoではこのWebGPU APIをベースにBYOWという独自の機能が実装されています。

https://github.com/denoland/deno/issues/21713

https://github.com/denoland/deno/pull/21835

正直、BYOWは使ったことはなくてあまり理解しきれてはいないのですが、WebGPU APIを活用することでプラットフォームを問わずウィンドウのレンダリングを高速化するための機能と認識しています。

WebGPU APIはDenoのコミュニティにおいては機械学習やGUIなどの領域で使用されている印象があります。

https://github.com/denosaurs/netsaur

https://github.com/littledivy/wgui

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が許可されています:

  • deno.land
  • jsr.io
  • esm.sh
  • raw.githubusercontent.com
  • cdn.jsdelivr.net

上記以外のホストからモジュールを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)も自動で指定されるイメージです)

https://github.com/denoland/deno/pull/25370

組み込みの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サーバーの実装なども存在していました。

https://github.com/keroxp/servest

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は現在でも開発やメンテナンスが継続され続けています。

https://github.com/oakserver/oak

また、HonoもDeno.serveをベースに利用することができます。

https://github.com/honojs/hono

テストランナーの改善

テストランナー(deno test)に関しては、Deno v1.0.0と比較して大きく考え方や方針などは変わっていませんが、いくつか便利な機能が導入されています。

ドキュメンテーションテスト

Deno v2.0.0で導入された便利な機能として、deno test --docでJSDocコメントやMarkdownファイル中のコードスニペットの実行がサポートされています。(Deno v1.xでも型チェックは可能でしたが、コードの実行はつい最近サポートされました)

sum.js
/**
 * ```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時点では標準のタスク管理の仕組みがありませんでした。そのため、Velociraptormakeなどが使用されるケースが多かったのではないかと思います。

https://github.com/jurassiscripts/velociraptor

特にDeno v1.0.0時点ではGoが意識されていたこともあってか比較的makeを使ったプロジェクトも多かった印象があります。

Deno v2.0.0

Deno本体からdeno taskというコマンドが提供されています。使い方としてはnpm scriptsによく似ていて、まずdeno.jsontasks内でタスクを定義します。

deno.jsonc
{
  "tasks": {
    "lint": "deno fmt --check && deno lint && deno check 'packages/**/*.ts' && deno doc --lint 'packages/**/*.ts'"
  }
}

この状態でdeno task lintを実行すると、deno.jsontasks.lintに記述したスクリプトが実行されます:

$ deno task lint

deno taskコマンドの内部では、daxなどでも採用されているdeno_task_shellがシェルとして利用されており、各プラットフォームごとの差異を吸収してくれます。また、deno taskコマンドはpackage.jsonscriptsに記述されたスクリプトを実行することも可能です。

おわりに

Deno v1のリリースからDeno v2のリリースまでおよそ4年半程が経過しましたが、こうしてまとめてみると、思いの外大きな変更や方針転換などもあり驚きました。

Deno本体もそうですが、標準ライブラリについてもかなり安定性や使い勝手が当時と比較して改善していると思います。

JSRについては今後、どれくらい使われるようになるかは正直まだわからないのですが、個人的に使ってみた限り、パッケージの公開についてはかなり体験が良さそうなので、今後も色々試してみたいと思っています。

参考


脚注
  1. DenoのURLインポートは手軽で便利であるものの、巨大なアプリケーションなどを作成しようとした際に、semverに基づいた依存解決がないとある依存パッケージの複数のバージョンが重複してインストールされてしまう課題がありました。この課題を解消することがJSRが開発された大きな動機の一つだと思います。 ↩︎

  2. Deno社が開発しているサーバーレスTypeScript/JavaScriptランタイムです ↩︎

Discussion