🦕

Deno 1.17が来たぞ

2021/12/18に公開

2021年12月17日にDeno 1.17がリリースされました。

https://twitter.com/deno_land/status/1471615653173993476

https://deno.com/blog/v1.17#updates-to-the-file-watcher

本記事では、上記のリリースノートの内容をざっと紹介していきます。

  1. Import assertions/JSON modulesに対応した
  2. Web Cryptography APIが拡充された
  3. --no-check=remoteフラグが追加された
  4. Deno.connectTls()にALPN対応が追加された
  5. unrefTimer/refTimerが追加された
  6. 各種APIが更新された
  7. TypeScript 4.5に対応した
  8. 標準ライブラリが更新された

Import assertions/JSON modulesに対応した

Import assertionsおよびJSON modulesがフルサポートされました。
これらは今年のはじめにV8にStage 3として導入されました。

リリースノートのサンプルがわかりやすいです。

JSONファイルを読み込む際、1.16.xまではfetch()して.json()するか、Deno.readTextFile()してJSON.parse()するかのどちらかでした。
これらは両方とも--allow-readパーミッションが必要です。

json116.ts
// Get JSON data from remote server
const response = await fetch("https://example.com/data.json");
const jsonData = await response.json();
console.log(jsonData);

// Get JSON data from local file
const text = await Deno.readTextFile("./data.json");
const jsonData = JSON.parse(text);
console.log(jsonData);

1.17から、import jsonData from "..." assert { type: "json" }という記法で直接JSONファイルを読み込めるようになります。
こちらは--allow-readパーミッションが不要(dynamic importの場合は必要)となります。

json117.ts
// Get JSON data from remote server
import jsonData from "https://exmaple.com/data.json" assert { type: "json" };
console.log(jsonData);

// Get JSON data from local file
import jsonData from "./data.json" assert { type: "json" };
console.log(jsonData);

// You can also import JSON modules dynamically
const jsonData = await import("./data.json", { assert: { type: "json" } });
console.log(jsonData);

以下の記事により詳しい解説が載っています。

https://qiita.com/access3151fq/items/942a7e472515c8a7696a

Web Cryptography APIが拡充された

過去のアップデートでも度々更新されてきましたが、Web Criyptography APIに再び機能が追加されました。
これにより、ブラウザとほぼ同機能になったということです。

  • crypto.subtle.importKeyでサポートするインポート形式の追加
    • SPKIフォーマットでのRSA鍵
    • JWKフォーマットでのRSA鍵
    • JWKフォーマットでのAES鍵
    • PKCS#8フォーマットでのEC鍵
    • SPKIフォーマットでのEC鍵
    • JWKフォーマットでのEC鍵
  • crypto.subtle.exportKeyでサポートするエクスポート形式の追加
    • JWKフォーマットでのRSA鍵
    • JWKフォーマットでのAES鍵
  • crypto.subtle.unwrapKeyでサポートするアンラッピングの追加
    • RSA-OAEP
    • AES-GCM

まだ実装していない部分があるようですが、"likely be available in the next release"と書かれており、近いうちに完了する見込みとなっています。
今後のロードマップは以下のissueで議論されています。
https://github.com/denoland/deno/issues/11690

--no-check=remoteフラグが追加された

起動オプションの--no-checkremoteという引数を指定できるようになりました。
ローカルコードのみ型チェックを行い、リモートから読み込むモジュールは型チェックをスキップします。

> deno run --no-check=remote server.ts

将来的に、型チェックのデフォルトの挙動になる可能性があるとのことです。

Deno.connectTls()にALPN対応が追加された

Deno.connectTls()にALPNの対応が追加されました。

https://developer.mozilla.org/ja/docs/Glossary/ALPN

以下はリリースノートのサンプルです。
サーバーがhttp/2をサポートしているかのネゴシエーションを行います。

deno117_alpn.ts
const conn = await Deno.connectTls({
  hostname: "example.com",
  port: 443,
  alpnProtocols: ["h2", "http/1.1"],
});
const { alpnProtocol } = await conn.handshake();
if (alpnProtocol !== null) {
  console.log("Negotiated protocol:", alpnProtocol);
} else {
  console.log("No protocol negotiated");
}

現時点では、実行に--unstableフラグが必要です。

> deno run --allow-net --unstable deno117_alpn.ts
Negotiated protocol: h2

unrefTimer/refTimerが追加された

新たなunstable APIとして、Deno.unrefTimer(id: number)Deno.refTimer(id: number)が追加されました。
タイマー(setTimeoutsetInterval)のイベントループ終了時のブロック周りの挙動を調整できます。

デフォルトでは、すべてのタイマーはクリアされない限りイベントループの終了をブロックします。
以下は、最初にhello world!を表示し、2秒毎にhello after 2 secを表示するコードです。

block_by_timer.ts
setInterval(() => {
  console.log("hello after 2 sec");
}, 2000);

console.log("hello world!");

タイマーのIDをDeno.unrefTimerに指定することで、イベントループのブロックを回避できます。
以下のコードでは、hello world!表示後に即終了します。

unref_timer.ts
const timerId = setInterval(() => {
  console.log("hello after 2 sec");
}, 2000);

Deno.unrefTimer(timerId);

console.log("hello world!");

このサンプルではconsole.logを使っていたため一瞬で終了してしまいますが、この部分が一定の時間を要する処理で、その間setIntervalで外部からデータを取得したいとかログを出したいとかの用途に便利そうです。

各種APIが更新された

以下のAPIに更新が入りました。

AbortSignal

1.16でAbortSignalに理由が指定できるようになったのですが、これが以下のAPIで適切に伝搬されるようになりました。

  • ファイル入出力:Deno.readFile() Deno.readTextFile() Deno.writeFile() Deno.writeTextFile()
  • Stream API関連: ReadableStream TransformStream WritableStream WebSocketStream
  • fetch関連:fetch Request Response

さらに、throwIfAborted()メソッドが追加されました。
名前そのままですが、シグナルが既にAbortされている場合に例外を投げる機能を持ちます。
以下はリリースノートのサンプルです。

const controller = new AbortController();
const signal = controller.signal;

try {
  signal.throwIfAborted();
} catch (err) {
  unreachable(); // the abort signal is not yet aborted
}

controller.abort("Hello World");

try {
  signal.throwIfAborted();
  unreachable(); // the method throws this time
} catch (err) {
  assertEquals(err, "Hello World");
}

Deno Language Server

DenoのLSPが更新され、モジュールの補完まわりの機能が強化されました。
また、キャッシュが無制限ではなく、レジストリから送られてくるヘッダを尊重するようになりました。
機能を適切に利用するには、以下のようにlspの設定ファイルのdeno.suggest.imports.hostsに使用するレジストリを指定してください。

your-lsp-setting-file.json
{
  "deno.suggest.imports.hosts": {
    "https://deno.land": true,
    "https://cdn.nest.land": true,
    "https://crux.land": true
  }
}

加えて、vscode_denoにも更新が入り、有名なレジストリはデフォルトで有効になっているようです。
リリースノートには、esm.shSkypackJSPMなどとの連携が強化されたとも記載があります。
Deno専用に限らず、Nodeの資産を利用する方向でも開発環境が整ってきています。

deno test

Deno.test() APIに更新が入りました。
これまで、Deno.test()の書き方には以下の2通りがありました。

import { assertEquals } from "https://deno.land/std@0.118.0/testing/asserts.ts";

// "name", function
Deno.test("My test description", (): void => {
  assertEquals("hello", "hello");
});

// { "name", fn(), options }
Deno.test({
  name: "example test",
  ignore: Deno.build.os == "windows",
  fn(): void {
    assertEquals("world", "world");
  },
});

今回のリリースで、次の4通りが追加されました。

import { assertEquals } from "https://deno.land/std@0.118.0/testing/asserts.ts";

// namedFunction
Deno.test(function myTestName(): void {
  assertEquals("hello", "hello");
});

// "name", { options }, function
Deno.test("My test description", { permissions: { read: true } }, (): void => {
  assertEquals("hello", "hello");
});

// { "name", options }, function
Deno.test(
  { name: "My test description", permissions: { read: true } },
  (): void => {
    assertEquals("hello", "hello");
  },
);

// { options }, namedFunction
Deno.test({ permissions: { read: true } }, function myTestName(): void {
  assertEquals("hello", "hello");
});

ユーザーが必要な項目だけ書けるようにバリエーションが増えた形ですが、自由度が高まったと見るか混沌に近づいたと見るかは人それぞれでしょうか。

また、nested testの実行結果に、ネストされたステップの成功/失敗などの情報が含まれるようになりました。

deno117_test.ts
Deno.test("nested failure", async (t) => {
  const success = await t.step("step 1", async (t) => {
    let success = await t.step("inner 1", () => {
      throw new Error("Failed.");
    });
    if (success) throw new Error("Expected failure");

    success = await t.step("inner 2", () => {});
    if (!success) throw new Error("Expected success");
  });

  if (success) throw new Error("Expected failure");
});

stepの実行には--unstableフラグが必要です。
これは1.18で安定化が予定されています。

> deno test --unstable deno117_test.ts
running 1 test from file:///Users/kawarimidoll/deno_sample/deno117_test.ts
test nested failure ...
  test step 1 ...
    test inner 1 ... FAILED (5ms)
      Error: Failed.
          at file:///Users/kawarimidoll/deno_sample/deno117_test.ts:5:13
          at testStepSanitizer (deno:runtime/js/40_testing.js:187:13)
          at asyncOpSanitizer (deno:runtime/js/40_testing.js:68:15)
          at resourceSanitizer (deno:runtime/js/40_testing.js:138:13)
          at exitSanitizer (deno:runtime/js/40_testing.js:170:15)
          at TestContext.step (deno:runtime/js/40_testing.js:800:19)
          at file:///Users/kawarimidoll/deno_sample/deno117_test.ts:4:27
          at testStepSanitizer (deno:runtime/js/40_testing.js:187:13)
          at asyncOpSanitizer (deno:runtime/js/40_testing.js:68:15)
          at resourceSanitizer (deno:runtime/js/40_testing.js:138:13)
    test inner 2 ... ok (3ms)
  FAILED (10ms)
FAILED (13ms)

failures:

nested failure
Error: 1 test step failed.
    at runTest (deno:runtime/js/40_testing.js:432:11)
    at async Object.runTests (deno:runtime/js/40_testing.js:541:22)

failures:

        nested failure

test result: FAILED. 0 passed (1 step); 1 failed (2 steps); 0 ignored; 0 measured; 0 filtered out (24ms)

error: Test failed

上記のtest resultの行の(1 step)などがそれです。
筆者の手許で試した限りでは、1.16.2の時点で入っていたものと思われます。

--watch フラグ

起動時の--watchフラグに更新が入りました。
以下のように引数を渡すことで、module graphにないファイル(要するに実行ファイル内でimportされていないファイル)の更新を監視できるようになりました。

> deno run --watch=data/external.txt script.ts

また、リロードしたときにコンソールをクリアするようになりました。

自分が以下のプロジェクトで作ったものがほぼ全て公式から提供されてしまいました。
使いやすくなるのは嬉しいことだけど一抹の寂しさがある…。

https://zenn.dev/kawarimidoll/articles/d371abbd46b65b

REPL

denoまたはdeno replで起動するREPLが更新されました。

モジュール名の補完

以下のように入力して<TAB>キーを押すとパスが補完されるようになりました。

> import xxx from "https://deno.land/x

Node.jsモードの対応

deno repl --compat --unstableで起動することで、REPLでもNode.jsモードが使えるようになりました。

--unsafely-ignore-certificate-errorsフラグの追加

--unsafely-ignore-certificate-errorsフラグを付けて起動することで、TLS証明書の検証を無効にした状態でREPLを実行できるようになりました。
エラーを無視してしまうと危険なので使用には注意が必要とのことです。

FFI API

1.15当時に追加されていたFFI APIですが、これに関連してDeno.UnsafePointerDeno.UnsafePointerViewAPIが追加されました。
筆者のFFIの知見が足りないのでここについて詳しいことが書けません、申し訳ないです…。

TypeScript 4.5に対応した

最新のTypeScript 4.5に対応したことで、新しいimport記法が使えるようになりました。
関数やクラス等と型を一度にimportに書けるようになります。

import_example.ts
// old
import { A } from "https://example.com/mod.ts";
import type { B } from "https://example.com/mod.ts";

// new
import { A, type B } from "https://example.com/mod.ts";

標準ライブラリが更新された

標準ライブラリのdeno_stdも0.118.0に更新されました。

破壊的変更

以下のBREAKING CHANGEが入っています。

  • wsモジュールを削除
    • 代わりにDeno.upgradeWebSocket()を使用してください
  • testing/asserts.tsassertThrowsAsyncを削除
    • 代わりに同モジュールのassertRejectsを使用してください
  • http/server_legacy.tsを削除
    • 代わりにhttp/server.tsを使用してください
  • fs/mod.tsからcopyを削除
    • 代わりにfs/copy.tsから直接インポートしてください
  • signalsからonSignalを削除
    • 代わりにDeno.addSignalListener()を使用してください
  • collectionsからfindLastfindLastIndexを削除
    • ArrayTypedArray自体にfindLastfindLastIndexが定義されているのでそちらを使用してください
  • httpサーバが文字列のアドレスを受け付けなくなり、{ host: string, port: number }インターフェースを利用するよう変更

すでにdeprecatedになっていたものが削除されたというのが多いですが、結構BREAKINGなので、各モジュールを使用している場合は確認したほうが良さそうです。

HTTPサーバーのオプションの追加

これまでstd/serverserveは、ハンドラの生成するエラーを無視してしまっていましたが、0.118からはエラーをconsole.errorで出力するようになりました。
この挙動は、ServerクラスにonErrorを渡すことでカスタマイズできます。

server118.ts
import { Server } from "https://deno.land/std@0.118.0/http/server.ts";

const port = 4505;
const handler = (request: Request) => {
  const body = `Your user-agent is:\n\n${
    request.headers.get(
      "user-agent",
    ) ?? "Unknown"
  }`;

  return new Response(body, { status: 200 });
};

const onError = (_error: unknown) => {
  return new Response("custom error page", { status: 500 });
};

const server = new Server({ port, handler, onError });

std/collectionsへの関数の追加

aggregateGroups関数が追加されました。
値が配列であるようなオブジェクトとアグリゲータ関数を引数に取り、値にアグリゲータ関数を適用した結果のオブジェクトを返します。

日本語だと意味がわかりませんね。コードをご覧ください。
[Array.prototype.reduce()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce)をオブジェクトのvalueに対して実行しているようなイメージでしょうか。

aggregate_groups.ts
import { aggregateGroups } from "https://deno.land/std@0.118.0/collections/mod.ts";
const foodProperties = {
  Curry: ["spicy", "vegan"],
  Omelette: ["creamy", "vegetarian"],
};
const descriptions = aggregateGroups(
  foodProperties,
  (current, key, first, acc) =>
    first ? `${key} is ${current}` : `${acc} and ${current}`
);
console.log(descriptions);
> deno run aggregate_groups.ts
{ "Curry": "Curry is spicy and vegan", "Omelette": "Omelette is creamy and vegetarian" }

関係ないのですが、リリースノートにstd/collections is a playground for us to experiment with more advanced functions for iterating over arrays and iterables.と記載があり、playgroundだったんか…と思いました。確かに他のモジュールと比べて実験的な関数も多いように感じられますね。
個人的には普通に便利で面白いと感じています。

fmt/bytesの追加

fmt/bytesモジュールが追加されました。バイト形式を人間が読みやすい形式にフォーマットするための関数を提供します。

fmt_bytes.ts
import { prettyBytes } from "https://deno.land/std@0.118.0/fmt/bytes.ts";
import { assertEquals } from "https://deno.land/std@0.118.0/testing/asserts.ts";

assertEquals(prettyBytes(1024), "1.02 kB");
assertEquals(prettyBytes(1024, { binary: true }), "1 kiB");

npmのpretty-bytesモジュールを移植したものとのことです。

おわりに

個人的には今回のリリースでは--watch機能の更新が印象深いです。
記事中でも少し触れましたが、自作のツールでこねくり回してなんとか実装していたのですが、公式から提供されたことで、御役御免となりました。
嬉しいような寂しいような。

https://github.com/kawarimidoll/deno-dex

また、秋ごろに少し話題の出ていたDeno 2は、一旦保留になったようです。

ブログで触れられていない詳細な更新内容に関しては、GitHubのリリースページをご覧ください。
https://github.com/denoland/deno/releases/tag/v1.17.0
https://github.com/denoland/deno_std/releases/tag/0.118.0

Discussion