🦕

Deno v1.15で導入されたNode.js互換モードについて

2021/10/15に公開

はじめに

2021/10/12にDeno v1.15がリリースされました。

この記事では、Deno v1.15で新しく導入されたNode.jsの互換モードについて解説します。

Node.jsの互換モードとは?

まず、以下のようなJavaScriptファイルがあったとします。

main.mjs
import { EventEmitter } from "events";

const emitter = new EventEmitter();
emitter.on("foo", () => console.log("foo"));
emitter.emit("foo");

Node.jsの組み込みモジュールであるeventsモジュールを利用しているため、通常ではこのファイルをDenoで実行することはできません。

$ deno run main.mjs
error: Relative import path "events" not prefixed with / or ./ or ../ from "file:///home/uki00a/ghq/github.com/uki00a/deno-sandbox/main.mjs"
    at file:///home/uki00a/ghq/github.com/uki00a/deno-sandbox/main.mjs:1:30

それでは、このファイルを--compatオプションを付けて実行してみます。

$ deno run --compat --unstable main.mjs
foo

このように--compatオプションを付与することで、Node.jsの互換モードが有効化され、DenoからNode.jsの組み込みモジュールを利用できるようになります。

また、このモードではglobalprocessなどのオブジェクトを参照することもできます。

console.log(global.Buffer.from('Hello'));
console.log(process.version);

どうやって実現されてるの?

内部的にはImport mapsとdeno_std/nodeを併用することで実現されています。

まずはじめにこれら2つについて説明します。

Import maps

Import mapsとはWICGで提案されている機能であり、Denoや一部のブラウザなどでサポートされています。

例えば、以下のような内容のJSONファイルを用意します。

import_map.json
{
  "imports": {
    "std/": "https://deno.land/std@0.111.0/",
    "redis": "https://deno.land/x/redis@v0.24.0/mod.ts"
  },
  "scopes": {}
}

このようなファイルを用意しておくことで、ソースコード中では以下のようにしてモジュールを読み込むことができます。

main.ts
import { connect } from "redis"; // => https://deno.land/x/redis@v0.24.0/mod.ts
import * as path from "std/path/mod.ts"; // => https://deno.land/std@0.111.0/path/mod.ts

const redis = await connect({ hostname: "localhost", port: 6379 });
console.log(await redis.get("foo"));
await redis.quit();

Import mapsを利用する際は、Denoを実行する際に--import-mapオプションでJSONファイルのパスを指定する必要があります。

$ deno run --import-map=import_map.json --allow-net main.ts

deno_std/node

deno_stdとはDeno公式によって提供されているDenoの標準モジュールです。

このdeno_stdはNode.jsの互換レイヤとしてdeno_std/nodeを提供しています。

これを利用することで、Common JS形式のJavaScriptモジュールを読み込むことができます。

main.ts
import { createRequire } from "https://deno.land/std@0.111.0/node/module.ts";

const require = createRequire(import.meta.url);

// 組み込みモジュールの読み込み
const EventEmitter = require("events");

// Common JS形式のモジュールの読み込み
const add = require("./add");

上記ファイルを実行するには、deno_std v0.111.0の時点では--unstableが必要です。

$ deno run --unstable --allow-read main.ts

Node.js互換モードの内部実装

Denoの起動時に--compatオプションが指定されると、Denoプロセスは内部で保持しているImport mapsにdeno_std/node/events.tsdeno_std/node/net.tsなどの各種Polyfillモジュールを登録していきます。

そして、Denoの引数として与えられたメインモジュールを読み込む直前にdeno_std/node/global.tsを読み込むことで、各種グローバル変数などを登録しています。

これにより、Node.jsの組み込みモジュールやglobalなどのグローバル変数の透過的な読み込みが実現されています。

deno_std/nodeでサポートされている組み込みモジュールについて

現時点(deno_std v0.111.0)でサポートされている組み込みモジュールの一覧は、以下から確認できます。

まだ全てのモジュールがサポートされているわけではありませんが、直近でもnetdnsなどのモジュールが実装されており、今後徐々に互換性は向上していくと思われます。

実際にいつ使えばいいの?

個人的には既存のNode.jsで書かれたツールなどに変更を加えることなく、そのままDenoで動かしたい場合に使用するのがよいと思っています。

理由については後述しますが、DenoにはNode.js互換モード以外にもNode.jsの資産を活かすための代替手段があるためです。

Node.js互換モードの代替手段

今回のリリースでNode.jsの互換モードが導入されましたが、実はこれを使用しなくとも、Denoでは既存のNode.jsの資産を活かす手段があります。

例えば、esm.shSkypackなどのCDNは、Node.jsの組み込みモジュールの読み込みを検出すると、自動的にdeno_std/nodeを読み込むように置換してくれます。

これらのCDNからnpmパッケージをimportすることで、Node.js互換モードを使用せずとも既存の資産をある程度活かすことができます。

https://zenn.dev/uki00a/articles/how-to-use-npm-packages-in-deno

そのため、実用上は、すぐにNode.js互換モードを使用するのではなく、まずはこれらのCDNからパッケージを使用できないか試してみるとよいのではないかと個人的には思います。

おわりに

この記事ではDeno v1.15で導入されたNode.js互換モードについて解説しました。

Deno v1.15には、これ以外にも様々な機能が追加されています。

例)

  • 入れ子のテストケースへの実験的なサポート
  • deno uninstallコマンド (deno installコマンドでインストールされたツールのアンインストール用コマンド)
  • Deno.kill/Deno.resolveDns/URLPatternなどの安定化

もし興味がありましたら、ぜひ試してみてください!

参考

Discussion