虎の穴開発室ブログ

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

MENU

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

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

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

Deno 1.38

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

deno doc のアップデート

deno doc --html の追加

deno doc --html により、Rustdocを参考として静的なドキュメントサイトを構築できるようになりました。

使ってみます。

$ tree
.
|-- mod.ts
`-- src
    `-- add.ts

$ cat mod.ts
export * from "./src/add.ts";

$ cat src/add.ts
export function add(a:number, b: number){
    return a + b;
}

# これまで通りの機能
$ deno doc ./mod.ts
Defined in file:///usr/src/app/1_38/src/add.ts:1:1

function add(a: number, b: number)

# 今回リリースされた機能
$ deno doc --html --name=hoge-mod ./mod.ts
Written 7 files to "./docs/"

$ tree docs
docs
|-- add.html
|-- compound_index.html
|-- fuse.js
|-- index.html
|-- search.js
|-- search_index.js
`-- styles.css

1 directory, 7 files

生成されたHTMLをブラウザで開くと次のように表示されます。

先の例では特にコメントも用意しませんでした。 JSDOCを書き、アサーションも拡充して、deno doc --html では次のようにページが作成されます。

$ cat src/add.ts
/**
 * 2つの数値を加算します。
 * @param {number} a 数値A
 * @param {number} b 数値B
 */
export function add(a:number, b: number): number{
    return a + b;
}

複数エントリポイント対応

複数エントリポイントに対応して生成できるようになりました。

$ deno doc ./mod.ts sub.ts`

# '--html' にも対応していました。
$ deno doc --html --name=hoge-mod ./mod.ts sub.ts

deno doc --lint の追加

--lint によって、ドキュメントの生成に当たっての潜在的な問題を見つけやすくなります。

次のように動作します。

$ cat sub.ts
export function sub(a:number, b: number){
    return a - b;
}

# --lint を付ける事で、指摘を受けます。
$ deno doc --lint sub.ts
Missing JS documentation comment.
Missing return type.
    at file:///usr/src/app/1_38/sub.ts:1:1

error: Found 2 documentation diagnostics.

$ cat sub.ts

/**
 * 2つの数値で減算(a-b)をします
 * @param {number} a 数値A
 * @param {number} b 数値B
 */
export function sub(a:number, b: number){
    return a - b;
}

# 返り値の型が無いことに指摘を受けます。
$ deno doc --lint sub.ts
Missing return type.
    at file:///usr/src/app/1_38/sub.ts:6:1

error: Found 1 documentation diagnostic.

# 返り値の型として number を設定し、再度実行すると通ります。
$ deno doc --lint sub.ts
Defined in file:///usr/src/app/1_38/sub.ts:6:1

function sub(a: number, b: number): number
  2つの数値で減算(a-b)をします

  @param {number} a
      数値A

  @param {number} b
      数値B

HMR(Hot Module Replacement) に対応

これまで --watch フラグを設定することで自動的な再起動を行うソリューションが提供されていました。

--unstable-hmr を使用することで、アプリケーションの再起動を伴わずにモジュールの更新/置換ができるようになりました。unstable の機能です。

トップレベルのシンボルや、実行中の非同期関数が更新/置換の対象になるとき、完全な再起動も起きるそうです。 また、モジュールの更新/置換を捕捉するイベントが定義されています。

import { text } from "./text.ts";

let count = 0;
setInterval(() => {
  console.log(`${text}${count}`);
  count++;
}, 1000);

// hmr イベントが定義されている
addEventListener("hmr", (e) => {
  console.log(`HMR Event:${e.detail.path}`);
  // コンソール出力は一瞬で消えるので、ファイル出力すると捕捉しやすい。
  Deno.writeTextFile("hmr_event_log",`HMR Event:${e.detail.path}`)
});

Node.js 互換性向上の向上

npm パッケージマネージャ対応

npmパッケージマネージャをDenoから使うためのサポートが入りました。 この機能は、unstableです。

次のように動きます。

$ npm init -y
$ npm install cowsay

$ cat call_cawsay.ts
import cowsay from "cowsay";

console.log(cowsay.say({
  text: `Hello from Deno using BYONM!`,
}));

# --unstable-byonm を付けるか、deno.json(c)に "unstable": ["byonm"] の記述を追加する
$ deno run --unstable-byonm --allow-read call_cowsay.ts
 ___________________________________
< Hello from Deno using BYONM flag! >
 -----------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

node_modules ディレクトリを削除してから、再度実行するとnpm installを求められます。

$ rm -rf node_modules
$ deno run --unstable-byonm --allow-read .\call_cowsay.ts
error: Could not resolve "cowsay", but found it in a package.json. Deno expects the node_modules/ directory to be up to date. Did you forget to run `npm install`?
    at file:///usr/src/app/1_38/call_cowsay.ts:1:20

--unstable-byonm を外して実行すると、実行のタイミングでインストールされます。

$ deno run --allow-read .\call_cowsay.ts
 ___________________________________
< Hello from Deno using BYONM flag! >
 -----------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

この機能は、package.json があった場合に自動的に、デフォルトで有効にすることが計画されているそうです。

npm installで処理した時と、実行時に自動でインストールされるときに、node_modulesディレクトリに差分が有ったので、掲載します。

[npm install されたnode_modules]

$ ls -la node_modules/
.                   cliui          get-caller-file          path-exists            which-module
..                  color-convert  get-stdin                require-directory      wrap-ansi
.bin                color-name     is-fullwidth-code-point  require-main-filename  y18n
.package-lock.json  cowsay         locate-path              set-blocking           yargs
ansi-regex          decamelize     p-limit                  string-width           yargs-parser
ansi-styles         emoji-regex    p-locate                 strip-ansi
camelcase           find-up        p-try                    strip-final-newline

[deno run でインストールされたnode_modules]

$ ls -la node_modules/
.  ..  .deno  cowsay

$ ls -la node_modules/.deno
.   node_modules    string-width@2.1.1  strip-ansi@4.0.0  wrap-ansi@6.2.0      yargs@15.4.1
..  p-locate@4.1.0  string-width@4.2.3  strip-ansi@6.0.1  yargs-parser@18.1.3

こういったnode_modules ディレクトリ内での差分も出ることから、現在Node.jsを使用したプロジェクトを使うときにnode_modulesには手をつけず、パッケージマネージャの機能は既存の方法を取るというのは、一定の要求があるように思います。

本件についての、Issue でも段階的なDenoへの移行について記述があり、部分的なDenoの導入(部分的なNode.jsの残留)は意味を持ちます。

https://github.com/denoland/deno/issues/18967

Node.js ビルトインモジュールのベア指定子

Node.js 組み込みモジュールをDenoで使うには、node:fs のように、node:指定子が必要です。 fs のように記述すると、エラーになります。

# node:fs を使っている場合
$ cat use_node_specifiers.ts
import fs from "node:fs";
console.log(fs);

$ deno run use_node_specifiers.ts
{
  access: [Function: access],
  accessSync: [Function: accessSync],
  appendFile: [Function: appendFile],
....省略
}

# fs を使っている場合
$ cat bare_specifiers.ts
import fs from "fs";
console.log(fs);

$ deno run bare_specifiers.ts
error: Relative import path "fs" not prefixed with / or ./ or ../
If you want to use a built-in Node module, add a "node:" prefix (ex. "node:fs").
    at file:///usr/src/app/1_38/bare_specifiers.ts:1:16

--unstable-bare-node-builtins フラグを設定するか、deno.json に、"unstable": ["bare-node-builtins"] の記述をする事で、このエラーを回避できるようになりました。

$ deno run --unstable-bare-node-builtins bare_specifiers.ts
{
  access: [Function: access],
  accessSync: [Function: accessSync],
  appendFile: [Function: appendFile],
....省略
}

# deno.jsonに
$ cat deno.json
{
  "unstable": ["bare-node-builtins"]
}

$ deno run bare_specifiers.ts
{
  access: [Function: access],
  accessSync: [Function: accessSync],
  appendFile: [Function: appendFile],
....省略
}

Node.js APIの更新

以下のAPIの修正が行われました。

  • buffer.Buffer.utf8ToBytes
  • crypto.randomFillSync
  • http2.ClientHttp2Session
  • os.availableParallelism
  • process.argv0
  • process.version (v18.18.0を返すように。Deno1.37では、v18.17.1を返していました。)
  • tty.ReadStream
  • tty.WriteStream
  • uv.errname

(最)高速なJSX変換

JSXのレンダリングが7~20倍程度高速になり、ガベージコレクション時間が50%削減。 JSXに含まれるHTML部分が多いほど高速になるとのことです。

JSXのレンダリングに当たってオブジェクトの作成を行う形式から、トランスパイル時にテンプレート化するようにしたことで、概念としては単純な配列の連結に落とし込む処理形態を取れるようになりました。

実際に、設定しこれまでのものと比較します。

リリースノートを参考に、jsx を含むソースコードを用意します。

[jsx_source.tsx]

const link = "https://yumenosora.co.jp/tora-lab";
const name = "Tora Lab";

const jsxSource = (
  <div className="greeting">
    <a href={link}>
      Hello <b>{name}!</b>
    </a>
  </div>
);

[deno.json]

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

一旦実行します。実行後、JavaScript に変換された結果を参照します。

$ deno run jsx_source.tsx

$ cat /deno-dir/gen/file/usr/src/app/1_38/jsx_source.tsx.js
import { jsx as _jsx, jsxs as _jsxs } from "https://esm.sh/preact/jsx-runtime";
const link = "https://yumenosora.co.jp/tora-lab";
const name = "Tora Lab";
const jsxSource = /*#__PURE__*/ _jsx("div", {
  className: "greeting",
  children: /*#__PURE__*/ _jsxs("a", {
    href: link,
    children: [
      "Hello ",
      /*#__PURE__*/ _jsxs("b", {
        children: [
          name,
          "!"
        ]
      })
    ]
  })
});
//# sourceMappingURL=data:application/json;base64,~~省略~~=

JSX が、オブジェクトとして展開されているのがわかります。 続いて、今回登場した "jsx": "precompile" を設定し確認します。

始めに、カスタム jsx ランタイムは、次の関数を用意しエクスポートする事が推奨されます。

  • jsxTemplate(strings, ...dynamic)
  • jsxAttr(name, value)
  • jsxEscape(value)
  • jsx(type, props, key)

公開されている、Deno CLIのテストデータを参考に用意します。 https://github.com/denoland/deno/blob/main/cli/tests/testdata/jsx/jsx-precompile/index.ts

[jsx-precompile/jsx-runtime/index.ts]

export function jsx(
  _type: any,
  _props: any,
  _key: any,
  _source: any,
  _self: any,
) {}

export const jsxAttr = (name: string, value: any) => {};

export const jsxTemplate = (template: string[], ...exprs: any[]) => {
};

export const jsxEscape = (value: any) => "";

declare global {
  namespace JSX {
    interface IntrinsicElements {
      [tagName: string]: Record<string, any>;
    }
  }
}

こちらを、deno.json で設定します。

[deno.json("jsx": "precompile"を設定)]

{
  "compilerOptions": {
    "jsx": "precompile",
    "jsxImportSource": "jsx-precompile"
  },
  "imports": {
    "jsx-precompile/jsx-runtime": "./jsx-precompile/jsx-runtime/index.ts"
  }
}

改めて実行し、JavaScript に変換された結果を参照します。

$ deno run jsx_source.tsx

$ cat /deno-dir/gen/file/usr/src/app/1_38/jsx_source.tsx.js
import { jsxTemplate as _jsxTemplate, jsxAttr as _jsxAttr, jsxEscape as _jsxEscape } from "jsx-precompile/jsx-runtime";
const $$_tpl_1 = [
  '<div class="greeting"><a ',
  ">Hello <b>",
  "!</b></a></div>"
];
const link = "https://yumenosora.co.jp/tora-lab";
const name = "Tora Lab";
const jsxSource = _jsxTemplate($$_tpl_1, _jsxAttr("href", link), _jsxEscape(name));
//# sourceMappingURL=data:application/json;base64,~~省略~~=

JSXが、HTML文字列の配列に展開されているのがわかります。 後は、先の説明の通り固定された文字列とプロパティをjoinしていくだけでレンダリングが完了する様子が読み取れます。

コンポーネントは、従来のJSX変換に戻るそうです。

JSXをプリコンパイルするランタイムがDenoland から提供されていないだろうかと探しまた。 ToDoとなっていますがこちらで提供がされるようです。

https://github.com/denoland/precompiled_jsx

deno run --env

.env に記載された環境変数の情報を取得するには、ファイルを読み込むための --allow-read が必要です。

例えば、std/dotenv を使用する場合次のようになります。

$ cat .env
HOGE_KEY=HOGE_VALUE

$ cat read_dotenv.ts
import "https://deno.land/std@0.205.0/dotenv/load.ts";

console.log(Deno.env.get("HOGE_KEY")); 

# std/dotenv は自動的に3つのファイルを読んでいますが、--allow-read が必要ということだけ押さえてください。
$ deno run  read_dotenv.ts
✅ Granted read access to ".env".
✅ Granted read access to ".env.defaults".
✅ Granted read access to ".env.example".
✅ Granted env access to "HOGE_KEY".
HOGE_VALUE

新たに追加された --env フラグを使用することで、次のようにできます。

$ cat read_env.ts
console.log(Deno.env.get("HOGE_KEY"));

# --allow-read を要求されない
$ deno run --env read_env.ts
✅ Granted env access to "HOGE_KEY".
HOGE_VALUE

--allow-read 権限の要求なく .env に記述された環境変数を使用することができました。 --env=.env.dev のようにファイルの指定も可能です。 今後のリリースでは、複数行の変数サポートが計画されているそうです。

Deno.testとJSXをREPLで扱えるようになった

以下のようにREPLで、Deno.test が使用できます。

$ deno
Deno 1.38.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.
> import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
undefined

# 成功するテスト
> Deno.test("test#1", ()=> assertEquals(1,1) )
test#1 ... ok (0ms)

ok | 1 passed | 0 failed (0ms)
undefined

# 失敗するテスト
> Deno.test("test#1", ()=> assertEquals(1,2) )
test#1 ... FAILED (2ms)

 ERRORS

test#1 => <anonymous>:1:27
error: AssertionError: Values are not equal.


    [Diff] Actual / Expected


-   1
+   2

  throw new AssertionError(message);
        ^
    at assertEquals (https://deno.land/std@0.205.0/assert/assert_equals.ts:53:9)
    at <anonymous>:1:46

 FAILURES

test#1 => <anonymous>:1:27

FAILED | 0 passed | 1 failed (0ms)
undefined

JSXも、REPLで使用できるようになりました。

$ deno
Deno 1.38.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.

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

> <b>test</b>
{
  type: "b",
  props: { children: "test" },
  key: undefined,
  ref: undefined,
  __k: null,
  __: null,
  __b: 0,
  __e: null,
  __d: undefined,
  __c: null,
  __h: null,
  constructor: undefined,
  __v: -1,
  __i: -1,
  __source: undefined,
  __self: undefined
}

Deno API の変更

Deno.serve

Deno.serve が Unix domain socket をサポートするようになりました。 また、Deno.serveは、Deno.Serverの代わりにDeno.HttpServerを返すようになりました。

using を Deno API で使用する

Deno 1.37 から同梱されるようになったTypeScript 5.2から using キーワードが使用できるようになりました。

https://devblogs.microsoft.com/typescript/announcing-typescript-5-2/#using-declarations-and-explicit-resource-management#using-declarations-and-explicit-resource-management

スコープを外れた場合に、何かしらのクリーンアップ(ネットワークを閉じる、ファイルを閉じるなど)をGoやSwiftのdefer メソッドを使うように指定できます。

以下のメソッドは、using に対応してリソースが閉じられるようになりました。

  • Deno.Command
  • Deno.FsFile
  • Deno.FsWatcher
  • Deno.HttpConn
  • Deno.HttpServer
  • Deno.Kv
  • Deno.Listener

using キーワードは、TypeScript ファイルでのみ有効です。

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

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

let fs2: Deno.FsFile

{
  using fs = await Deno.open("./text.txt", { read: true, write: true });
  fs2 = fs;
  const buf = new Uint8Array(100);
  assertEquals(await fs2.read(buf), null);
}

const buf = new Uint8Array(100);
await assertRejects(async() => await fs2.read(buf), Deno.errors.BadResource);

スコープを抜けるとリソースが解放されていることが確認できます。

Web APIの変更

window.name

互換性のために空文字列が帰ってくるようになりました。 将来的に、window.name を設定することでプロセス名を変更できるように検討する可能性があるそうです。

EventSource

EventSource Web API が追加されました。 EventSource をクライアントとして使用する、サーバーからのイベントを受信する通信を行えます。 次のようにサーバーとクライアントを用意し通信できます。

[deno run -A event_source_server.ts]

import {
  ServerSentEvent,
  ServerSentEventStreamTarget,
} from "https://deno.land/std@0.204.0/http/server_sent_event.ts";

Deno.serve({ port: 8000 }, (_) => {
  const target = new ServerSentEventStreamTarget();
  target.dispatchEvent(
    new ServerSentEvent("message", {
      data: "text message",
    }),
  );
  return target.asResponse();
});

[deno run -A event_source_client.ts]

const source = new EventSource("http://localhost:8000");

source.onopen = () => {
  console.log("opened");
};

source.onmessage = (e) => {
  console.log(e.data);
  source.close();
  console.log("closed");
};
# サーバー起動
$ deno run -A event_source_server.ts
Listening on http://localhost:8000/

# 別のコンソールで起動
$ deno run -A event_source_client.ts
opened
text message
closed

--unstable-* フラグを細かく設定できるようになりました

これまで、unstable な機能を有効にするには、--unstable を使ってすべてまとめて設定することしかできませんでした。 このリリースから、以下フラグ群を使用して個別に有効にできるようになりました。

--unstable-bare-node-builtins --unstable-broadcast-channel --unstable-byonm --unstable-cron --unstable-ffi --unstable-fs --unstable-hmr --unstable-http --unstable-kv --unstable-net --unstable-worker-options

また、deno.json に以下のように記述することでも個別に有効にできるようになりました。

{
  "unstable": ["ffi", "fs", "kv"]
}

その他

  • WebSocketのバグの改善
  • deno task が head コマンドをサポート
  • VSCode 拡張機能と言語サーバー
    • VSCodeの組み込みTS/JSオプションのサポート
    • DenoタスクをVSCodeのサイドバーに表示できるようになった
    • Quick Fixからキャッシュされていないモジュールをキャッシュできるようになった。
    • その他多数の修正
  • Jupyter Notebook 向け機能の拡張
  • 標準ライブラリの更新
    • std/path の更新
    • std/http の更新
    • std/io の廃止
    • std/wasi の廃止
  • パフォーマンスの向上
  • Deno 1.38には、V8 12.0 が同梱されています。

Deno 1.38 を見てきました。

JSXの変換の変換オプションは

Deno 1.38 には、リリースノートに詳細の記載が無く、--unstable-cron フラグが追加だけ記載されている Deno.cron が、unstable な機能として導入されています。

https://github.com/denoland/deno/blob/8acf059ac683ff13c6973914c57caa0ef07d6d9a/cli/tsc/dts/lib.deno.unstable.d.ts#L1338

導入時のプルリクエストはこちら。

https://github.com/denoland/deno/pull/21019

export function cron(
  name: string,
  schedule: string,
  handler: () => Promise<void> | void,
  options?: { backoffSchedule?: number[]; signal?: AbortSignal },
): Promise<void>;

次のように使用できます。

[cron.ts]

Deno.cron("sample cron", "* * * * *", () => {
  console.log("Call cron job.");
});

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

# 1分ごとに呼び出される。
$ deno run --unstable-cron cron.ts
Call cron job.
Call cron job.

この機能を Deno Deploy で動かすことも予告されているので、Cloudflare WorkersのCron Triggersのような運用ができるるようになるのが見込めそうです。

https://twitter.com/deno_land/status/1719416065288229315

Deno task のクロスプラットフォームのシェルのように、クロスプラットフォームのcron環境が手に入りそうなのも魅力的ですね。

Deno 1.39も公開されたら、また追いかけていきます。

採用情報

虎の穴では一緒に働く仲間を募集中です!
この記事を読んで、興味を持っていただけた方はぜひ弊社の採用情報をご覧下さい。
カジュアル面談やエンジニア向けイベントも随時開催中です。ぜひチェックしてみてください♪
yumenosora.co.jp