虎の穴開発室ブログ

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

MENU

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

皆さんこんにちは。最近、玄米生活を始めました、おっくんです。

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

実行環境

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

Deno 1.16

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

deno.com

fetch API が、ファイル URL をサポートするようになりました

これまで Deno では、 fetch を web 標準の API として、ネットワークリソースにアクセスするためのみに使用してきました。 また、ローカルファイルアクセスには、Deno.readfile()などのメソッドが使用されてきました。 このリリースでは、file プロトコル を使用して fetch でローカルファイルにもアクセスできるようになります。 既存の API Deno.readfileでのファイルアクセスと比較しながら紹介します。

[これまでのやり方]

// Deno.readfile を使うパターン
const loadBytes = await Deno.readFile("./textfile.md");
console.log(loadBytes);
// => Uint8Array(18) [
//       72, 101, 108, 108, 111,
//       32, 227, 129, 168, 227,
//      130, 137, 227, 131, 169,
//      227, 131, 156
//    ]

const text = new TextDecoder().decode(loadBytes);
console.log(text);
// => Hello とらラボ

[Deno 1.16からできるやり方]

// fetch API + file URL を使うパターン
const url = new URL("./textfile.md", import.meta.url);
console.log(url);
// => URL {
//      href: "file:///usr/src/app/textfile.md",
//      origin: "null",
//      protocol: "file:",
//      username: "",
//      password: "",
//      host: "",
//      hostname: "",
//      port: "",
//      pathname: "/usr/src/app/textfile.md",
//      hash: "",
//      search: ""
//    }

const result = await fetch(url);
console.log(result);
// => Response {
//      body: ReadableStream { locked: false },
//      bodyUsed: false,
//      headers: Headers {},
//      ok: true,
//      redirected: false,
//      status: 200,
//      statusText: "OK",
//      url: "file:///usr/src/app/textfile.md"
//    }

const text = await result.text();
console.log(text);
// => Hello とらラボ

実行にあたっては、ローカルファイルへのアクセスが必要なので、--allow-read の読み取り権限付与が必要です。 先に示した例の場合、次のように実行できます。

deno run --allow-read app.ts
# もしくは、特定のファイルに対してのみ読み取り可能とする
deno run --allow-read=//usr/src/app/textfile app.ts
# file:/ は不要

fetch API でのローカルファイルアクセスは、チャンクとして読み込みされます。 このことで、大きなファイルを返却する HTTP サーバーを作成するときなど、ファイル全体を一度すべてメモリに持つ必要がありません。

Deno での、fetch API でファイルアクセスする仕様は、Firefox の仕様をベースにしているとのことです。

その他仕様として、

  • 「ファイルが無い」場合、次のようにエラーになります。
    Uncaught (in promise) TypeError: NetworkError when attempting to fetch resource.
  • 「指定されたパスがディレクトリだった」場合、次のようにエラーになります。 Uncaught (in promise) TypeError: NetworkError when attempting to fetch resource.  ブラウザで、file://などとアクセスするとディレクトリが参照できるので、この点はFirefox の仕様との差異になります。(Windows環境の場合C:/などでローカルドライブ参照できます。)  
  • レスポンスヘッダーには、content-length を含みません。レスポンスボディがストリームであるため、最終的な長さがわからないことが原因です。
  • レスポンスヘッダーには、content-type ヘッダーは設定されていません。
    ファイル拡張子から、コンテンツタイプを知りたい場合、https://deno.land/xmedia_typesを使用するように勧められています。

github.com

最後に、media_typesを使用したコンテンツタイプの取得を行ってみます。

import { lookup } from "https://deno.land/x/media_types@v2.10.2/mod.ts";

const url = new URL("./textfile.md", import.meta.url);
console.log(lookup(url.pathname));
// => text/markdown

新しい JSX トランスフォーム のサポート

JSX トランスフォームについては、丁寧な解説が React 公式のブログにあります。 かいつまんで書くと、「JSX を JavaScript のコードに変換する機能」のことです。 この機能が React 17 で、更新され任意のライブラリを参照できるようになりました。 この新しい JSX トランスフォームを、Deno 1.16 からサポートするようになります。

Deno の公式マニュアルの Deno での JSX の構成に書かれている記述を確認すると、デフォルトでは以下の設定が使用されていることがわかります。

{
  "compilerOptions": {
    "jsx": "react",
    "jsxFactory": "React.createElement",
    "jsxFragmentFactory": "React.Fragment"
  }
}

この JSX トランスフォーム設定を開発者が切り替えることができます。 JSX トランスフォームをpreactに切り換える2つの方法を紹介します。

やり方1 @jsxImportSource プラグマで記述

/** @jsxImportSource https://esm.sh/preact */

export Welcome({ name }) {
  return (
    <div>
      <h1>Welcome {name}</h1>
    </div>
  );
}

やり方2 --config を使って設定する

[config.json]

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "https://esm.sh/preact"
  }
}

deno run --config config.json hogehoge.ts のように実行します。

実行すると多数の型定義に関するエラーが発生するかと思います。 Deno での JSX の構成に書かれた制限事項として、 「インポートまたは、エクスポートがない JSX モジュールが型チェック時に正しく動作しない」ことが書かれています。 この動作の不具合は、TypeScript のバグとして報告されています。

github.com 内容としてはJavaScriptにトランスパイルされた結果は、_jsx があることを期待しているが、実際にはインポートされていないのが原因です。

対応として、次の二つの方法が示されています。

  • ファイルに export{} を追加する
  • --no-check フラグを使用する

先に示した使用方法の関連として、インポートマップの使用方法が記載されているのでこちらも紹介します。

以下のように config.json、importmap.json を用意します。

//[config.json]
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
  }
}
//[importmap.json]
{
  "imports": {
    "preact/jsx-runtime": "https://cdn.skypack.dev/preact/jsx-runtime?dts",
    "preact/jsx-dev-runtime": "https://cdn.skypack.dev/preact/jsx-dev-runtime?dts"
  }
}

次のように実行することができます。 deno run --config config.json --import-map importmap.json hogehoge.jsx

この時、importmap に記載されたソースコードの取得先には?dtsをつけて型情報を含むことで、--no-checkの付与をせずに済んでいます。

では、config.json"jsxImportSource": "https://cdn.skypack.dev/preact/jsx-runtime?dts"と記述すると、問題は解決することができそうに感じますが、https://cdn.skypack.dev/preact/jsx-runtime?dts/jsx-runtime'というアドレスで取得を試みるので、この方法では解決できません。

不安定な新たな signal listener API が追加されました

OS からのシグナルを取得する、API が追加されました。不安定なAPI として、--unstable を実行時に要求します。 この API は、既存の Deno.signals API と置き換えられます。 リリースノートでは次のように紹介されています。

const listener = () => {
  console.log("SIGTERM!");
};

// Starts listening to SIGTERM
Deno.addSignalListener("SIGTERM", listener);

// Stops listening to SIGTERM
Deno.removeSignalListener("SIGTERM", listener);

Error.cause が、コンソールに表示されるようになりました

Deno 1.13 で導入された、Error.cause プロパティにエラーの原因を載せて、デバッグに活かすことができました。 が、コンソールには表示されていませんでした。 Deno 1.16 からは、エラーがスローされるとき、Error.cause がコンソールに表示されるようになっています。

Deno 1.15.3 での動作と比較してみましょう。

// Deno 1.15.3
> throw new Error("main error", { cause: new TypeError("caused by this") })
Uncaught Error: main error
    at <anonymous>:2:7

// Deno 1.16.1
> throw new Error("main error", { cause: new TypeError("caused by this") })
Uncaught Error: main error
    at <anonymous>:2:7
Caused by TypeError: caused by this
    at <anonymous>:3:12

Deno 1.16.1 では、Error.cause に設定された caused by this という文言が、コンソールに表示されています。

明示的に TLS 接続のハンドウェイクを実行できるようになりました

暗号化された接続をする前に、TLS 接続を確立する必要があります。 この TLS 接続の確立には、TLS ハンドシェイクの実行が必要です。 ほとんどのユーザーは、ハンドシェイクの詳細を気にする必要はなく、読み書きの接続をしたときに自動的に行われます。

Deno.TlsConn に、ハンドシェイクを明示的に行うための handshake() メソッドが追加されています。 このメソッドは、 Promise を返します。 もし、ハンドシェイクがすでに成立している時は、Promise はすぐに resolve を返します。 ハンドシェイクの実行中と完了していないときは、完了した時点で Promise がresolve を返します。

deno doc を参照すると、インターフェイスについてのドキュメントを確認できます。

Deno.startTls が安定化しました

Deno は、Deno.connectTls Deno.startTls の二つの TLS 接続方法を提供しています。

  • Deno.connectTls:TCP 接続が開いたなら、すぐに TLS 接続を開始します。
  • Deno.startTls:プレーンテキストの TCP 接続が接続されていた上で、TLS 接続が必要な時に切り替えます。

Deno 1.16 では、Deno startTls が安定化し、安定した Deno 用 SMTP ドライバーが作成できるようになりました。 また、Deno 用の安定した PostgreSQL と MySQL のドライバーにも使用できます。

postgres ドライバーは、安定版で動作するようになりました。 次のコードがリリースノートで紹介されています。

import { Client } from "https://deno.land/x/postgres@v0.14.0/mod.ts";

const client = new Client({
  user: "user",
  database: "test",
  hostname: "psql.example.com",
  port: 5432,
  tls: {
    enforce: true,
    caCertificates: [await Deno.readTextFile("/path/to/ca.crt")],
  },
});
await client.connect();

const result = await client.queryObject("SELECT id, name FROM people");
console.log(result.rows); // [{id: 1, name: 'Carlos'},  ...]

テスト用のパーミッション指定が安定化しました

Deno 1.10 で、以下の様にテストに対してパーミッションを許可する機能が導入されていました。

// test.ts

// Deno 1.10.1 で確認済み
Deno.test({
  name: "write",
  permissions: { write: true, read: false }, // <= permissions プロパティで指定
  async fn() {
    await Deno.writeTextFile("./foo.txt", "Write!");
    console.log(await Deno.readTextFile("./foo.txt"));
  },
});

この機能が安定し --unstable が不要になりました。 以下のように実行すると、実行時のパーミッションとしてファイルの読み書きを許可していますが、 permissions プロパティ で読み込みを禁止しているので、エラーになります。

$ deno test --allow-read --allow-write --unstable test.ts

また、実行時に許可していない場合、permissions プロパティで許可することはできないので、 この点には注意が必要です。

localStorage を使うとき、--location が不要になりました

Deno 1.10 で localStorage を使用できるようになりました。 これまで、--location example.com のように指定することが必要でしたが、 Deno 1.16 から、--location を付与せずに、localStorage を使用することができます。 どういったキーが使用されるのかは、以下のルールに従います。

  • --location を使用して指定:与えられたオリジンに基づいて使用するキーが定まります。 http://example.com/a.tshttp://example.com/b.tsでは、同じストレージになり http://example.com/https://example.com/ は異なったストレージになります。
  • --config を使用して指定:--config を指定されると異なったストレージが使用されます。 deno run --config deno.jsonc a.tsdeno run --config deno.jsonc a.tsでは、同じストレージになり deno run --config deno.jsonc a.tsdeno run --config tsconfig.jsonc a.ts は異なったストレージになります。

  • 設定しない:設定をしない場合、メインモジュールの絶対パスに基づいてストレージが定まります。Deno REPL では、Deno REPL を開始したカレントディレクトリに基づいてストレージが定まります。このため、同じパスで複数回指定無く実行すると、永続化されたデータが残った状態で開始されます。

以下のソースを実行して確認してみます。

const key = "key";
const value = "value";

// localStorage
console.log(`key:${localStorage.getItem(key)}`);

localStorage.setItem(key, value);

console.log(`key:${localStorage.getItem(key)}`);
$ deno run web_strage.ts
Check file:///usr/src/app/web_strage.ts
key:null
key:value

$ deno run web_strage.ts
key:value
key:value

$ deno run --location http://example.com web_strage.ts
key:null
key:value

$ deno run --location http://example.com web_strage.ts
key:value
key:value

# オリジン が異なるので、別のストレージ
$ deno run --location http://example-1.com web_strage.ts
key:null
key:value

$ deno run --config config.json web_strage.ts
key:null
key:value

$ deno run --config config.json web_strage.ts
key:value
key:value

# --config が異なるので、別のストレージ
$ deno run --config config-1.json web_strage.ts
key:null
key:value

localStrage を含む、web storage API についての詳しい情報は、マニュアルに記載があります。

AbortSignal への理由設定をサポートします

WHATWG が、AbortSignal の理由を設定する仕様を採用しました。Deno は、この仕様を採用した最初のプラットフォームになります。 リリースノートでは、以下のソースが紹介されています。

const abortController = new AbortController();
abortController.abort();
console.log(abortController.signal.reason);
// => DOMException: The signal has been aborted

const abortController = new AbortController();
const reason = new DOMException("The request timed out", "TimeoutError"); // <= 理由を設定
abortController.abort(reason);
console.log(abortController.signal.reason);
// => DOMException: The request timed out <= 指定した The request timed out が取得できます

Deno 向けパッケージを Node 向けパッケージに変換するツールの提供

Deno 向けモジュールを npm パッケージとして、公開するためのdntが導入されます。 dnt を使用して npm の公開された例として、deno_license_checkerがあります。

dnt によって、Deno ファーストなコードを Node 環境で使うことができます。 ソースは、Deno 公式リポジトリで管理されていますが、サードパーティモジュールとして管理されているのがちょっと面白いです。

その他

V8 が 9.7 に更新されました

数多くのパフォーマンス改善とバグフィックスが含まれています。 機能の追加として次のものが導入されます。

  • findLast findLastIndex が Array クラスに追加されます。
  • WebAssembly の参照型をサポートするようになりました。 WebAssembly の参照型については、The wasm-bindgen Guideにある参照型の解説が助けになるかと思います。

Web Streams API が改善されました

  • ReadableStreamBYOBReader をサポートします。
  • WritableStreamDefaultController.signal をサポートします。
  • ReadableStream.getIterator が廃止されます。

まとめ

Deno 1.16 は、dnt とfetch APIのファイルURL対応、localStorage を使うときに --location が不要になったのがエポックなリリースだと感じました。 特に dnt は、これから Deno 向けモジュールがより増えていく契機になるかもしれません。

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

P.S.

採用情報

■募集職種
yumenosora.co.jp

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

■お申し込みはこちら!
news.toranoana.jp

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

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

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

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