🦕

Deno v2に向けて - Deno v2, deno_std v1, Fresh v2について

2024/06/10に公開

Deno v1がリリースされてから4年程が経過しました。

https://deno.com/blog/v1

そろそろDeno v2はいつごろ出るんだろう?と疑問に思っている方もいらっしゃるかもしれません。

この記事ではDeno v2やその周辺などに関して、現状、どのような対応が進んでいるのかなどについてまとめます。

Deno v2について

Deno v2についてなのですが、リリース時期についてはちょっとまだわからない状況です。

ただ、現在の状況として、Deno v2のリリースに向けた対応は少しずつ進められてる様子が見られます。

具体的に現在、どういった変更が計画または進められているのかについて見ていきたいと思います。

Node.js互換性の改善

以前にいくつか記事にもしましたが、Node.js互換性の改善は引き続きかなり力を入れて進められています。現状では以下のような機能などが実装されています。

Node.js互換性に力が入れられている背景について

背景の一つとして依存関係の重複問題を解決したいという目的があります。

https://gist.github.com/ry/f410f6977a164477953e903bcf9d7d74

まずDenoの大きな特徴の一つとして、任意のURLからのモジュールのimportがサポートされています。この機能を活用するために、Deno公式からdeno.land/xというレジストリが提供されており、現状、Deno向けの多くのライブラリはこのdeno.land/xで公開されています。

以下はdeno.land/xからライブラリを利用する例です。

import { connect } from "https://deno.land/x/[email protected]/mod.ts";
const redis = await connect({
  hostname: "127.0.0.1",
  port: 6379,
});

この機能はシンプルでわかりやすく特にスクリプティングなどにおいては便利であるものの、以下の公式ブログでも解説されているように、大規模なアプリケーションなどにおいては依存関係の重複が発生し得るというデメリットがあります。

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

具体的にはstd/uuid/v1.tsに依存したfoobarという2つのパッケージがあったとします。パッケージfoostd/uuid/v1.ts0.223.0, パッケージbarstd/uuid/v1.ts0.224.0に依存しているとします。

app
├── foo
│   └── [email protected]/uuid/v1.ts
└── bar
    └── [email protected]/uuid/v1.ts

パッケージfoobarそれぞれが依存しているstd/uuid/v1.ts0.223.00.224.0は内容がまったく変わりません。

しかし、これらはそれぞれバージョンが異なるため、もしアプリケーションがfoobarの両方のパッケージに依存している場合、内容が重複したモジュール(std/uuid/v1.ts0.223.00.224.0)が複数インストールされてしまいます。

この問題を解消するためにはsemverに基づいた依存解決の仕組みなどが必要になります。Denoでnpmパッケージをサポートするためにはいずれにせよsemverに基づいた依存解決の仕組みが必要になるため、Denoにnpmパッケージのサポートが導入されることによって自然とこの問題に対する解決策が提供されることになります。また、npmパッケージがサポートされることにより、Node.js向けの豊富な資産をDenoでも活用できる余地が産まれます。

Node.js互換性に関する現在の状況について

Node.js互換性の改善によりDocusaurusがDenoで動作するようになりました。これに伴い、Deno公式のドキュメンテーションサイトの実装もNode.js+DocusaurusからDeno+Docusaurusへ移行されています。

https://github.com/denoland/deno-docs/pull/423

また、Next.jsが動作するようになったことが直近のDeno v1.44の公式ブログで発表されています。(この件に関しては、後日、Deno公式からブログが公開される予定のようです)

https://deno.com/blog/v1.44

ただし、Next.jsを動作させるためには、現時点では後述するDENO_FUTURE=1の指定が必要なようです。

Node.js互換性に関するv2に向けた対応について

Deno v2向けに以下の変更などが検討されています。

  • BYONMのデフォルトでの有効化
  • deno installコマンドの振る舞いの変更
  • npmやYarnなどとの相互運用性の改善

それぞれの内容について紹介していきます。

BYONMのデフォルトでの有効化

BYONMとはnpmやpnpm, Yarnなどのパッケージマネージャーによって作成されたnode_modulesディレクトリからnpmパッケージを読み込むための機能です。

このBYONMを使うことで、npmパッケージの管理はYarnに任せつつ、DenoからはYarnによってインストールされたパッケージをそのまま利用するようなことができます。

https://zenn.dev/uki00a/articles/frontend-development-in-deno-2024-winter

現時点では、BYONMを利用するためにはdeno.jsonなどで明示的に有効化する必要があります。

Deno v2ではこの挙動が変更され、package.jsonが存在する場合はデフォルトでBYONMを有効化することが計画されています。

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

後述するDENO_FUTURE=1を指定することで、Deno v1でもこの挙動を試すことができます。

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

deno installコマンドの振る舞いの変更

Denoにはdeno installというコマンドがあります。このdeno installコマンドはDeno v1においては、任意のホストで公開されたスクリプトをローカルにインストールするためのコマンドです。(Node.jsで例えるとnpm install -g, Goで例えるとgo install相当の振る舞いをするようなイメージです)

$ deno install -rf --allow-read=. --allow-write=. --allow-net https://deno.land/x/[email protected]/main.ts
⚠️ `deno install` behavior will change in Deno 2. To preserve the current behavior use the `-g` or `--global` flag.
✅ Successfully installed udd
/path/to/.deno/bin/udd

このdeno installコマンドの振る舞いがDeno v2で変更されることが計画されています。

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

まず、deno installコマンドは依存関係をプロジェクトに追加するためのコマンドとして振る舞うように挙動が変更されます(後述するdeno addコマンドと同様の振る舞いをします)

# 現時点でこの挙動を試すには`DENO_FUTURE=1`の指定が必要です
$ deno install npm:[email protected]


# deno.jsonにchalkに関するマッピングが書き込まれます
$ cat deno.json | jq .imports.chalk
"npm:chalk@^5.3.0"

また、引数なしでdeno installコマンドを実行した場合は、プロジェクトが依存している各パッケージをダウンロードし、ローカルにキャッシュしてくれるようです。

# 現時点でこの挙動を試すには`DENO_FUTURE=1 `の指定が必要です
$ deno install

このように、deno installコマンドはnpm installyarn addなどと同様の振る舞いをするように挙動が変更される想定です。

もしdeno installコマンドにDeno v1と同じような振る舞いをして欲しい場合には、明示的に--globalというオプションを指定する必要があります。

$ deno install --global -rf --allow-read=. --allow-write=. --allow-net https://deno.land/x/[email protected]/main.ts
npmやYarnなどとの相互運用性の改善

Denoは独自にロックファイルの仕組みを備えています。(deno.lock)

直近でリリースされたDeno v1.44ではpackage.jsonが存在する場合に、自動でロックファイルを生成する仕組みも導入されています。

しかし、既存のNode.jsアプリケーションでDenoを利用する上では、deno.lockではなくYarnなどで生成されたロックファイルをそのまま使いたいというケースもあると思います。こういった要求を満たすため、前述のdeno installコマンドでnpmやYarnなどが生成したロックファイルを認識できるようにすることがDeno v2で検討されているようです。

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

Node.js互換性に関するまとめ

Deno v2に向けては、主に既存のパッケージマネージャーとの相互運用性の改善や使用感の統一などが想定されているようです。おそらく、既存のNode.jsプロジェクトにおいて、ソースコードを変更せずにDenoを使えるようにすることなどが想定されているのだと思われます。

今後、Deno公式からNext.jsの利用に関する記事が公開されることが計画されているようなので、そちらでも色々と発表される可能性があるのではないかと思います。


JSR

JSRというパッケージレジストリがDeno公式で公開されました。

https://jsr.io/

このJSRの特徴として、TypeScriptのネイティブサポートやドキュメンテーションの自動生成などの機能を備えています。

また、npmレジストリとも互換性があり、以下のツールを利用することでNode.jsやBunなどのランタイムでもJSRで公開されたパッケージを利用することができます。

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

このJSRの公開に合わせて、DenoにJSRのネイティブサポートが追加されました。

DenoでJSRパッケージを利用する方法

jsr:URL

DenoからJSRで公開されたパッケージを利用するには、jsr:@<scope>/<package>@<version>という形式のURLを指定します。

例えば、以下は@davidというスコープで公開されているdaxパッケージのv0.41.0を利用する例です。

import $ from "jsr:@david/[email protected]";

$.log("Hello Deno!");

このようにJSRではパッケージの公開にあたって必ずスコープの指定が必要になるのが特徴です。jsr:経由で指定されたJSRパッケージは、通常のhttps:形式のパッケージなどと同様に、Denoを実行する際に自動でダウンロードされてキャッシュされます。

deno addコマンド

また、deno addというコマンドも追加されています。このコマンドを使うと、Denoが指定されたJSRパッケージをdeno.jsonimportsに書き込んでくれます。

$ deno add @david/dax
Add @david/dax - jsr:@david/dax@^0.41.0

$ cat deno.json | jq '.imports."@david/dax"' 
"jsr:@david/dax@^0.41.0"

これにより、以下のようにして@david/daxパッケージを読み込むことができます。

import $ from "@david/dax";

$.log("Hello Deno!");

また、npmパッケージの追加も可能です。

$ deno add npm:chalk@^5 
Add chalk - npm:chalk@^5.3.0


$ cat deno.json | jq .imports.chalk
"npm:chalk@^5.3.0"
パッケージの公開 (deno publish)

Deno本体からJSRにパッケージを公開するためにdeno publishというコマンドが提供されています。このdeno publishではドライランもサポートされているので、パッケージを公開したい場合はまずこれを試してみるとよいと思います。

$ deno publish --dry-run

パッケージの公開に関しては以下のページなどで詳しく説明されているため、よろしければそちらなども参照ください。

https://developer.mamezou-tech.com/blogs/2024/05/09/jsr/

fast check

JSRに関連した独自の仕組みとしてfast checkという機能がDenoに組み込まれています。

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

fast checkとは、slow typesと呼ばれるTypeScriptにおける型チェックの低速化の要因になりうる定義を検出してくれる仕組みです。具体的には、パッケージの公開APIのうち、明示的に型定義が記述されていないような関数などを検出してくれます。

// Bad - 戻り値の型定義が省略されている
export function add(a: number, b: number) {
  return a + b;
}

// Good - 引数と戻り値の型がきちんと定義されている
export function add(a: number, b: number): number {
  return a + b;
}

このslow typesが存在するパッケージについては、そうでないパッケージと比べて、型チェックをする際に時間がかかってしまう可能性があります。

Denoはdeno publishなどのコマンドを実行するときにfast checkを実行することで、JSRパッケージの利用者がslow typesによって体験を低下してしまわないようにすることが意識されています。

このfast checkはJSRパッケージの作者向けの機能です。そのため、ユーザーとしてJSRパッケージを利用する分には特に気にしなくても問題ありません。

このslow typesの詳細については以下のドキュメントなども参照ください。

https://jsr.io/docs/about-slow-types

JSRが導入された背景

jsr:URLは元々はdeno:URLとしての導入が想定されていた機能だと思われます。

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

元々、deno:URLが導入されようとしていた背景は、前述の依存関係の重複問題を解消することが目的でした。

npmパッケージをDenoがサポートすることにより、依存関係の重複問題は部分的に解消されます。npmパッケージを利用する場合は、Denoがsemverに基づいて依存解決を行ってくれるためです。

しかし、DenoやDeno Deploy向けのアプリケーションを開発する場合は、npmパッケージではなく、DenoやDeno Deploy向けに専用に開発されたライブラリ(例: Fresh, deno_stdなど)を使いたいというケースも出てくるかと思います。この場合は、deno.land/xを利用することになるため、結局、前述の依存関係の重複問題が発生してしまいます。

これを解消するためにはDeno向けのパッケージに対してもsemverに基づいた依存解決の仕組みが必要になります。

この問題の解決策の一つとして、Deno向けのライブラリをnpmレジストリに公開するという手段がありそうです。しかし、Deno向けのライブラリをnpmレジストリに公開するとなると、今度はそのパッケージをNode.jsなどからも利用したいという要望が発生する可能性が高いと思われます。その場合は、Node.jsからはTypeScriptコードを直接実行することはできないため、パッケージの公開前に自前でTypeScriptのコードをJavaScriptにトランスパイルしてから公開するなどの手間が発生してしまいます。

JSRではレジストリがこういった作業などを肩代わりしてくれることで、パッケージ公開に関する手間を軽減してくれます。

まとめると、以下の課題を解消することなどを主な目的としてJSRは開発されたものなのだと思われます。

  • Deno向けのパッケージにおいても依存関係の重複問題を解消したい
  • パッケージの公開に関する体験を改善したい

このJSRにはすでにHonoOakなどの著名なパッケージも公開されており、今後、Deno向けのライブラリを利用する場合は、JSRを利用することがメジャーになる可能性が高いのではないかと思います。


ワークスペース機能の導入

Denoにワークスペース機能が導入されます。ワークスペースを利用する際は、まずdeno.jsonworkspacesを定義する必要があります。

deno.json
{
  "imports": {
    "$dax": "jsr:@david/[email protected]"
  },
  "workspaces": [
    "foo",
    "bar"
  ]
}

上記ではfooというbarという2つのワークスペースが定義されています。ディレクトリ構造としては以下のように各ワークスペースごとにディレクトリを用意する必要があります。

.
├── deno.json
├── mod.ts
├── bar
│   ├── deno.json
│   └── mod.ts
└── foo
    ├── deno.json
    └── mod.ts

このように、それぞれのワークスペースごとにdeno.jsonを配置することができます。

以下はbarワークスペースのdeno.jsonの定義で、このようにnameversion, exportsなどを定義する必要があります。

bar/deno.json
{
  "name": "@test/bar",
  "version": "0.0.1",
  "exports": {
    ".": "./mod.ts"
  },
  "imports": {
    "chalk": "npm:[email protected]"
  }
}

barワークスペース内のモジュールではbar/deno.jsonとルートのdeno.jsonに基づいて依存解決が行われます。

bar/mod.ts
// `[email protected]`が読み込まれます
export { default as chalk } from "chalk";

// ルートディレクトリで定義された`@david/[email protected]`が読み込まれます
export { default as $ } from "$dax";

fooワークスペースについても同様です。こちらではbarワークスペースとは異なるバージョンのchalkを読み込んでいます。

foo/deno.json
{
  "name": "@test/foo",
  "version": "0.0.1",
  "exports": {
    ".": "./mod.ts"
  },
  "imports": {
    "chalk": "npm:[email protected]"
  }
}
foo/mod.ts
// `[email protected]`が読み込まれます
export { default as chalk } from "chalk";

以下はプロジェクトのルートディレクトリに配置されたmod.tsの例で、それぞれのワークスペースをパッケージとして読み込むことができます。

mod.ts
// fooを読み込みます
import { chalk as chalk_foo } from "@test/foo";

// barを読み込みます
import { chalk as chalk_bar, $ } from "@test/bar";

console.info(chalk_foo.red("foo"));
console.info(chalk_bar.red("bar"));

// fooでは`[email protected]`, barでは`[email protected]`が利用されているため、falseになります
console.assert(chalk_foo.red !== chalk_bar.red);

$.log(chalk_foo.green("Hello bar"));

今まで、Denoではプロジェクトごとに一つしかImport mapsを定義できない課題がありました。ワークスペース機能の導入により、この課題の解消が期待されます。

このワークスペース機能はすでにDeno本体に実装されていますが、Deno v2に向けていくつかの改善がまだ残っているようです。具体的にはnpm workspaceのサポートなどが検討されているようです。詳しい進捗などについては以下のissueなどを参照ください。

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


非推奨APIの削除

Deno本体の非推奨APIが削除される想定です。対象APIについては以下のマイグレーションガイドで解説されています。

https://github.com/denoland/deno-docs/blob/558eb18f6d480b9b10d5bce9ec1208795ab5b27c/runtime/manual/advanced/migrate_deprecations.md

大きなものとしては、Deno.ReaderDeno.Writerが削除される予定です。

これらはDenoの初期の頃に、Goのio.Readerio.Writerなどに影響を受けて導入されました。Deno v1のリリース以降、DenoではWeb APIの利用が重要視されるようになったこともあり、IOに関する機能などもDeno.ReaderDeno.WriterではなくWeb Streams APIをベースに実装されるケースが増えてきました。

また、以下のissueでも説明されていますが、現在のDenoにおいてはシステムコールに依存しない機能については、できる限りDeno.*配下に置くべきではないという方針が取られているようです。

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

こういった背景などもあり、Deno本体からはDeno.ReaderDeno.Writerなどの型が削除されることになりました。(注意点として、Deno.ReaderDeno.Writerについては型が削除されるだけであって、Deno.FsFileなどのAPIからは依然としてreadwriteなどのメソッドが提供されます)

これらの型は@std/io/typesに移植されているため、今後はこちらから利用するとよさそうです。


非推奨APIの安定化

今まで非推奨APIとして提供されていたいくつかの機能がv2で安定化される可能性が高そうです。大きなものとしては、以下の機能などが安定化される想定のようです。


DENO_FUTURE環境変数の導入

DENO_FUTUREという環境変数が導入されています。この環境変数を指定することで、Deno で将来的に実施予定の破壊的変更などを先んじて試すことができます。例えば、DENO_FUTUREを利用することで、非推奨化されており削除予定のAPIを無効化することができます。

$ DENO_FUTURE=1 deno run mod.ts

この環境変数を設定しておくことで素早くv2に移行しやすくなると思われるため、もしパッケージなどを開発されている場合は、CIなどでのテスト実行時にこの環境変数を有効化しておくと便利かもしれません。


deno_std v1

Denoの公式標準パッケージであるdeno_stdのv1について解説します。

deno_std v1はいつ出るの?

結論として、deno_stdで提供されるモジュールの一つである@std/bytesではすでにv1がリリースされています。

具体的にどういった状況なのかについて説明いたします。

2024/06/11 追記

Deno公式からdeno_stdの各モジュールの安定化に関するスケジュールが公開されました。

https://deno.com/blog/stabilize-std

v1が中々リリースできなかった背景

deno_stdには様々なモジュールが存在します。

例)

これらはモジュールごとに成熟度にバラツキがあります。

例えば、@std/pathは使用率が高く長期間メンテナンスもされており、比較的動作が安定していると考えられます。そのため、@std/pathについては今後、破壊的変更が起きる可能性も比較的低いと思われます。

しかし、@std/expectなど、中にはまだ成熟度が高くないパッケージもあります。そのため、deno_std全体でv1をリリースしてしまうと、こういった成熟度が高くないパッケージに対して破壊的変更を入れることが難しくなってしまいます。

JSRへの移行

Deno公式からJSRというパッケージレジストリが開発されました。deno_stdもこのJSRへパッケージが公開されています。(@std)

https://deno.com/blog/std-on-jsr

JSRが登場したことにより、deno_stdの各モジュールごとに独立してバージョン管理をすることが可能になりました。

これにより、安定性が高いと考えられる@std/bytesについては先日、v1がリリースされました。

@std/path@std/collectionsなどのモジュールについてもv1のRCバージョンがすでに公開されており、近日中にv1がリリースされる可能性が高いと思われます。

逆に@std/expectなどの成熟度がまだ高くないモジュールについては、しばらくv1はリリースせずに開発が継続されていくものと思われます。

deno.land/stdはどうなるの?

deno.land/stdについては、過去のバージョンのdeno_stdは残り続けているものの、JSRへの移行後のバージョンについては公開されていません。

そのため、今後はdeno.land/stdではなくJSRからdeno_stdを利用することが推奨されます。

$ deno add @std/path

Fresh v2

FreshはPreactとesbuildをベースにしたDeno公式のWebフレームワークです。

Fresh v2の開発状況について

元々、Fresh v2向けのコードは独立したブランチで開発が行われていたのですが、先月、mainブランチにマージされました。

https://github.com/denoland/fresh/pull/2449

現在は、Fresh v2のアルファバージョンがJSRで公開されており、近いタイミングでFresh v2がリリースされる可能性があるかもしれません。

Fresh v2のRoadmapについては以下のページで公開されています。

https://github.com/denoland/fresh/issues/2363

また、Freshのリポジトリのwwwディレクトリのコードを見てみるとイメージがしやすいかもしれません。

https://github.com/denoland/fresh/tree/b0b1a306c5b3ca481a0444651aa52647a942ffd6/www

主な変更内容

内容が多くなってしまいそうなので、主要そうなものについてのみ抜粋します。以下のページにも変更点を少しずつまとめていっているため、もし興味がありましたら参照いただければと思います。

JSRへの公開

Fresh関連の各種パッケージがJSRへ公開されています。(@fresh)

この変更に合わせて、Preactやesbuildなどのパッケージがesm.sh経由ではなくnpm:経由(npmレジストリ)で読み込まれるように変更されています。この変更により、Freshのプロジェクトでdeno.lockが利用しやすくなりそうです。

ExpressライクなAPIの提供

以下のように命令的にルーティングやIslandなどの設定をするためのAPIが追加されるようです。これにより、Freshのプラグインからより柔軟に設定をカスタマイズすることができるようになりそうです。

import { App, fsRoutes, trailingSlashes } from "@fresh/core";
  
const app = new App({ root: import.meta.url })
  // ミドルウェアの設定
  .use(trailingSlashes("never"));

// ルーティングの設定
await fsRoutes(app, {
  loadIsland: (path) => import(`./islands/${path}`),
  loadRoute: (path) => import(`./routes/${path}`),
});

export { app };

createDefine()API

FreshにおけるHandlerやページコンポーネントをより定義しやすくするよう、createDefine()というAPIが提供されるようです。

define.ts
import { createDefine, page } from "fresh";

interface State {
  title: string;
}

const define = createDefine<State>();

export const handler = define.handlers({
  async GET(ctx) {
    const { title, content } = await readPost(ctx);
    ctx.title = title;
    return page({ content });
  },
});

export default define.page<typeof handler>(function PostPage(props) {
  return (
    <>
      <article>
        {props.data.content}
      </article>
    </>
  );
});

@fresh/core/compat

Fresh v1との互換性のためのレイヤーが存在するようです。

https://github.com/denoland/fresh/tree/b0b1a306c5b3ca481a0444651aa52647a942ffd6/src/compat

Fresh v1を使ったプロジェクトでFresh v2のアルファバージョンを試してみたい場合に活用すると便利かもしれません。

プロジェクト公正に関する変更

マイグレーションガイドによると以下のような変更がありそうです。

  • fresh.gen.ts/fresh.config.tsの削除
  • 事前ビルドの利用の必須化

Fresh v1においては事前ビルドなどのプロセスが不要であることが特徴の一つでしたが、現在はFreshのホームページからもそれに関する言及が削除されています。

https://github.com/denoland/fresh/pull/2407

今後、Freshのアプリケーションを本番環境にデプロイする際は、事前ビルドは基本的に必須になる可能性が高そうに思います。

マイグレーションガイド

現時点で最新のマイグレーションガイドは以下にあります。(適宜、最新のものを参照いただければと思います)

https://github.com/denoland/fresh/blob/b0b1a306c5b3ca481a0444651aa52647a942ffd6/docs/canary/examples/migration-guide.md

このページでもFresh v2でどのような変更が行われる想定なのか説明されています。


おわりに

Deno v2についてはそろそろ出そうな雰囲気はありそうですが、正直ちょっとまだいつ出るかは分からない状況です。ただし、昨年あたりからマイグレーションガイドの整備が行われていたり、DENO_FUTURE環境変数の導入なども進んでおり、Deno v2に向けた対応については着実に進んでいるようには見えます。個人的にはDeno v2に向けて開発が進められているワークスペース機能についてはかなり便利そうに感じているため、とても良さそうに見えました。

Discussion