虎の穴開発室ブログ

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

MENU

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

皆さんこんにちは。自宅のダイナゼノンが、カイゼルグリッドナイトになりました。おっくんです。

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

実行環境

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

Deno 1.13

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

ネイティブ HTTP サーバー API が安定化しました。

Deno 1.9 より登場した ネイティブ HTTP サーバー API が安定化し、利用に当たって --unstable の付与が不要になりました。

Rust の高速な HTTP サーバー hyper Webサーバーを JavaScript API として利用できます。 この数週間、https://deno.land への数百万件に及ぶリクエストを新しい API で対応したことからも、十分安定した API だと確信しているそうです。

https://deno.land/に記載されていたサーバーのサンプルコードも、ネイティブ HTTP サーバー API を利用した、以下のコードに切り替わっていました。

const listener = Deno.listen({ port: 8000 });
console.log("http://localhost:8000/");
for await (const conn of listener) {
  (async () => {
    const requests = Deno.serveHttp(conn);
    for await (const { respondWith } of requests) {
      respondWith(new Response("Hello world"));
    }
  })();
}

これまで見られていた以下のような std/http を使用した サーバーは、ネイティブ HTTP サーバー API への切り替えが推奨されています。

// 今後使えなくなる std/http を使った実装
import { serve } from "https://deno.land/std@0.91.0/http/server.ts";

const s = serve({ port: 8000 });
console.log("http://localhost:8000/");
for await (const req of s) {
  req.respond({ body: "Hello World\n" });
}

std/http は今後いくつかのバージョンでは、利用を継続できますが、もうすぐ削除になるそうです。 削除にあたり、次のようにサポートされます。

  • 移行するための情報は、近日マニュアルに記載される予定である
  • oakの最新版を使っていれば、Deno 1.13 環境で動かすとネイティブ API にシームレスに移行できる

denoの第3クォーターのロードマップには、std/http のオーバーホールが予定されており、あまり遠くない未来に実施されるのかもしれません。

構造化クローンアルゴリズム を使用する関数 self.structuredClone() が追加されました 

新しく、self.structuredClone() が、追加されます。 この、self.structuredClone() は、これまで Web Worker とのメッセージのやり取りなどで使用されていた機能になります。

self.structuredClone() を使うと、値(オブジェクト)のディープクローンができます。 構造化クローンアルゴリズムについては、MDN に、The structured clone algorithmという記事があるので、こちらが参考になります。

self.structuredClone() を使ってみます。

[clone.ts]

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

// self.structuredClone() 動作確認
const foo = { bar: "baz" },
  foo = { bar: "baz" };
foo.foo = foo;

const clone = structuredClone(foo);
// self.structuredClone は self 名前空間に属した関数なので self は省略できる

assert(foo1.foo === foo);
assert(clone !== foo);
assert(clone.bar === "baz");
assert(clone.foo1 === clone); // <= 循環している構造としてコピーできている

deno run --no-check clone.ts で実行します。

cloneDeep でも同様の self.structuredClone() と同様の動作を確認できました。 外部のライブラリ依存なくディープクローンができるのは非常に助かります。

この self.structuredClone() は、 Chrome や firefox にも structuredClone() が追加される可能性があるそうです。

環境変数で、TLS に使用する認証局をシステム CA ストアに変更できるようになった

Deno 1.13 から、Deno で使用できる環境変数に、DENO_TLS_CA_STORE が追加になります。 DENO_TLS_CA_STORE を使うことで、Deno が TLS に対して、信頼する認証局を変更することができます。 設定可能な値は、mozillasystemで、デフォルト値は mozilla です。

DENO_TLS_CA_STORE=system deno run hogehoge.ts のように使用することで、各種 OS のシステムルート CA ストア(windows なら、システム証明書ストア、macOS なら、キーチェイン)が使えるようになります。 この環境変数を使用すると、Deno 組み込みの CA ストアは無効になります。

REPL が改善されました

Deno の REPL に 2 つの大きな改善が入ります。

export を無視します。

これまで export を REPL で使うと、次のように シンタックスエラーになっていました。

$ deno
Deno 1.12.0
exit using ctrl+d or close()
> export const val = 1;
Uncaught SyntaxError: Unexpected token 'export'

Deno 1.13 より、REPL での export の使用は、無視されるのでエラーにならなくなります。 export が書かれているモジュールをコピーアンドペーストして確認するときなど、手間が減ります。

--eval フラグが追加されました

--eval を使うことで、REPL 起動時に、与えたスクリプトを実行してから、対話的操作に移ります。 注意する点は、deno --eval ~~ でなく deno repl --eval ~~ のように repl が必要です。

次のように使えます。

# deno repl --eval 'let val = 1; val = 2;'
Deno 1.13.0
exit using ctrl+d or close()
> val
2
>

リリースノートでは、--eval フラグで外部モジュールのインポートを紹介しています。 REPL で確認をしていく際に、必須のモジュールなどが固まっているようであれば、シェルスクリプトとして次のように書いてしまうのがいいと思います。

#!/bin/bash
# repl.sh として保存

deno repl --eval 'let val = 1; val = 2;'

これで、./repl.sh とするだけで、val が定義された状態で起動できます。 REPL の起動都度、外部モジュールのインポートを毎回履歴から実行するという手間がなくなるので、とてもありがたい更新でした。

navigator.hardwareConcurrency API がサポートされるようになります

読み取り専用の navigator.hardwareConcurrency プロパティは、ユーザーエージェントが使用可能な、論理プロセッサの個数を返します。 詳細は、MDN にあるので、こちらを参照されるのがいいと思います。

MDN - Navigator.hardwareConcurrency

> console.log(navigator.hardwareConcurrency);
6 // Windows 上の Docker コンテナの場合 macOS では 4 でした

この API により得られる値を元に、Web worker の立ち上げ個数を制御するといった用途に使うそうです。

v8 バージョンが 9.3 になります。

Deno 1.13 では、Deno 1.12 に引き続き v8 が更新されます。 更新されたことで、新しい JavaScript の機能が使えるようになります。

リリースノートでのサンプルコードから紹介します。

Error cause

Error オブジェクトが cause プロパティを持つようになります。

const parentError = new Error("parent");
const error = new Error("parent", { cause: parentError });
console.log(error.cause === parentError);
// → true

こちら、Chrome などブラウザでは、未対応でした。 MDN - Error

Object.hasOwn

オブジェクトが指定のキーのプロパティを持つか問い合わせる Object.prototype.hasOwnProperty.call のエイリアス Object.hasOwn が導入されます。

> const obj = {key:1};
undefined
> Object.prototype.hasOwnProperty.call(obj,"key")
true
> Object.hasOwn(obj,"key") // <=
true

こちらも、Chrome などブラウザでは、未対応でした。 MDN - Object.hasOwn()

ここで、v8 のバージョン最新版を確認してみると現在 9.4 系となっているので、Deno も併せてアップデートがそのうちあるのではと思います。 現在のプルリクには該当するようなものはありませんでした。

https://github.com/v8/v8

https://github.com/denoland/deno/pulls?page=2&q=is%3Apr+is%3Aopen

Deno info

deno info hoge とすると、hoge モジュールの依存情報が参照できるサブコマンドがあります。

Deno 1.13 から、/// <reference lib="..."> や、// @deno-types ディレクティブ、X-TypeScript-Types ヘッダーにより指定される型宣言が、ツリービューに表示されるようになるそうです。

/// <reference lib="..."> や、// @deno-types ディレクティブ でのツリービューの表示内容を Deno 1.13.0 と Deno 1.12.0 の間で確認してみたのですが、ちょっと違いが掴めなかったので、これは別途改めて確認したいと思います。

Deno.writeFile が、AbortSignal をサポート

Deno 1.12 では、Deno.readFile ファイルの AbortSignal 対応がリリースされていました。 引き続き、Deno 1.13 では、Deno.writeFile が、AbortSignal をサポートするようになります。

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

const aborter = new AbortController();
const data = new UInt8Array(32 * 1024 * 1024);

Deno.writeFile("./super_large_file.txt", { signal: aborter.signal })
  .then((data) => console.log("File write:", data.length))
  .catch((err) => console.error("File write failed:", err));

setTimeout(() => aborter.abort(), 1000);

マークダウンドキュメント内の、型チェックができるようになりました。

Deno 1.10 にて、ソースコードのコメントに書かれたサンプルコードの型チェックがリリースされていました。

Deno1.13 では、マークダウンのコードブロックに書かれた JavaScript jsx TypeScript tsx  をチェックできるようになります。

やってみます。

読み込み対象のモジュールを作成。

[mod.ts]

// mod.ts
export const func1 = (a: number, b: number): number => {
  return a + b;
};

使用例を書いたマークダウンを次のように作成します。

[doc.md]

引数の型を誤った使い方

```ts
import { func1 } from "./mod.ts";

console.log(func1("1", "2"));
```
$ deno test --doc doc.md

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

error: An unsupported media type was attempted to be imported as a module.
  Specifier: file:///usr/src/app/doc.md
  MediaType: Unknown

上記の通り、サポートしていないメディアタイプだというエラーになります。 記述の仕方が問題となっていないことから、現在利用できない状態のようです。

環境変数を引き継がずに、サブプロセスを起動できるようになります。

新たな --unstable フラグを要求する機能として、Deno.run のオプション Deno.RunOptions インターフェースに、clearEnv が追加されました。

Deno 実行時の環境変数が、Deno.run で起動したサブプロセスに引き継がれなくなります。 試してみます。

Deno.run で起動されるシェルクリプトは以下の通りです。

[show_env.sh]

#!/usr/bin/sh

# 環境変数を表示するだけ
echo sh:$AAA

このシェルを呼び出す スクリプト env.ts を次のようにします。 まず、前提として clearEnv オプションを使用しないものを確認します。

[env.ts]

// Denoとして読み込んだ環境変数を表示
console.log(`Deno:${Deno.env.get("AAA")}`);

const result = Deno.run({
  cwd: "./",
  cmd: ["sh", "show_env.sh"],
  stdout: "piped",
});

await result.status();

// Deno.run の実行結果を表示
console.log(new TextDecoder().decode(await result.output()));

実行してみます。

$ AAA=aaaaa deno run --allow-run --allow-env env.ts
Check file:///usr/src/app/env.ts
Deno:aaaaa
sh:aaaaa

実行時に渡した AAA=aaaaa が Deno.run で機能したサブプロセスにも引き継がれています。 この引継ぎをされないようにできるのが、clearEnv オプションです。 env_clean.ts を改修します。

[env_clean.ts]

console.log(`Deno:${Deno.env.get("AAA")}`);

const sub = Deno.run({
  cwd: "./",
  cmd: ["sh", "show_env.sh"],
  stdout: "piped",
  clearEnv: true, // <= clearEnv: true を記述
});

console.log(new TextDecoder().decode(await sub.output()));

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

$ AAA=aaaaa deno run --unstable --allow-run --allow-env env_clean.ts
Check file:///usr/src/app/env.ts
Deno:aaaaa
sh:

サブプロセスでの環境変数が参照できなくなりました。

ここで、気になるところとしては、clearEnv: true を設定しなかったとき、他にも引き継がれている環境変数があるのか?です。

show_env.sh を修正してシェルが読み込んでいる環境変数をすべて表示してみます。

[show_env.sh(修正)]

#!/usr/bin/sh

# 環境変数をすべて表示
env

実行します。

# clearEnv: true 未設定
$ AAA=aaaaa deno run --allow-run --allow-env env.ts
Check file:///usr/src/app/env.ts
LANG=en_US.UTF-8
HOSTNAME=3117b748d4e9
DENO_VERSION=1.13.0
DENO_DIR=/deno-dir/
DENO_INSTALL_ROOT=/usr/local
PWD=/usr/src/app
HOME=/root
AAA=aaaaa
TERM=xterm
SHLVL=2
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LESSOPEN=||/usr/bin/lesspipe.sh %s
_=/usr/bin/env

# clearEnv: true 設定済
$ AAA=aaaaa deno run --unstable --allow-run --allow-env env_clean.ts
Check file:///usr/src/app/env_clean.ts
Deno:aaaaa
sh:
PWD=/usr/src/app
SHLVL=1
_=/usr/bin/env

# OSに定義されている環境変数
$ env
LANG=en_US.UTF-8
HOSTNAME=3117b748d4e9
DENO_VERSION=1.13.0
DENO_DIR=/deno-dir/
DENO_INSTALL_ROOT=/usr/local
PWD=/usr/src/app
HOME=/root
TERM=xterm
SHLVL=1
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LESSOPEN=||/usr/bin/lesspipe.sh %s
_=/usr/bin/env

clearEnv: true を設定した時には、多数の環境変数がサブプロセスに引き継がれていることがわかります。 Deno と直接関係しないような定義済の環境変数も引継ぎされないものもあり、用途によってはむしろ邪魔になってしまうケースがあるかもしれません。

Permissions API に URL を設定できるようになりました。

Deno.permissions API で対象を指定する時、これまでは read write run に、文字列でパスを指定してきました。 こちらが URL オブジェクトも受け付けるようになります。

memo というファイルが現在のディレクトリにあるものとして、ファイルの読み込みを試してみます。

[permissions_url.ts]

// URLで読み込み先ファイルを準備
const url = new URL("memo", import.meta.url);

const permissionsQuery = async () => {
  return await Deno.permissions.query({
    name: "read",
    path: url,
  });
};

// 現在のパーミッション アクセス権無
console.log(await permissionsQuery());

// 対話的なアクセス権取得処理
await Deno.permissions.request({
  name: "read",
  path: url,
});

// 現在のパーミッション アクセス権有
console.log(await permissionsQuery());

console.log(Deno.readTextFileSync(url));

// アクセス権取り消し
await Deno.permissions.revoke({
  name: "read",
  path: url,
});

// 現在のパーミッション アクセス権無
console.log(await permissionsQuery());

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

$ deno run permissions.ts
Check file:///usr/src/app/permissions.ts
PermissionStatus { state: "prompt", onchange: null }
⚠️  ️Deno requests read access to "/usr/src/app/memo". Allow? [y/n (y = yes allow, n = no deny)] y
PermissionStatus { state: "granted", onchange: null }
hogehoge
PermissionStatus { state: "prompt", onchange: null }

リリースノートに示されているDeno.permissions.query だけでなく、Deno.permissions.requestDeno.permissions.revoke でも URL オブジェクトで受け付けることができています。

サンプルコードで示しているとおり、Deno.readTextFileSync が URL オブジェクトで受け付けられることからも、部分的に文字列で書く必要が無くなり、ソースコードの見通しがよくなると思います。

ネイティブプラグインが削除され、FFI が追加されます

deno の第 3 クォーターのロードマップにて、ネイティブプラグインの削除が予告されていました。 代替として、新たに用意された FFI API に置き換えられます。

現時点では、実験的機能として実行時には、--unstable の付与が必要です。 動作確認に使用している docker イメージ denoland/deno:centos-1.13.0 で実行できるまでの順を追って記載します。

gcc のインストール。

yum install -y gcc

ソースコードの作成

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

共有ライブラリとして、コンパイルします。

cc -c -o add_numbers.o add_numbers.c
cc -shared -o add_numbers.so add_numbers.o 

リリースノートに記載された -Wl オプションは、入れていません。今回の共有ライブラリの内容では、リンカオプションの設定は不要だったためです。

作成した add_numbers.so を読み込む、ffi.js を作成します。

[ffi.js]

let libSuffix = "so";

if (Deno.build.os == "windows") {
  libSuffix = "dll";
}

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

実行するために、--allow-ffi を付与することで ffi が使用できます。

$ deno run --allow-ffi --unstable ffi.js
579

libffi-devel などの環境構築を要するライブラリを導入せずとも使えるのが、とてもいいですね。 Deno.dlopen の第 2 引数に 共有ライブラリに記述されている関数の構造を記述する必要があります。 Rust の wasm-bindgenのような、共有ライブラリに合わせて JavaScript のコードや Typescript 型定義を生成をしてくれるツールが欲しくなりますね。

WebSocketStreamAPI が追加

Deno 1.12 リリースノートでは、Deno のネイティブウェブサーバーが WebSocket をサポートしました。 その際に、既存の WebSocket オブジェクトは、WebSocketStream が安定して使えるようになれば、切り替える予定あると紹介されていました。

この WebSocketStream が、実験的な API として Deno に導入されます。 リリースノートに示されているコードを紹介します。

// 新しい WebSocketStream API
const wss = new WebSocketStream("wss://example.com");
const { writable, readable } = await wss.connection;

const writer = writable.getWriter();
await writer.write("Hello server!");

for await (const message of readable) {
  console.log("new message:", message);
}
// WebSocket API
const ws = await new Promise((resolve, reject) => {
  const ws = new WebSocket("wss://example.com");
  ws.onerror = (e) => {
    reject(new Error(e.message));
  };
  ws.onopen = (e) => {
    resolve(ws);
  };
});

ws.onerror = (e) => {
  console.error("connection closed:", e.code, e.reason);
};

ws.send("Hello server!");

ws.onmessage = (e) => {
  console.log("new message:", e.data);
};

WebSocketStream は、async/await の API に変わり、かなり見通しがよくなったように感じます。 実験的な API なので、実行には、--unstable が必要です。

その他

  • TLS 検証を無効にするフラグ --unsafely-ignore-certificate-errors を追加
    追加はされたものの、通信の機密性が損なわれるので、基本的に使用が推奨されないものになります。
  • Deno 言語サーバーと、VSCode 向け拡張機能のアップデート
  • WebCrypto API の更新 HMAC 形式のキーのインポート、エクスポートに新たに対応し、既存のcrypto.subtle.verify()関数も HMAC 形式に対応しました。
    • crypto.subtle.importKey():追加
    • crypto.subtle.exportKey():追加
    • crypto.subtle.verify():HMAC 形式への対応追加

Deno 1.13 のリリースノートを追ってきました。 FFI API がエポックメイキングなリリースと言えるのではないかと思います。 特に特別な環境構築なく、作成した共有ライブラリから関数の呼び出しができたのは非常に快適でした。

次の リリースは 9 月 14 日予定です。またそのころ Deno 1.14 でお会いしましょう。

P.S.

採用情報

■募集職種
yumenosora.co.jp

カジュアル面談も随時開催中です

■お申し込みはこちら!
yumenosora.connpass.com

■ToraLab.fmスタートしました!

メンバーによるPodcastを配信中!
是非スキマ時間に聞いて頂けると嬉しいです。
anchor.fm

■Twitterもフォローしてくださいね!

ツイッターでも随時情報発信をしています
twitter.com