虎の穴開発室ブログ

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

MENU

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

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

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

Deno 1.32

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

Node.js の互換性が強化されました

今回のリリースでも引き続き、Node.js との互換性の強化が入りました。

パッケージインストールの制限

リリースノートでは、package.json に記述があり、ベア指定子を使用した読み込みが、コードで使用されていた場合にパッケージがインストールされると読めたのですが、 Deno 1.31 と Deno 1.32 の間で比較を試みたところ、具体的な差異が出る部分が確認できませんでした。

遅延してのエラー表示

package.json に記述されたパッケージのインストールの際に、依存関係の解析にエラーが起きることがあります。このエラーが遅延して出力されるようになり、中断することが最小限になったとのことです。

package.json の自動検出を制御するフラグ --no-config --no-npm が追加

掲題の通り、--no-config または、--no-npm を使用することで、package.json を自動検出しなくなります。 Deno 1.32 のリリースノートとして記載があるものの、このフラグ自体は、Deno 1.31 では、既に deno run -h でヘルプを見ると出てきます。

実際に使ってみると次のようになります。

$ deno -V
deno 1.32.0

$ cat index.mjs
import express from "express";
const app = express();

app.get("/", function (req, res) {
  res.send("Hello World");
});

console.log("start server")

app.listen(8000);

$ deno run -A --no-npm index.mjs
error: Relative import path "express" not prefixed with / or ./ or ../
    at file:///usr/src/app/index.mjs:1:21


$ deno run -A --no-config index.mjs
error: Relative import path "express" not prefixed with / or ./ or ../
    at file:///usr/src/app/index.mjs:1:21

$ deno run -A index.mjs
start server

環境変数 DENO_NO_PACKAGE_JSON が追加

互換性の強化という項目ではありますが、こちらも package.json の読み込みを制御する環境変数です。

DENO_NO_PACKAGE_JSON=1 を設定することで、次のように動作します。

$ DENO_NO_PACKAGE_JSON=0 deno run -A index.mjs
start server

$ DENO_NO_PACKAGE_JSON=1 deno run -A index.mjs
error: Relative import path "express" not prefixed with / or ./ or ../
    at file:///usr/src/app/index.mjs:1:21

Node.js 環境から移設してきたものではない package.json を使う意図の無いプロジェクトでは、積極的に使用していくのがよいのではないでしょうか。

暗号化のサポート範囲拡張

node:crypto に createCipheriv と createDecipheriv の 2 つの API が導入されました。

詳細なそれぞれの仕様は、Node.js のドキュメントがわかりやすいです。

nodejs.org

deno compile が、Web Worker と 動的インポートをサポート

Deno 1.31 で対応された動的インポートが強化され、Webワーカーも同一バイナリに取り込めるようになりました。 Deno 1.32 では、静的に解決できないインポートにも対応します。

比較し、確認します。

$ deno -V
deno 1.31.0

$ cat main.js
const dynamicModule = await import("./dynamic_module.ts");

console.log(dynamicModule.hello());

$ cat dynamic_module.ts
export function hello(): string {
  return "hello world!";
}

$ deno compile -o compiled_main main.ts

$ ./compiled_main
hello world!

Deno 1.31 のリリースノートを確認した際に、記載されていたポイントは「静的解析可能」という部分でした。 const dynamicModule = await import("./dynamic_module.ts"); のように、このソースは、インポートの対象が文字列で指定された、静的なものです。

では、静的解析できないものを確認します。

$ deno -V
deno 1.31.0

$ cat main.js
const dynamicModule = await import(import.meta.resolve("./dynamic_module.ts")); // <= 書き換えた

console.log(dynamicModule.hello());

$ cat dynamic_module.ts
export function hello(): string {
  return "hello world!";
}

$ deno compile -o compiled_main main.ts
Check file:///usr/src/app/main.ts
Compile file:///usr/src/app/main.ts
Emit compiled_main

$ ./compiled_main
error: Uncaught (in promise) TypeError: Module not found
const dynamicModule = await import(import.meta.resolve("./dynamic_module.ts"));
                      ^
    at async file:///usr/src/app/main.ts:1:23

import.meta.resolve("./dynamic_module.ts") のように、静的解析できないものを与えると、上記のようにエラーになります。 実行環境を Deno 1.32 に切り替えてリトライしてみます。

$ deno -V
deno 1.32.0

$ cat main.js
const dynamicModule = await import(import.meta.resolve("./dynamic_module.ts"));

console.log(dynamicModule.hello());

$ cat dynamic_module.ts
export function hello(): string {
  return "hello world!";
}

# 静的解析できないものを、バイナリに含めるには、--include フラグを使う
$  deno compile -o compiled_main --include dynamic_module.js main.js
Check file:///usr/src/app/main.ts
Compile file:///usr/src/app/main.ts
Emit compiled_main

$ ./compiled_main
hello world!

静的に解析できない、(import.meta.resolve("./dynamic_module.ts") をインポートし動作させることができました。

これと同様のことを、WebWorker のワーカー側のソースをバイナリに含めるときにも使用できます。 リリースノートにならって実装し動作確認します。

$ deno -V
deno 1.32.0

$ cat main.ts
const worker1 = new Worker(import.meta.resolve("./worker1.js"), {
  type: "module",
});

worker1.postMessage("hello from main!");
worker1.onmessage = (e) => {
  console.log("main received", e.data);
  worker1.terminate();
};

const worker2 = new Worker(import.meta.resolve("./worker2.js"), {
  type: "module",
});

worker2.postMessage("hello from main!");
worker2.onmessage = (e) => {
  console.log("main received", e.data);
  worker2.terminate();
};

$ cat worker1.js
self.onmessage = (e) => {
  console.log("worker1 received:", e.data);
  self.postMessage("hello from worker1!");
};

$ cat worker2.js
self.onmessage = (e) => {
  console.log("worker2 received:", e.data);
  self.postMessage("hello from worker2!");
};

# 失敗パターン --include を使わない
$ deno compile -o worker_example main.ts
Compile file:///usr/src/app/main.ts
Emit worker_example

# 実行時にエラーを起こす
$./worker_example
error: Uncaught (in worker "") Module not found
error: Uncaught (in worker "") Module not found
error: Uncaught (in promise) Error: Unhandled error in child worker.
    at Worker.#pollControl (ext:runtime/11_workers.js:157:19)

# 成功パターン
# --include は、複数個指定できる
$ deno compile --include worker1.js --include worker2.js -o worker_example main.ts
Compile file:///usr/src/app/main.ts
Emit worker_example

# 実行できる
$ ./worker_example
worker1 received: hello from main!
main received hello from worker1!
worker2 received: hello from main!
main received hello from worker2!

Web Worker の動作も確認できました。

ここで疑問に1つの疑問が浮かぶかもしれません。
ワーカーのソースコードを静的解析可能な形で呼び出したらどうなるのか?です。 具体的には、次のようにワーカーを呼び出せれば、--include は不要なのではないか?ということです。

const worker1 = new Worker("./worker1.js", {
  type: "module",
});

実際に、この状態で deno compile したものを実行すると、次のようにエラーになります。

$ ./worker_example
error: Uncaught URIError: invalid URL: relative URL without a base: relative URL without a base
    at createWorker (ext:runtime/11_workers.js:39:14)
    at new Worker (ext:runtime/11_workers.js:108:16)
    at file:///usr/src/app/main.ts:1:17

URL として解釈できないというエラーになります。 Deno で、Web Worker を使用したマルチスレッドアプリケーションをバイナリ化したいとき、--includeは必須になります。

拡張子の無いファイルを deno run で実行できるようになりました

--ext フラグを使用することで、拡張子の無いファイルを実行できるようになりました。 ヘルプで、設定可能な値を確認できます。

$ deno run -h
# 省略
  --ext <ext>
      Set content type of the supplied file [possible values: ts, tsx, js, jsx]

こちらを使用し、動作確認してみます。

# 一旦 --ext を使用しないパターンで確認
$ cat ./no_ext_script
#!/usr/bin/env -S deno run 

console.log("Hello Deno")

# これでも 動作する。
$ ./no_ext_script
Hello Deno

# 関数をTypeScriptの構文で記述
$ cat ./no_ext_script
#!/usr/bin/env -S deno run

console.log("Hello Deno")

function add(a: number, b:number):number{
  return a + b
}

console.log(add(1, 2))

# 今回は失敗する => Typescript として解釈できていない様子が確認できる
$ ./no_ext_script
error: The module's source code could not be parsed: Expected ',', got ':' at file:///usr/src/app/no_ext_script:5:15

  function add(a: number, b:number):number{

拡張子が無い場合に、JavaScript として解釈ができているようです。 TypeScriptの構文で、関数を記述したらエラーになりました。 --ext=ts オプションを設定して、Typescript として解釈させてみます。

# --ext オプションを設定
$ cat ./no_ext_script
#!/usr/bin/env -S deno run --ext=ts

console.log("Hello Deno")

function add(a: number, b:number):number{
  return a + b 
}

console.log(add(1, 2))

# 動作します。
$ ./no_ext_script
Hello Deno
3

この機能が何を目的としているのかですが、拡張子の無いスクリプトを作成したいという issue が出ていました。

github.com

2020年5月に立ち上がり、こちらを起点としたpull requestがいくつも立っています。

この確認では、--ext を指定していない時 JavaScript として解釈している様だと先に書きましたが、こちらは修正され TypeScript として解釈されるようになるようです。(本記事の公開タイミングでは既にマージ済みかもしれません。)

github.com

Deno API の変更

Deno.FileInfo.dev が、Windows環境 にも対応しました

Deno.FileInfo.dev が、Windows環境上でも定義されました Deno.FileInfo.dev のインターフェイス定義はこれまで null | number でしたが、これを機に number に変わっています。

次のように使用します。

$ Deno 1.32.1
exit using ctrl+d, ctrl+c, or close()
REPL is running with all permissions allowed.
To specify permissions, run `deno repl` with allow flags.

> const fileInfo = await Deno.stat("./no_ext_script");
undefined

> fileInfo.dev
3701651949

この値は、ファイルを含むデバイスのIDになっています。

インターフェースの差分は、次のドキュメントで確認できます。

Deno 1.31.3 時点

deno.land

Deno 1.32.0 時点

deno.land

「Linux/Mac OS only.」の記述が、Deno 1.32 では消えているのがわかります。

unstable な API の追加

次の2つのunstableなAPIが追加されました。

  • Deno.DatagramConn.joinMulticastV4
  • Deno.DatagramConn.joinMulticastV6

マルチキャストに使用するAPI群です。

Web API の変更

URLSearchParams.size のサポートを追加

whatwg による提案で策定された URLSearchParams.size() が実装されました。

提案の issue はこちらです。

github.com

簡単に動作確認します。

// deno 1.31 で確認
$ deno
Deno 1.31.0
exit using ctrl+d, ctrl+c, or close()
REPL is running with all permissions allowed.
To specify permissions, run `deno repl` with allow flags.
> const url = new URL('https://example.com?foo=1&bar=2');
undefined
> const params = new URLSearchParams(url.search);
undefined
> params.size
undefined
> Array.from(params).length
2
>

Deno 1.31 では、.size は存在しておらず、Array.from()を使用することで個数を確認できるようになっています。

// Deno 1.32 で確認
$ deno
Deno 1.32.1
exit using ctrl+d, ctrl+c, or close()
REPL is running with all permissions allowed.
To specify permissions, run `deno repl` with allow flags.
> const url = new URL('https://example.com?foo=1&bar=2');
undefined
> const params = new URLSearchParams(url.search);
undefined
> params.size
2
>

.size を使用すると、シンプルな記述ができます。

Web GPU API は削除されました

Web GPU は unstable なAPIとしてサポートをしていましたが、
Web GPU を使わないユーザーも deno バイナリサイズの肥大化と起動時間が長くなるというコストを背負う状況になっていました。
このリリースでは、Web GPU API は削除になります。

引き続き、起動時間に影響しない実装を調査するそうです。

標準ライブラリの変更

std/ 以下で提供されている標準モジュールのディレクトリ構造が大きく変わりました。

std/encoding/以下の6モジュールが std/ に移動されます。

Typescript 5.0 を同梱しています

Deno 1.32 に同梱されるTypeScriptは、最新の安定版 TypeScript 5.0 になりました。 これによる型チェックのパフォーマンスが向上しました。

注意事項として、 ES decorator はまだサポートされておらず、将来的なリリースで有効化される見込みです。

V8 11.2 を導入

Deno に同梱されている V8 のバージョンが 11.2 に変わりました。 いくつかの新しい機能を使えます。

この中で、リサイズ可能な ArrayBuffer が導入されていますが、こちら脆弱性の指摘がされています。 対象は、Deno 1.32.0 だけですので、Deno 1.32.1 以上を必ず使用しましょう。

脆弱性の指摘はこちら

github.com

リリースノート記事に書いていない機能

Deno 1.32.0 で、リリースノート記事では紹介にはありませんが、興味深い unstable なAPIが追加されました。

key-value ストアがランタイムに導入されました。

pull request

github.com

ドキュメント

deno.land

pull requestとドキュメントを参考に、こちらを使ってみます。

[use_store_1.ts]

const db = await Deno.openKv(); 

await db.set(["keys","key1"], "string")
await db.set(["keys","key2"], {a:1,b:"string"})

const entries = db.list({prefix:["keys"]})

for await(const entry of entries){
    console.log(entry)
}
// => { key: [ "keys", "key1" ], value: "string", versionstamp: "00000000000000010000" }
// => {
//      key: [ "keys", "key2" ],
//      value: { a: 1, b: "string" },
//      versionstamp: "00000000000000020000"
//    }

console.log(await db.get(["keys","key1"]))
// -> { key: [ "keys", "key1" ], value: "string", versionstamp: "00000000000000010000" }

console.log(await db.get(["keys","key2"]))
// => {
//      key: [ "keys", "key2" ],
//      value: { a: 1, b: "string" },
//      versionstamp: "00000000000000020000"
//    }

await db.set(["keys","key1"], "string-update")
console.log(await db.get(["keys","key1"]))
// => {
//      key: [ "keys", "key1" ],
//      value: "string-update",
//      versionstamp: "00000000000000030000"
//    }
await db.delete(["keys","key1"])
console.log(await db.get(["keys","key1"]))
// => { key: [ "keys", "key1" ], value: null, versionstamp: null }

await db.close();

このように動作します。 データの保存先を確認したところ、/deno-dir/location_data/[ハッシュ値]/kv.sqlite3 に保管されていました。 カレントディレクトリを変更して実行したところ、別のハッシュ値でパスが切られ保存されていました。

任意のファイルで永続化することもできます。 この場合には、--allow-write --allow-read の付与が必要です。

[use_store_2.ts]

const db = await Deno.openKv("store");

console.log(await db.get(["key1"]))

await db.set(["key1"], "string")

await db.close();

こちらを実行すると次のように動作します。

$ ls
use_store_1.ts  use_store_2.ts

# 1回目
$ deno run --unstable --allow-read --allow-write use_store_2.ts
{ key: [ "key1" ], value: null, versionstamp: null }

# store が増えている
$ ls
store  use_store_1.ts  use_store_2.ts


# 2回目 => 1回目で書き込んだ内容が取得できる
$ deno run --unstable --allow-read --allow-write use_store_2.ts
{ key: [ "key1" ], value: "string", versionstamp: "00000000000000010000" }

# store を削除
$ rm store

# store を削除したので、永続化した情報は無い
$ deno run --unstable --allow-read --allow-write use_store_2.ts
{ key: [ "key1" ], value: null, versionstamp: null }

Deno.openKv に渡した名称でファイルが作成され、登録した内容が永続化できていることが確認できます。 また、Deno.openKv(":memory:") のように呼び出すと、メモリ上にデータベースが構築されるので永続化されません。

少し中を探索するため、ソースを見ると Cache Web API 同様に SQLite の機能が使われているようです。

sqlite3 クライアントを使用し、中を覗いてみます。

$ sqlite3 store
SQLite version 3.34.1 2021-01-20 14:10:07
Enter ".help" for usage hints.

sqlite> .tables
data_version     migration_state  queue_running
kv               queue

sqlite> .schema kv
CREATE TABLE kv (
  k blob primary key,
  v blob not null,
  v_encoding integer not null,
  version integer not null
) without rowid;

sqlite> select * fromkv;
Error: near "fromkv": syntax error

sqlite> select * from kv;
 key1|" string|1|1

一部文字化けを起こしていますが、key1 をキーに、値 "string" を持っていることが確認できます。

localStorage に加え、Deno でデータ永続化が可能なAPIが1つ増えたことになります。

本記事を書いている時ちょうど、さらに詳細に解説されている記事が公開されていましたので、そちらも紹介させていただきます。

zenn.dev


まとめ

今回のリリースでは、--include の導入で Web Worker も含めてバンドルできるようになったこと、key-value ストアの導入の2点が特に気になるものでした。

特に key-value ストアは先に書いたとおり、データの永続化が可能なAPI として、localStorage との使い分けの点で少々悩ましい点を感じます。 ただ、localStorage ブラウザ互換のAPIであること、永続化されたときのデータ実体の保存先に差異がでるので、そのあたりで用途が分かれるのではないかと感じます。 今回は、リリースノート記事でも言及が無いものでしたので、stable になった時には、意図しているところについて言及があればうれしいと思います。

Deno 2.0 のマイルストーンが、少しずつ埋まり始めています。

github.com

deno.json の構成のフラット化の提案もされているものの、破壊的変更のバッチがついていないので不安にも思いましたが、pull requestを見ると旧構成もサポートが継続するようで安心しました。

github.com

いつ頃すべてクローズになり(過去のリリースでは100%完了でなくてもリリースされていることもある)、リリースになるのか見えてはいませんが首を長くして待ちましょう。

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

P.S.

採用

虎の穴では一緒に働く仲間を募集中です!
この記事を読んで、興味を持っていただけた方はぜひ弊社の採用情報をご覧下さい。
yumenosora.co.jp