虎の穴ラボ技術ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

Deno 1.18 へのアップデートと変更事項まとめ

皆さんこんにちは。おっくんです。

去る 2022 年 1 月 20 日に Deno 1.18 がリリースされました。
今回も、リリースノートを参考に 変更事項の気になるところを紹介したいと思います。

実行環境

Docker イメージ denoland/deno:centos(確認時点では Deno 1.18.0 でした)

Deno 1.18

Deno 1.18 での変更事項をDeno 1.18 リリースノートを元に確認します。

deno.com

Web Cryptography API の更新と、完成

これまでのリリースでも機能拡張されてきた Web Cryptography API が遂に完成したとのことです。

この実装をもって、 Web Cryptography API の Web プラットフォームテストスイートで、Deno は93.4%の項目に合格しました。

この結果は、wpt.fyiで確認できます。

wpt.fyi

(wpt.fyi とは、クロスブラウザの Web プラットフォームのテストスイート(=WPT)のテスト結果を公表している Web サイト)

今回の完成に向けて、新たに導入された API は次の通りです。

  • crypto.subtle.encrypt
    • AES-GCM 形式のサポートを追加
    • AES-CTR 形式のサポートを追加
  • crypto.subtle.decrypt
    • AES-GCM 形式のサポートを追加
    • AES-CTR 形式のサポートを追加
  • crypto.subtle.wrapKey
    • AES-KW 形式のサポートを追加
  • crypto.subtle.unwrapKey
    • AES-KW 形式のサポートを追加
  • crypto.subtle.importKey
    • ECP-384 形式のサポートを追加
  • crypto.subtle.exportKey
    • ECDSA, ECDHpkcs8, spki, jwk 形式のエクスポートのサポートを追加

config ファイルの自動検出機能を追加

Deno 1.14 で、deno fmtdeno lint を実行するとき、 --config に設定を記述したファイルを指定できるようになっていました。 指定することで、一部の lint ルールの変更などを行うことができるようになります。

この設定を記述したファイルを、Deno が自動で検出する機能をリリースすることを Deno 1.14 リリース時に予告していたのですが、こちらがリリースになりました。 自動検出機能は、deno.json、 deno.jsonc ファイルに限定されます。

実行時のコマンドは、次のように変わります。

# Deno 1.17系以前
$ deno run --config ./deno.json ./src/file1.js
$ deno fmt --config ./deno.json
$ deno lint --config ./deno.json

# Deno 1.18から
$ deno run ./src/file1.js
$ deno fmt
$ deno lint

設定ファイルを自動的に検出しますが、置き場所はどこでも OK ということはなく、ファイルが見つからない場合、順を追ってディレクトリツリーを上に'/'までたどり、各ディレクトリでファイルを探索するようになっています。

Error.cause が すべてのスタックトレースを表示するようになりました

Deno 1.13 で導入された Error.cause プロパティは、エラーの原因を取得できるデバッグに便利な機能ですが、こちらが更新されました。 すべてのスタックトレースを表示するようになりました。

リリースノートを参考に確認します。

[error_cause.js]

function fizz() {
  throw new Error("boom!"); // <= エラー箇所1
}

function bar() {
  try {
    fizz();
  } catch (e) {
    throw new Error("fizz() has thrown", { cause: e }); // <= エラー箇所2
  }
}

function foo() {
  try {
    bar();
  } catch (e) {
    throw new Error("bar() has thrown", { cause: e }); // <= エラー箇所3
  }
}

foo();

実行結果は次の通りです。

$ deno run error_cause.js
error: Uncaught Error: bar() has thrown      # <= エラー箇所3
      throw new Error("bar() has thrown", { cause: e });
            ^
    at foo (file:///usr/src/app/error_cause.js:18:13)
    at file:///usr/src/app/error_cause.js:22:3
Caused by: Uncaught Error: fizz() has thrown # <= エラー箇所2
      throw new Error("fizz() has thrown", { cause: e });
            ^
    at bar (file:///usr/src/app/error_cause.js:10:13)
    at foo (file:///usr/src/app/error_cause.js:16:7)
    at file:///usr/src/app/error_cause.js:22:3
Caused by: Uncaught Error: boom!              # <= エラー箇所1
    throw new Error("boom!");
          ^
    at fizz (file:///usr/src/app/error_cause.js:3:11)
    at bar (file:///usr/src/app/error_cause.js:8:7)
    at foo (file:///usr/src/app/error_cause.js:16:7)
    at file:///usr/src/app/error_cause.js:22:3

スタックトレースを順に追っていくようになっています。

注意する点は、Error.cause を順にすべての例外処理で記述しないと、最初の throw されたエラーまで追うことができません。 例えば、次のように書き換えた時は、throw new Error("boom!") まで追ってくれません。

[error_cause_1.js]

function fizz() {
  throw new Error("boom!");
}

function bar() {
  try {
    fizz();
  } catch (e) {
    throw new Error("fizz() has thrown"); // <= { cause: e } を記述しない
  }
}

function foo() {
  try {
    bar();
  } catch (e) {
    throw new Error("bar() has thrown", { cause: e });
  }
}

foo();

次の実行結果のように、Error("fizz() has thrown") までは追いますが、Error("boom!") まで追いません。

$ deno run error_cause.js
error: Uncaught Error: bar() has thrown
      throw new Error("bar() has thrown", { cause: e });
            ^
    at foo (file:///usr/src/app/error_cause.js:18:13)
    at file:///usr/src/app/error_cause.js:22:3
Caused by: Uncaught Error: fizz() has thrown
      throw new Error("fizz() has thrown");
            ^
    at bar (file:///usr/src/app/error_cause.js:10:13)
    at foo (file:///usr/src/app/error_cause.js:16:7)
    at file:///usr/src/app/error_cause.js:22:3

上記のように、一か所書かなかっただけで、根本のエラー原因にたどり着くまでの速さが変わります。
今後、例外処理を記述するときには、積極的に Error.cause を記述していくことになるだろうと感じます。

test steps API が安定化

Deno 1.15 で、ネストされたテストを記述できる test steps API が、実行時に --unstable が必須な実験的 API としてリリースされました。
こちらが安定化しました。

個別のテストステップは、独自のサニタイザースコープ(リソースの解放など確認しテストが期待される方法で動作することを目的とした機能)を持ちます。 このサニタイザーについては、Deno Manual の Test Sanitizersに詳しい解説があります。

リリースノートでは、データベースも関わるテストを例に出しています。 以下は、よりシンプルなテストの例です。

[nest_test.ts]

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

const sq = (src: number) => {
  return src ** 2;
};

Deno.test("test1", async (t) => {
  await t.step("test1-1", async () => {
    assertEquals(sq(1), 1);
  });

  await t.step("test1-1", async () => {
    assertEquals(sq(2), 4);
  });
});

実行すると次のようになります。

$ deno test
Download https://deno.land/std@0.122.0/testing/asserts.ts
Download https://deno.land/std@0.122.0/fmt/colors.ts
Download https://deno.land/std@0.122.0/testing/_diff.ts
Check file:///usr/src/app/nest_test.ts
running 1 test from file:///usr/src/app/nest_test.ts
test test1 ...
  test test1-1 ... ok (5ms)
  test test1-1 ... ok (3ms)
ok (12ms)

test result: ok. 1 passed (2 steps); 0 failed; 0 ignored; 0 measured; 0 filtered out (25ms)

テストのネストはインデントで表現される形です。

また、このネストしたテストを Mocha のスタイルで記述するためのシンプルなポリフィルが公開されています。

これを使用すると、先に実行したテストは次のように記述できます。

[nest_test_mocha.ts]

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

const sq = (src: number) => {
  return src ** 2;
};

describe("test1-1-mocha", () => {
  it("test1-1", async () => {
    assertEquals(sq(1), 1);
  });

  it("test1-2", async () => {
    assertEquals(sq(2), 4);
  });
});

./testlib/deno_mocha.ts として、公開されているポリフィルを置いています。 実行すると次のようになります。(型チェックが動作するとポリフィルのコードにエラーを判定したため、--no-check をつけています。)

$ deno test --no-check nest_test_mocha.ts
running 1 test from file:///usr/src/app/nest_test_mocha.ts
test test1-1-mocha ...
  test test1-1 ... ok (5ms)
  test test1-2 ... ok (3ms)
ok (12ms)

test result: ok. 1 passed (2 steps); 0 failed; 0 ignored; 0 measured; 0 filtered out (25ms)

describeit という記述を見ると 普段 Ruby をよく触る身からは RSpec に近くわかりやすく感じるところです。

FFI API のアップデート

シンボル 型推論を追加

Deno.dlopen によって定義するダイナミックライブラリが提供するシンボルについて、型推論し、一致しない場合には、エラーを返すようになりました。

a + b の結果を返す関数だけを定義したダイナミックライブラリを例に、次のようになります。

[symble.ts]

const libName = `/usr/src/app/add_numbers.so`;

const dylib = Deno.dlopen(libName, {
  add_numbers: { parameters: ["i32", "i32"], result: "i32" },
});

console.log(dylib.symbols.add_numbers(123, 456));
// => 579

console.log(dylib.symbols.add_numbers(123));
// error: Uncaught TypeError: Expected FFI argument to be a signed integer, but got Null

console.log(dylib.symbols.add_numbers(null));
//error: TS2345 [ERROR]: Argument of type 'null' is not assignable to parameter of type 'number'.

型チェックが効くようになったことでより扱いやすくなったと感じます。

シンボル 定義のエイリアスを定義できるようになった

Deno.dlopen で、ダイナミックライブラリにシンボル定義するとき、エイリアスを定義できるようになりました。 用途として、

  • コードスタイルを統一するためにエイリアスで別名を割り当てる(スネークケースで定義されているシンボルをキャメルケースで呼び出す)。
  • 同じ機能を複数のオーバーロードを提供するなど

シンボルの型推論でも使用した add_numbers.so の呼び出しを例に、エイリアスを使用すると次のようになります。

[symble_alias.ts]

const libName = `/usr/src/app/add_numbers.so`;

const dylib = Deno.dlopen(libName, {
  // エイリアス無し
  add_numbers: { parameters: ["i32", "i32"], result: "i32" },
  // スネークケースでダイナミックライブラリに定義の有る関数をキャメルケースでエイリアス設定
  addNumbers: {
    name: "add_numbers",
    parameters: ["i32", "i32"],
    result: "i32",
  },
  // 全くの別名
  add: { name: "add_numbers", parameters: ["i32", "i32"], result: "i32" },
});

console.log(dylib.symbols.add_numbers(123, 456));
// => 579
console.log(dylib.symbols.addNumbers(123, 456));
// => 579
console.log(dylib.symbols.add(123, 456));
// => 579

ダイナミックライブラリ側で決めた関数名に依存せず import { a as b } from 'hogehoge' のようにエイリアスが使えるのは単純に便利です。

Deno.UnsafeFnPointer API の追加

Deno で ダイナミックライブラリから提供される関数ポインターを扱えるようになりました。

C 言語でライブラリを作成し、動作を確認します。

[add_numbers_ptr.c]

int add_numbers(int a, int b) {
  return a + b;
}

int (* get_add_numbers_ptr(int a, int b))(){

  return add_numbers;
}

add_numbers_ptr.c を次のコマンドで、コンパイルします。

$ cc -c -fPIC -o add_numbers_ptr.o add_numbers_ptr.c
$ cc -shared -o add_numbers_ptr.so add_numbers_ptr.o

コンパイルしたダイナミックライブラリから、関数ポインタを介して add_numbers を呼び出す実装は次の通りです。

[unsafe_fn_pointer.ts]

const libName = `/usr/src/app/add_numbers_ptr.so`;

const dylib = Deno.dlopen(libName, {
    get_add_numbers_ptr: { parameters: [], result: "pointer" },
  } as const
);

const addNumbersPtr = dylib.symbols.get_add_numbers_ptr();
const addNumbers = new Deno.UnsafeFnPointer(addNumbersPtr, {
  parameters: ["u32", "u32"],
  result: "u32",
});

console.log(addNumbers.call(123, 456));
// => 579

関数ポインタを扱えるようになったことで FFI API の対応力がまた一つ上がりました。

こういった、FFI API を使用したいくつかのプロジェクトについて、Deno の公式も興味を持っているようです。

などです。

WebSockeet の拡張

アウトバウンド WebSocket へのヘッダー設定を追加

自身から WebSocket 接続を作るとき、ユーザーが独自のヘッダーを設定できるようになりました。
設定したヘッダーは、ハンドシェイクと、WebSocket 接続をするとき追加の情報を提供するのに使用できます。
標準 API の仕様に準拠した拡張では無いので、注意が必要です。

リリースノートにあるサンプルは、次の通りです。

const ws = new WebSocketStream("wss://example.com", {
  headers: { "X-Custom-Header": "foo" },
});

WebSocketStream を使ったコードを動かす場合には、--unstable をつけることが必要です。 headers プロパティには、fetch 関数や、Request、Response コンストラクタの headers プロパティと同じものが設定できるそうです。

fetch 関数の Headers はこちらに解説があります。

developer.mozilla.org

インバウンド WebSocket にキープアライブ機能を追加

WebSocket 接続を Deno.upgradeWebSocket で行うとき、自動で、Pong メッセージを処理するようになり、 他のメッセージが送られていない時、接続を維持するために Ping メッセージを自動的に送信するようになりました。

Ping と Pong メッセージについてはこちらの解説がわかりやすいです。

developer.mozilla.org

Ping Pong メッセージの送信間隔は、Deno.upgradeWebSocket のオプションに idleTimeout を設定できます。 デフォルト値は、120(= 120 秒)で、0 を設定することで、無効にできます。

リリースノートのサンプルは、次の通りです。

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

serve((req: Request) => {
  const { socket, response } = Deno.upgradeWebSocket(req, { idleTimeout: 60 });
  handleSocket(socket);
  return response;
});

function handleSocket(socket: WebSocket) {
  socket.onopen = (e) => {
    console.log("WebSocket open");
  };
}

その他

  • Deno LSP(Language Server Protocol)が改善
  • deno coverage が改善
  • 起動時間が高速化
    • JavaScript ランタイムの起動が 33%高速化
    • TypeScript コンパイラの起動が 10%高速化
  • Deno にバンドルされる V8 のバージョンが、9.8 になりました。

まとめ

Deno 1.18 のリリース内容を見てきました。
Error.cause がすべてのスタックトレースを表示を表示するようなってデバッグ時など追いやすくなったのと、 test steps API が安定化したのがありがたいです。

Deno は、関数を書いたら並びに Deno.test~~~ とテストを書き始められるくらいテストとの距離感が近い プラットフォームです。
そういうことも有り、以前よりテストを頭の片隅に置きながら開発することが増えたと感じるようになりました。

似通ったテストを書く際に、test step API が安定化したらすぐ書き換えたいと思っていたので、プライベートで書いていたものは更新したいと思います。

先日 Deno 公式から 2021 年の Deno の活動について総括記事が公開されました。
Deno 2 についても述べられていて、近日ロードマップが公開される見込みで、今年の前半にはリリースが予定されている様です。
NPM エコシステムとの互換性の向上などの焦点が当たるそうです。 既に、Deno 2.0.0 のマイルストーンが公開されているので、眺めてみると面白いかと思います。

興味深いのは、「deno run 実行時の型チェックをデフォルトでは止める 」かという議論です。

github.com

次回のリリースも追いかけます。

P.S.

虎の穴ラボでは、私たちと一緒に新しいオタク向けサービスを作る仲間を募集しています。
詳しい採用情報は以下をご覧ください。
yumenosora.co.jp