虎の穴開発室ブログ

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

MENU

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

皆さんこんにちは。年の瀬をどの様にお過ごしでしょうか?おっくんです。

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

実行環境

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

hub.docker.com

Deno 1.17

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

import assertions と、JSON modules

Deno 1.17 では、import assertions と JSON modules を完全な形でサポートします。

import assertions は、V8 にリリースされた 機能です。 これが、Deno でも使用できるようになりました。

このプロポーザルの使用例は、JSON モジュールのインポートを許可することです。
アサーションを使用せず、MIME タイプのみに依存してファイルを区別するのは、セキュリティ上の問題が危惧されます。

リリースノートを参考に紹介します。

// Deno 1.17 以前
// リモートサーバーからのJSONの取得
const response = await fetch("https://example.com/data.json");
const jsonData = await response.json();
console.log(jsonData);

// ローカルファイルの読み込みとしてJSONの取得
const text = await Deno.readTextFile("./data.json");
const jsonData = JSON.parse(text);
console.log(jsonData);

// また、ファイルURLでの読み込みも可能です。
const response_file = await fetch(`file://${Deno.cwd()}/data.json`);
console.log(await response_file.json());

続けて Deno 1.17 で導入される import assertions を使用した記述は、次のようになります。

// Deno 1.17 以降
// リモートサーバーから インポート
import jsonData from "https://exmaple.com/data.json" assert { type: "json" };
console.log(jsonData);

// ローカルファイルをインポート
import jsonData from "./data.json" assert { type: "json" };
console.log(jsonData);

// ダイナミックインポートでも、import assertions が使用できます。
const jsonDataDy = await import("./data.json", { assert: { type: "json" } });
console.log(jsonDataDy);
// => Module { default: { hoge: "fuga" } }
//    ダイナミックインポートはModele で包まれた形で取得できます。

ポイントになるのは、import 宣言で import assertions を使う場合には、--allow-read--aloow-net が不要な点です。 ダイナミックインポートでは、権限付与が必要です。

import assertions は Chrome 91 にも導入済みですが、FireFoxなどのプラットフォームは対応していないようです。

v8.dev

--no-check=remote フラグが導入されます

Deno の一般的な問題として依存関係が解決されているが、型チェックが正しく動作しないことが有ることです。
Deno 1.17 から、--no-checkout=remote が追加されます。
このフラグの追加により、「型チェックはされるがリモートモジュールの診断結果を破棄」するようになります。

次のようにフラグを付与して実行します。

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

このフラグを試してフィードバックを送ることが歓迎されています。
--no-checkout=remote の提供された状態が、将来デフォルトの設定になることが検討されているようです。

ALPN ネゴシエーションを不安定版としてサポート

ALPN は、クライアント-サーバー間で通信プロトコルをネゴシエートする TLS の拡張です。
これをDeno.connectTls()関数で実行できるようになりました。

リリースノートから紹介します。

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 が必要です。

Unref timers が導入されます

新たな unstable な API として次の二つが追加されます。

  • Deno.unrefTimer(id: number)
  • Deno.refTimer(id: number)

これらの API を使うことで、setTimeoutsetInterval の動作を変更し終了時にイベントループをブロックをする/しないを設定できるようになります。

これらが導入されても、デフォルトではこれまで通りイベントループがクリアされるまでイベントループの終了をブロックします。

リリースノートを参考に実行結果を見ていきます。

[timer_1.ts]

setTimeout(() => {
  console.log("hello from timeout");
}, 5000);

console.log("hello world!");

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

$ deno run timer_1.ts
hello world!
hello from timeout <= 5秒後に表示して終了

setTimeoutを使うときによくイメージする動作です。
それでは、Deno.unrefTimer(id: number) を導入します。

[timer_2.ts]

const timerId = setTimeout(() => {
  console.log("hello from timeout");
}, 5000);

Deno.unrefTimer(timerId);

console.log("hello world!");

こちらを実行すると次のようになります。

$ deno run --unstable timer_2.ts
hello world! <= 表示直後に終了

続けて、リリースノートを参考にsetIntervalを使用したサンプルを紹介します。 リリースノートでは、テレメトリの送信タスクのようなものが想定されていますが、定期実行するのはコンソールへのへの出力としています。

[timer_3.ts]

setInterval(() => {
  console.log("Output from Interval");
}, 1000);

const longRunningTask = () => {
  return new Promise((resolve) =>
    setTimeout(() => console.log("End of Task"), 3000)
  );
};

longRunningTask();

実行すると、次のようにメインの実行対象になっている longRunningTask() の処理終了後も setInterval() で設定したタスクが実行され続きます。

$ deno run timer_3.ts
Check file:///usr/src/app/timer_3.ts
Output from Interval
Output from Interval
End of Task
Output from Interval
Output from Interval
Output from Interval <= ctrl + cで終了しないと永遠に実行が続く 

こちらに Deno.unrefTimer(id: number) を導入したのが次の timer_4.ts になります。

[timer_4.ts]

const id = setInterval(() => {
  console.log("Output from Interval");
}, 1000);

const longRunningTask = () => {
  return new Promise((resolve) =>
    setTimeout(() => console.log("End of Task"), 3000)
  );
};

Deno.unrefTimer(id);

longRunningTask();

実行すると、次のようにメインの実行対象になっている longRunningTask() の処理終了後 setInterval() で設定したタスクが実行されること無く終了します。

$ deno run timer_4.ts
Check file:///usr/src/app/timer_3.ts
Output from Interval
Output from Interval
End of Task <= longRunningTask() の終了によりプログラム全体が終了する 

timer_1.ts と timer_2.ts、timer_3.ts と timer_4.ts を比較すると、Deno.unrefTimer() の実行の有無でイベントループの終了をブロックしているかの動作が変わっているのがわかります。

Node.js では setTimeout の返すオブジェクトが unref()ref()メソッドを持っており、同様の動作が実現できていました。

nodejs.org

また、Deno の Node.js 互換モード(実行時に--compatを付与)での動作では、unref()ref() をサポートしていませんでした。
Node.js との互換性が無い部分になるので注意が必要になるかと思います。

リリースノートでは、この機能のユースケースとしてテレメトリの送信を例に示されていました。

「理由の指定 を指定した AbortSignal の実行」にいくつかの API が対応しました

Deno 1.16 で  AbortSignal に理由を指定できるようになりました。
Deno 1.17 では、次のメソッドでこの変更に対応しました。

  • Deno.readFile()
  • Deno.readTextFile()
  • Deno.writeFile()
  • Deno.writeTextFile()
  • WHATWG が策定しているすべての streams API(ReadableStream、TransformStream、WritableStream など)
  • WebSocketStream
  • fetch
  • Request
  • Response

加えて、throwIfAborted() メソッドが追加されました。
これを使用することで AbortSignal の状態に基づいて同期させ例外処理を記述することができます。

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

[abort.ts]

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

try {
  signal.throwIfAborted();
  console.log("Try[1]");
} catch (err) {
  console.log(`Catch[1]:${err}`);
}

controller.abort("Abort!!");

try {
  signal.throwIfAborted();
  console.log("Try[2]");
} catch (err) {
  console.log(`Catch[2]:${err}`);
}

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

$ deno run abort.ts
Try[1]
Catch[2]:Abort!!

abort() が実行された後だけ、throwIfAborted() で throw することができ、abort()に設定した理由をcatch 節で取得することができます。

deno test のアップデート

Deno.test() のオーバーロードの追加

テストは、Deno.test() を使用して登録することができました。
このDeno.test()には、「省略形」と詳細に制御できる「非省略形」の 2 つのオーバーロードが有ります。 例えば、次のようになります。

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

// 省略形
Deno.test("shorthand test", (): void => {
  assertEquals(2, 1 + 1);
});

// 非省略形
Deno.test({
  name: "definition test",
  ignore: Deno.build.os == "windows",
  fn(): void {
    assertEquals(2, 1 + 1);
  },
});

上の2つのオーバーライドの通り、パラメータの内容がかなり異なっています。
書き換える場合には大幅な変更となってしまうので、この負担を軽減して柔軟性を高める為に、4 つのオーバーライドが追加されます。

Deno.test(function newStyleTest1(): void {
  assertEquals(2, 1 + 1);
});

Deno.test(
  "New style test2",
  { ignore: Deno.build.os == "windows" },
  (): void => {
    assertEquals(2, 1 + 1);
  }
);

Deno.test(
  { name: "New style test2", ignore: Deno.build.os == "windows" },
  (): void => {
    assertEquals(2, 1 + 1);
  }
);

Deno.test({ ignore: Deno.build.os == "windows" }, function myTestName(): void {
  assertEquals(2, 1 + 1);
});

これまでの 2 つのオーバーライドでの記述方法を混ぜたような形になるので、ドキュメントを見ながらどののスタイルで書くのか決めていければいいものと感じます。
これまでの省略形も十分に省略された形でしたが、名前付き関数だけを引数に取れる新しいスタイルは、かなり多用されるのではないかと思います。

テストのネスト機能のアップデート

Deno 1.15 で unstable な API として Deno.test() の中で、step() を用いてテストのネストが行えるようになりました。
このネストしたテストは実行後のレポートでtest() の単位で出力されていましたが、Deno 1.17 からstep()で登録したテストの単位でもレポートが出力されるようになりました。

次のテストを Deno 1.16 と Deno 1.17 の表示を比較してみます。

[nest_test.ts]

// 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-2", async () => {
    assertEquals(sq(2), 4);
  });
});

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

# Deno 1.16
$ deno test --unstable nest_test.ts
running 1 test from file:///usr/src/app/nest_test.ts
test test1 ...
  test test1-1 ... ok (9ms)
  test test1-2 ... ok (6ms)
ok (23ms)

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

# Deno 1.17
$  deno test --unstable nest_test.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-2 ... ok (2ms)
ok (12ms)

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

上の通り、Deno 1.17 で実行したものには、内訳(2 steps) が出力されています。 この内訳が、passed, failed, ignored, measured のそれぞれにされるようになりました。

file watcher のアップデート

Deno は、--watch をつけることでファイルの変更を監視することができていました。
監視の対象はアプリケーションの依存しているソースコードだけでした。
Deno 1.17 から次のように監視対象に設定する任意のファイルを設定するリストを受け入れるようになりました。

$ deno run --watch=params.txt,params2.txt app.ts

加えて、再起動時にコンソールがクリアされるので、かなり見やすくなりました。

REPL のアップデート

モジュールインポートの補完

REPL(deno とだけ打って起動したときのもの) が import 文の "https://deno.land/" 以降を補完してくれるようになりました。
ダイナミックインポートは補完しないので注意です。

Node.js 互換モード対応

Deno 1.15 で導入された Node.js 互換モードで REPL が呼び出されるようになりました。
次のように起動して Node.js の fs モジュールを REPL で呼び出しできます。

$ deno repl --compat --unstable
Deno 1.17.1
exit using ctrl+d or close()
> const fs = require('fs')
undefined
> fs.readdir('.', (err, files) => {
    files.forEach(file => {
        console.log(file);
    });
});
a.txt
app.ts
b.txt
undefined

TLS 証明書の検証を無視するできるようになった

Deno 1.17 で、TLS 証明書の検証を無視できるよになる --unsafely-ignore-certificate-errors フラグが REPL に追加されました。

こちらの使用は、危険であることが説明されているので、アクセス先がイントラである場合など別途アクセス先が信用に足る理由など無い限り使わない方がいいでしょう。

FFI API のアップデート

Deno 1.15 で導入された FFI API がアップデートされました。
このリリースでは、FFI 機能を使う Deno 側(JavaScript)と呼び出される側との間で、ポインターへのアクセスを提供する次の 2 つの新たな API が追加されます。

  • Deno.UnsafePointer
  • Deno.UnsafePointerView

加えて、パラメータタイプ buffer は、pointer に名前が変更になりました。

リリースノートでは、Rust での実装例が示されていますので、こちらでは C 言語でライブラリ中の文字列を Deno 側に取り出しを実装してみます。

[ptr.c]

const unsigned char BUFFER[8] = {1, 2, 3, 4, 5, 6, 7, 8};

const unsigned char* return_buffer() {
  return &BUFFER[0] ;
}

ptr.c を次の手順で、ライブラリに変換します。

$ yum install -y gcc
$ gcc --version
gcc (GCC) 8.5.0 20210514 (Red Hat 8.5.0-4)
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ gcc -c -fPIC ptr.c -o ptr.o
$ gcc -shared -o ptr.so ptr.o

ptr.so が作成できたので呼び出し側の ffi.js を作成します。

const dylib = Deno.dlopen("./ptr.so", {
  return_buffer: { parameters: [], result: "pointer" },
});

console.log(dylib.symbols.return_buffer());
const ptr = dylib.symbols.return_buffer();
const ptrView = new Deno.UnsafePointerView(ptr);
console.log(ptrView);

const into = new Uint8Array(8);

ptrView.copyInto(into);
console.log([...into]);

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

$ deno run --allow-ffi --unstable ffi.js
UnsafePointer { value: 139751684358176n }
UnsafePointerView { pointer: UnsafePointer { value: 139751684358176n } }
[
  1, 2, 3, 4,
  5, 6, 7, 8
]

C言語で作成したライブラリが関数で返すポインタを Deno 側で取得し文字列として取得することができました。

リリースノートでは、数値の配列をポインタを介して出力したり、JavaScript 側で作成した文字列をポインタで Rust で実装したライブラリに送ったりということをしています。

これまで、Deno の提供する FFI 機能がポインターを扱えるようになり非常に応用が利くようになりました。
この機能を利用した実装例として sqlite3 モジュールが紹介されているので、ぜひこちらもご覧ください。

github.com

標準ライブラリのアップデート

重大な変更

Deno の標準ライブラリ std@0.118.0 には、多数の重大な変更が入りました。

  • 削除
    • ws モジュール
      • 代替方法:Deno.upgradeWebSocket() API を使う
    • testing/asserts.ts の assertThrowsAsync
      • 代替方法:testing/asserts.ts の assertRejects を使う
    • http/server_legacy.ts
      • 代替方法:http/server.ts を使う
    • fs/mod.ts の copy
      • 代替方法:fs/copy.ts をインポートして使う
    • signals の onSignal
      • 代替方法:Deno.addSignalListener() API を使用する
    • findLast と findLastIndex
      • 代替方法:collections モジュールを使う
  • 仕様変更
    • http モジュールが、文字列の addr インターフェースを取りやめ、{ host: string, port: number } を使用するように変更

HTTP サーバーに新しいオプションが追加

std@0.116.0 までの http/server.ts が提供する Server クラスは、ハンドラのエラーをコンソールに出力するといったことが無く、デバッグが難しいものでした。
std@0.117.0 からは、ハンドラのエラーを、console.error で返すとともに、Server に onError 関数を登録し、エラー時の挙動をカスタマイズできるようになりました。

std@0.116.0 とstd@0.118.0 でリリースノートのサンプルを参考に実装と実行結果を比較してみます。

[app_116.ts]

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

let count = 0;

const addr = ":8080";

const handler = (request: Request) => {
  const body = `Your user-agent is:\n\n${
    request.headers.get("user-agent") ?? "Unknown"
  }`;
  count++;
  if (count % 2 == 0) {
    console.log("OK");
  } else {
    throw "ERROR Msessage";
  }

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

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

server.listenAndServe();

ワザと 2 回に 1 回 throw し、レスポンスを返さないサーバーアプリを作成しました。
実行すると次のように、throw しなかったときだけ、出力があり、動作が確認できます。

$ deno run -A app_116.ts
Check file:///usr/src/app/app_117.ts
OK
OK
OK
OK

同様の動作をするサーバーアプリをstd@1.118.0を使用して作成します。

[app_118.ts]

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

let count = 0;
const port = 8080;
const handler = (request: Request) => {
  const body = `Your user-agent is:\n\n${
    request.headers.get("user-agent") ?? "Unknown"
  }`;
  count++;
  if (count % 2 == 0) {
    console.log("OK");
  } else {
    throw "ERROR Msessage";
  }
  return new Response(body, { status: 200 });
};

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

server.listenAndServe();

ワザと 2 回に 1 回 throw し、レスポンスを返さないサーバーアプリをstd@1.118.0を使用して作成しました。
実行すると次のように、throw したとき内容をコンソールに出力するようになりました。

$ deno run -A app_116.ts
Check file:///usr/src/app/app_118.ts
ERROR Msessage
OK
ERROR Msessage
OK

先述の通り console.error での出力に加え onError 関数で動作のカスタマイズが可能です。

[app_118_custom.ts]

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

let count = 0;
const port = 8080;
const handler = (request: Request) => {
  const body = `Your user-agent is:\n\n${
    request.headers.get("user-agent") ?? "Unknown"
  }`;
  count++;
  if (count % 2 == 0) {
    console.log("OK");
  } else {
    throw "ERROR Msessage";
  }
  return new Response(body, { status: 200 });
};

const onError = (_error: unknown) => {
  console.error(`ERROR:${_error}`);  // ERROR: を先頭につけてコンソールへ出力
  return new Response("custom error page", { status: 500 });
};

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

server.listenAndServe();

onError 関数を追加、登録しました。
実行すると次のように、throw したとき内容 onError 関数で処理していることを確認できます。

$ deno run -A app_118_custom.ts
Check file:///usr/src/app/app_118_custom.ts
ERROR:ERROR Msessage
OK
ERROR:ERROR Msessage
OK

エラーレスポンスや、ロギングに有益に利用できるのではないかと思います。

std/collections が追加

std/collections に、イテレーターと配列の処理用の関数群が実装されています。
こちらに新たに、aggregateGroups 関数が追加になりました。

リリースノートでは、次のソースコードが紹介されています。

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

const foodProperties = {
  "Curry": ["spicy", "vegan"],
  "Omelette": ["creamy", "vegetarian"],
};

const descriptions = aggregateGroups(
  foodProperties,
  (current, key, first, acc) => {
    if (first) {
      return `${key} is ${current}`;
    }
    return `${acc} and ${current}`;
  },
);

assertEquals(descriptions, {
  "Curry": "Curry is spicy and vegan",
  "Omelette": "Omelette is creamy and vegetarian",
});

std/fmt/bytes の追加

std@0.115.0 から fmt/bytes.ts モジュールが追加されました。 このモジュールは、prettyBytes 関数を提供します。

import { prettyBytes } from "https://deno.land/std@0.115.0/fmt/bytes.ts";

console.log(prettyBytes(1024))
// => 1.02 kB
console.log(prettyBytes(1024, { binary: true }))
// => 1 kiB

こちらのライブラリは、npm パッケージ pretty-bytes の移植だそうです。

www.npmjs.com

その他

  • Deno Language Server がアップデート
  • Improvements to the Web Cryptography API
    各種形式のキーのサポートが拡充されました。
  • Deno に TypeScript 4.5 が導入されました これまで、型と関数のインポートはインポート先が同一ファイルでも別に記述する必要がありましたが、 一つの import で、それらをまとめでインポートできるようになりました。
// TypeScript 4.4 以前
import { A } from "https://example.com/mod.ts";
import type { B } from "https://example.com/mod.ts";

// TypeScript 4.5 で導入される記法
import { A, type B } from "https://example.com/mod.ts";

まとめ

今回のエポックなリリースはimport assertionsとFFIのアップデートでしょうか。
import assertions は、JSONをインポートできるようになることでCLIの作成や、特定のパラメータを与えてdeno compileでスタンドアロンなバイナリを作成したいという場合に重宝するのでは?と感じています。

FFIは、バッファーに対応してポインターのやり取りができることで、非常に使い勝手が良くなりました。
FFIを使うライブラリとして、deno_win32 というものも最近公開されていました。

github.com

「FFIで何か作ろう」という流れがこれから加速しそうです。

P.S.

採用情報
■募集職種
yumenosora.co.jp

カジュアル面談も随時開催中です
■お申し込みはこちら!
news.toranoana.jp

■ToraLab.fmスタートしました!
メンバーによるPodcastを配信中!
是非スキマ時間に聞いて頂けると嬉しいです。
anchor.fm
■Twitterもフォローしてくださいね!
ツイッターでも随時情報発信をしています
twitter.com