虎の穴開発室ブログ

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

MENU

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

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

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

Deno 1.24

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

型チェックと、エミッション性能の向上

以前の Deno は、TypeScript から JavaScript への変換に 2 種類の方法が取られていました。

  • Typescript コンパイラ
  • swc

一般には、swc が使用され、--check フラグが付与されると、TypeScript コンパイラが使用されていました。 Deno 1.24 から、コンパイルにはすべて swc が使用されるようになります。

いくつかのリファクターにより、大幅なパフォーマンスの改善が見込まれています。

unhandledrejection イベントのサポートを追加

unhandledrejection イベントは、拒否ハンドラを持たない Promise が Reject されたときにグローバルスコープに送られるイベントです。

「拒否ハンドラを持たない」という状況がわかりにくく感じますが、要は .catch がない Promise のことです。

次のサンプルで確認します。

[unhandledrejection_test.ts]

globalThis.addEventListener("unhandledrejection", (e): void => {
  console.log("unhandled rejection at:", e.promise, "reason:", e.reason);
  e.preventDefault();
});

function noCatch() {
  Promise.reject(new Error("reject noCatch"));
}

function hasCatch() {
  Promise.reject(new Error("reject noCatch")).catch((e) => console.error(e));
}

noCatch();
hasCatch();

Promise.reject(new Error("reject global no catch"));
Promise.reject(new Error("reject global has catch ")).catch((e) =>
  console.error(e)
);

こちらを実行すると、次のように出力されます。

$ deno run unhandle.ts
Error: reject hasCatch # <--- catch で出力
    at hasCatch (file:///usr/src/app/unhandle.ts:11:18)
    at file:///usr/src/app/unhandle.ts:15:1
Error: reject global has catch  # <--- catch で出力
    at file:///usr/src/app/unhandle.ts:18:16
unhandled rejection at: Promise { # <--- unhandledrejection イベントで出力
  <rejected> Error: reject noCatch
    at noCatch (file:///usr/src/app/unhandle.ts:7:18)
    at file:///usr/src/app/unhandle.ts:14:1
} reason: Error: reject noCatch
    at noCatch (file:///usr/src/app/unhandle.ts:7:18)
    at file:///usr/src/app/unhandle.ts:14:1
unhandled rejection at: Promise { # <--- unhandledrejection イベントで出力
  <rejected> Error: reject global no catch
    at file:///usr/src/app/unhandle.ts:17:16
} reason: Error: reject global no catch
    at file:///usr/src/app/unhandle.ts:17:16

上記の出力結果を見ると、.catch を置いていない Promise.reject だけが、unhandledrejection イベントで検知できていることがわかります。

beforeunload イベントのサポートを追加

beforeunload イベント は、ウィンドウや、ドキュメントなどのリソースの解放、ブラウザならページの遷移がされる前に確認させる際に使用するようなイベントです。

こちらが Deno で使用できるようになります。

[beforeunload_1.ts]

window.addEventListener("beforeunload", function (e) {
  e.preventDefault();
  e.returnValue = "";
});

このようにして実行すると、アプリケーションが終了しないままとなります。 ブラウザで確認するときのように確認プロンプトは出てきません。 仮に終了するかの確認を beforeunload イベントで処理するなら次のようになるでしょう。

[beforeunload_2.ts]

globalThis.addEventListener("beforeunload", (e) => {
  if (confirm("本当に終了する?") e.preventDefault();
})

mdn の beforeunload イベント についての記述を見ると、 beforeunload イベントの実行中には、window.confirm() などが無視されることがあるとの記述があります。 Deno では無視されて処理されました。

リリースノートのサンプルを参考に動作確認したソースコードが次の通りです。

[beforeunload_3.ts]

let count = 0;

globalThis.addEventListener("beforeunload", (e) => {
  console.log("beforeunload");
  if (count < 3) {
    e.preventDefault();
    setTimeout(() => {
      console.log(count);
    }, 1000);
  }

  count++;
});

globalThis.addEventListener("unload", (e) => {
  console.log("unload");
});

setTimeout(() => {
  console.log("count up");
  count++;
  console.log(count);
}, 1000);

実行すると次のように表示されます。

$ deno run beforeunload.ts
count up # <== この出力までに1秒経過
1
beforeunload
2 # <== 前の出力から1秒経過
beforeunload
3 # <== 前の出力から1秒経過
beforeunload
unload

beforeunload イベントは、待っている処理がすべて終了すると呼び出されます。 なので、サンプルのように beforeunload イベント の中で、e.preventDefault()を呼び出すことでイベントキャンセルし、新たに setTimeout() を用いて待つ処理を走らせると、一定時間後に beforeunload イベント を再度呼び出すことができます。

ここで気になるのは、Deno 1.17 登場した、Deno.unrefTimer を使用した場合の動作です。 Deno.unrefTimer は、setTimeout()や setInterval()をイベントループの終了をブロックさせないようにできます。

以下のように修正して確認します。

[beforeunload_3.ts]

let count = 0;

globalThis.addEventListener("beforeunload", (e) => {
  console.log("beforeunload");
  if (count < 3) {
    e.preventDefault();
    const timerId2 = setTimeout(() => {
      console.log(count);
    }, 1000);
    Deno.unrefTimer(timerId2); // <== 変更
  }

  count++;
});

globalThis.addEventListener("unload", (e) => {
  console.log("unload");
});

const timerId1 = setTimeout(() => {
  console.log("count up");
  count++;
  console.log(count);
}, 1000);

Deno.unrefTimer(timerId1); // <== 変更

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

$ deno run --unstable beforeunload.ts
beforeunload # <==一切待たずに出力
beforeunload
beforeunload
beforeunload
unload

上記の出力が、得られます。 setTimeout に渡した count の表示がされておらず、待つことなくイベントの呼び出しだけがされていることがわかります。 ということは、Deno ではbeforeunload イベント は、「イベントループの終了」のイベントになっていることがわかりますね。

import.meta.resolve() API の追加

import.meta に resolve() API が追加されます。 この API により、現在のモジュールから相対的な指定によるモジュールの解決がより簡潔に記述できます。

[import.ts]

// 以前の記述方法
const worker = new Worker(new URL("./worker.ts", import.meta.url).href);

// import.meta.resolve() を使用した記述方法
const worker = new Worker(import.meta.resolve("./worker.ts"));

また、import-map にも対応しています。 次の import_map.json を用意します。

{
  "imports": {
    "fresh": "https://deno.land/x/fresh@1.0.1/dev.ts"
  }
}

実行するスクリプトは次の通りです。

[import_test.ts]

console.log(import.meta.resolve("fresh"));

以下のように実行します。

$ deno run --import-map=import_map.json import_test.ts
https://deno.land/x/fresh@1.0.1/dev.ts

deno test が改善

構成ファイル deno.json(c) テスト対象パスの設定

これまでは、deno test の実行対象に特定のパスを含めるにはコマンドライン引数が必要でした。 このリリースから構成ファイル deno.json(c) で設定ができるようになります。

動作確認してみます。

以下のディレクトリ構成を行いました。

$ tree
.
|-- app_test.ts
`-- src
    |-- test1.ts
    |-- test2.ts
    `-- test3.ts

この状態で deno test を行うと次のようになります。

$ deno test
running 1 test from ./app_test.ts
app_test #1 ... ok (6ms)

ok | 1 passed | 0 failed (28ms)

テスト対象のモジュールとして app_test.ts は検知されていますが、src 以下のファイルはテスト対象とみなされていません。 ここで、今回のリリースノートに則り以下の内容で deno.json(c) を作成します。

{
  "test": {
    "files": {
      "include": ["src/test1.ts", "src/test2.ts", "src/test3.ts"]
    }
  }
}

この状況で deno test を行うと次のように処理されます。

$ deno test
running 1 test from ./src/test1.ts
test1 #1 ... ok (5ms)
running 1 test from ./src/test2.ts
test2 #1 ... ok (4ms)
running 1 test from ./src/test3.ts
test3 #1 ... ok (6ms)

ok | 3 passed | 0 failed (56ms)

include で含めたものだけがテストされており、標準でテスト対象とみなされる app_test.ts は無視されているのが確認できます。

--paralel フラグの追加

deno test の並列実行フラグとして --parallel が追加されました。 以前より --jobs フラグがありましたが、このフラグは --jobs 4 のように = を必要としないものになっていました。 このため、値を指定しない場合は cli 実行するときに最後にこのフラグを記述必要があるというもので、これは設計の見落としと考えられたそうです。

先の構成ファイルの確認で 複数のテストを用意しているのでこちらで、確認します。 (並列実行をしている時のコンソール出力ですので、たまに崩れが見られますが今回は一番きれいに結果が見られるものを持ってきました。)

# 並列実行無
$ deno test
running 1 test from ./src/test1.ts
test1 #1 ... ok (5ms)
running 1 test from ./src/test2.ts
test2 #1 ... ok (5ms)
running 1 test from ./src/test3.ts
test3 #1 ... ok (6ms)

ok | 3 passed | 0 failed (54ms)

# --jobs は、deprecated になりました。
$ deno test --jobs
Warning: --jobs flag is deprecated. Use the --parallel flag with possibly the 'DENO_JOBS' environment variable.
running 1 test from ./src/test1.ts
running 1 test from ./src/test3.ts
running 1 test from ./src/test2.ts
test3 #1 ... ok (5ms)
test1 #1 ... ok (6ms)
test2 #1 ... ok (5ms)

ok | 3 passed | 0 failed (34ms)

# 今回追加の --parallel で並列実行
$ deno test --parallel
running 1 test from ./src/test1.ts
running 1 test from ./src/test2.ts
running 1 test from ./src/test3.ts
test2 #1 ... ok (5ms)
test3 #1 ... ok (7ms)
test1 #1 ... ok (8ms)

ok | 3 passed | 0 failed (37ms)

# 並列実行数は、環境変数 DENO_JOBS で設定します。
# 並列実行数を 2 に制限
$ DENO_JOBS=2 deno test --parallel
running 1 test from ./src/test2.ts
running 1 test from ./src/test1.ts
test1 #1 ... ok (6ms)
test2 #1 ... ok (6ms)
running 1 test from ./src/test3.ts
test3 #1 ... ok (6ms)

ok | 3 passed | 0 failed (43ms)

並列実行されると、テスト結果よりも実行している running ~~~ の表示が先に表示されることが確認できます。 また、並列実行数を 2 に制限した時には、2 つテストが終了してから次の実行が開始されているのがわかります。

サブプロセス API の更新

Deno.spawnChild の型

Deno 1.21 から新しいサブプロセスAPIが追加になりました。 このAPIに修正が入り、stdioストリームの型が変更されました。 次のサンプルを用意し、Deno 1.23 での挙動と比較します。

[spawn_child.ts]

const child = Deno.spawnChild("echo", {
  args: ["hello"],
  stdout: "piped",
  stderr: "null",
});
const readableStdout = child.stdout.pipeThrough(new TextDecoderStream());
const readableStderr = child.stderr.pipeThrough(new TextDecoderStream());

上記のサンプルをDeno 1.23で動かすと次のようになります。

$ deno -V
deno 1.23.0

$ deno check --unstable spawn_child.ts
Check file:///usr/src/app/spawn_child.ts
error: TS2531 [ERROR]: Object is possibly 'null'.
const readableStderr = child.stderr.pipeThrough(new TextDecoderStream());
                       ~~~~~~~~~~~~
    at file:///usr/src/app/spawn_child.ts:15:24

$ deno run --unstable --allow-run spawn_child.ts
error: Uncaught TypeError: Cannot read properties of null (reading 'pipeThrough')
const readableStderr = child.stderr.pipeThrough(new TextDecoderStream());
                                    ^
    at file:///usr/src/app/spawn_child.ts:15:37

続いてDeno 1.24で動かすと次のようになります。

$ deno -V
deno 1.24.0

$ deno check --unstable spawn_child.ts

$ deno run --unstable --allow-run spawn_child.ts
error: Uncaught TypeError: stderr is not piped
const readableStderr = child.stderr.pipeThrough(new TextDecoderStream());
                             ^
    at Child.get stderr (deno:runtime/js/40_spawn.js:107:15)
    at file:///usr/src/app/spawn_child.ts:15:30

Deno 1.24 では、.stderr が型レベルでは、存在している形に変わっているのがわかります。

Deno.spawn の結果情報のオブジェクトの形が変更

Deno.spawn で返されるオブジェクトの内容が変更されています。

[spawn.ts(deno1.23対応)]

const { status, stdout } = await Deno.spawn("echo", {
  args: ["hello"],
});

console.log(status.success);
console.log(status.code);
console.log(status.signal);
console.log(stdout);

deno1.23では、statusの中に各結果情報が入る形態になっていました。

[spawn.ts(deno1.24対応)]

const { success, code, signal, stdout } = await Deno.spawn("echo", {
  args: ["hello"],
});
console.log(success);
console.log(code);
console.log(signal);
console.log(stdout);

Deno 1.24 から、各コードを直接取得できるように変わりました。

Child.ref() Child.unref() APIが追加

「メインプロセスが、サブプロセスの終了を待つか」を制御できるAPI、Child.ref() Child.unref()が追加されました。

Deno.refTimer()、Deno.unrefTimer() にあたるようなAPIです。

これらの動作確認してみます。

[ref.ts]

globalThis.addEventListener("unload", (e) => {
  console.log(`END: ${performance.now()}`);
});

const child = Deno.spawnChild(Deno.execPath(), {
  args: ["eval", "setTimeout(()=> console.log('timeout'),2000)"],
  stdout: "piped",
});

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

$ deno run --unstable --allow-run --allow-read unref.ts
END: 2032 

数字部分が終了までの経過時間(ミリ秒)ですので、約2秒かけサブプロセスを待っていることを確認できます。

次は、child.unref()の記述を追加します。

globalThis.addEventListener("unload", (e) => {
  console.log(`END: ${performance.now()}`);
});

const child = Deno.spawnChild(Deno.execPath(), {
  args: ["eval", "setTimeout(()=> console.log('timeout'),2000)"],
  stdout: "piped",
});

child.unref();

実行します。

$ deno run --unstable --allow-run --allow-read unref.ts
END: 8

8ミリ秒で終了し、サブプロセスの終了を待っていないことがわかります。

その他

  • FFI API の改善
    • JavaScript関数をコールバック関数としてFFIへ渡すことができるように
    • FFI 呼び出しパフォーマンスの向上
    • Deno.UnsafePointer が削除
  • LSP が改善
  • 標準モジュールの更新
    • semver モジュールがstdに追加
    • std/flags が改善
    • std/dotenv が、.envファイルの記述に変数が使用できるようになった

まとめ

Deno 1.24 では、 サブプロセスの管理や、beforeunload イベントの検知といった終了時の考慮に関わるような開発者が、アプリケーションの終了にあたって使いたくなるような機能の拡張や、パフォーマンスの向上が目立っていたように思います。

昨今のDenoのニュースとしては、 サードパーティーの管理だったFresh が deno.landの管理下に入りました。 これを使用したフルスタックCRUD アプリケーションを作るサンプルなどが公開されています。

dev.to

Freshは使ってみると非常に体験がいいと感じています。興味がありましたら、ぜひ使ってみてください。

引き続き次のリリースも追いかけていきます。

P.S.

採用情報
虎の穴ラボではエンジニアを含めさまざまな職種の採用をおこなっております。
興味のある方はぜひ下記のリンク先の内容をご覧ください。
■募集職種
yumenosora.co.jp

LINEスタンプ
エンジニア専用のメイドちゃんスタンプが完成しました!
「あの場面」で思わず使いたくなるようなスタンプから、日常で役立つスタンプを合計40個用意しました。
エンジニアの皆さん、エンジニアでない方もぜひスタンプを確認してみてください。
store.line.me