Deno v1.25以降、npmインポートが使えるようになります!この機能を使うとNode.js向けのライブラリがDenoでも動作します。
現在は実験的API(実行に--unstableフラグが必要)となっていますが、今後3か月でサポートを強化し、使えるnpmライブラリを増やしていくとのこと。
実はnpmインポート機能の導入は突然決まったものではなく、今年の2月からryan dahlがやりたいと言っていた機能でした。半年以上かけて議論してやっと導入された感じですね。
またDenoのNode.js互換性自体はBunの登場よりも早い時期(2019年)から進められていて、今回満を持して登場ということになります。
この記事ではnpmインポートの概要と実装、将来について解説していきます。
Denoの npmインポート とは
npmから始まるURLをimport文に指定すると、npmパッケージが使えるようになる機能です。
import express from "npm:express@5";
// ^ npm:<パッケージ名>@<バージョン>
上記のように書くと、expressパッケージがDenoで動作します。
このときexpressパッケージはNode.jsのAPIがポリフィルされた環境で動きます。もちろんその中ではNode.js標準ライブラリやrequire
などが使えるため、問題なく動くというわけです。
下の画像(例)のように、
- npmインポートする側(ライブラリを使う側)のコード:これまで通りのDenoで実行される
- npmインポートされる側のライブラリ:Node.jsポリフィル環境で実行される
という形になります。
公式ブログ投稿の中では3か月以内に90%のnpmパッケージが動作するようになる予定だと書かれています。
npmインポートが追加されると同時に、これまで実験的にサポートされていた--compat
フラグ(Node.js互換モード)は削除されています。
今回削除された--compat
フラグは「node
コマンドをdeno run --compat
に差し換えるだけで動く」というモードで、Node.jsユーザーが手軽に&徐々にDenoに移行することを支援するためのものでした。
しかし--compat
フラグを使うとDenoの挙動とNode.jsの挙動が混じってしまうので、例えば
-
setTimeout()
の挙動はNode.jsとDeno(ブラウザ)のどちらに合わせたらいいのか - サーバー環境とブラウザ環境をif文で分岐しているライブラリの挙動はどうなるのか
といった互換性の問題が出てきました。
また、Node.jsの挙動を模倣することからnode_modules
にライブラリをダウンロードしなければ動かないという問題もあり、「結局npmを使用することになるのではないか?」という懸念も出ていました。
そこでDenoは方針転換をしてnpmインポートを実装し、
- npmインポートで呼び出されるライブラリはNode.jsの挙動
- npmインポートを呼び出す側のコードはDenoの挙動(ESM)
という形で、NodeとDenoの挙動が変わる境界をしっかり分けるようになりました。
この方針変更によってDenoの特徴であるESM Firstの世界を保ったまま、既存のNode.jsライブラリを使えるようになりました。
以下にnpmインポートの特徴を紹介します。
node_modulesが作られない
npmインポートを使う際に、事前にnode_modulesフォルダにライブラリをダウンロードしておく必要はありません。
npmインポートを含むファイルを実行すると、裏側でDenoのキャッシュディレクトリにライブラリがダウンロードされます(つまり他のDenoのライブラリと同じように使える)。
プロジェクトごとにダウンロードされるのではなく、グローバルな場所にキャッシュされるという点では、pnpmの挙動に近いと思います。
On Linux/Redox:
$XDG_CACHE_HOME/deno
or$HOME/.cache/deno
On Windows:%LOCALAPPDATA%/deno
(%LOCALAPPDATA%
=FOLDERID_LocalAppData
)
On macOS:$HOME/Library/Caches/deno
If something fails, it falls back to$HOME/.deno
https://deno.land/manual@v1.25.1/linking_to_external_code#linking-to-third-party-code
Node.js標準ライブラリに対応
Node.jsの標準ライブラリ(例:net
やfs
)は大体ポリフィルが実装されています。
そのためrequire("fs")
とかを使ったコードがちゃんと動きます。
一点注意が必要なのは、READMEに載っているリストの通り、非推奨のモジュールや実験的なモジュールはポリフィルされる予定がないということです。(例:async_hooks
やwasi
)
公式ブログ投稿が今後 3 か月以内に npm パッケージの 80 ~ 90% を Deno で動作させることができます
という控えめな表現になっているのは、このあたりが原因だと思われます。
ちなみにポリフィル実装はNode.js用のテストスイートを使ってテストされていて、現在はポリフィルをどんどん追加していってテストの合格率を上げている最中です。
N-APIに対応予定
N-APIというのはNode.jsに存在するネイティブアドオン用のAPIです。
DenoにN-APIのポリフィルを入れるPRが実装中で、この実装が完了するとN-APIを使っているnpmライブラリがDenoでも動作するようになります。
Denoのnpmインポートの仕組み
続いては、Denoでnpmライブラリが動く仕組みに迫っていきたいと思います。
npmライブラリのダウンロード
まず行われるのがnpmライブラリのダウンロードです。
実装はcli/npmディレクトリで行われており、対象ライブラリを https://registry.npmjs.org からダウンロードしたり、package.jsonから依存関係を解決したり、semverを解決したりといった処理が書かれています。
グローバル変数の挿入
Node.js固有の標準ライブラリ(fs
など)やグローバル変数(setTimeout
など)は標準ライブラリで定義されています。
npmインポートされたライブラリを実行する際に、これらのポリフィルを流し込んでやる必要があります。この処理はext/node/01_node.jsやcli/node/mod.rsで行われています。
require()の定義
require関数はモジュールの読み込みに関与するため、他のグローバル変数とは別枠で定義されています。ext/node/02_require.jsです。
さらに中身を読んでいくと時々ops.op_hogehoge()
という関数呼び出しをしているのがわかります。これはRust側の関数を呼び出していて、ext/node/lib.rsやext/node/resolution.rsの中に具体的なRustの処理が書かれています。
以上、かなりざっくりですが、npmインポートされたライブラリがどう動くのか追ってみました。
npmインポートは既に使える
npmインポートは8月リリースのv1.25から実装されており、既に試すことができます。
試しにaxiosを使っています。
import axios from "npm:axios";
console.log(axios.get(await "https://www.yahoo.jp"));
> deno run ./tmp.ts
Download https://registry.npmjs.org/axios
Download https://registry.npmjs.org/follow-redirects
Download https://registry.npmjs.org/form-data
Download https://registry.npmjs.org/asynckit
Download https://registry.npmjs.org/combined-stream
Download https://registry.npmjs.org/mime-types
Download https://registry.npmjs.org/delayed-stream
Download https://registry.npmjs.org/mime-db
Download https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz
Download https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz
Download https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz
Download https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz
Download https://registry.npmjs.org/axios/-/axios-0.27.2.tgz
Download https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz
Download https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz
Download https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz
<ref *1> [Function: wrap] {
request: [Function: wrap],
getUri: [Function: wrap],
delete: [Function: wrap],
get: [Function: wrap],
head: [Function: wrap],
options: [Function: wrap],
post: [Function: wrap],
postForm: [Function: wrap],
put: [Function: wrap],
putForm: [Function: wrap],
patch: [Function: wrap],
patchForm: [Function: wrap],
defaults: {
...(以下略)
…という感じで、依存関係も含めてきちんと初回実行時にダウンロードされ、正しくimportできているのが分かると思います。
現時点ではリリース直後という事もあって細かいバグが多いらしいのですが、エラーがあったらissueに投稿してくれと公式ブログ投稿に書いてあったので、みんな投稿しましょう。
npmインポートの今後
もともとDenoにおいてはnpmライブラリは https://esm.sh 経由や https://www.skypack.dev/ などのnpm CDN経由で使用できたのですが、この方法だとライブラリによっては依存関係を解決できなかったり、setTimeoutの違いが原因でハングしたり、ブラウザとサーバー側をif文で分岐する個所で変な分岐に入ってしまったりと、微妙に動かないことがありました。
今回導入されたnpmインポートでは完璧なNode.js環境が再現される(はず)で、そういった互換性の問題も解消できると思われます。
一方で、npmインポートはDenoの固有の機能なのでブラウザ互換性の問題があり、特にライブラリの制作者はnpmインポートと既存のnpm CDNをうまく使い分けていく必要があります。(今のところは)
現実的な解としては、まずesm.shを試し、動かなければブラウザ互換をあきらめてnpmインポートを使うのがいいと思います。
(参考:https://github.com/denoland/deno/issues/13703#issuecomment-1218146761 )
またNode.jsにありがちなCJS / Fake ESM(疑似ESM) / Native ESMの互換性問題やTypeScriptの拡張子問題などを、気にすることなく動かせるという点で、Node.jsよりもライブラリが使いやすくなると言えると思います。
2022年8月15日の公式ブログ投稿では「完成まであと3か月」と言っているので、今年の終わりには普通にnpmパッケージが動作するようになっているはずです。lsp対応もこれから行われるようで、エディタとの連携(特にTypeScript周り)が進んでいくのではないかと思います。
(2022/9/20追記)コアチームメンバーの方のissue投稿によると、今後は
-
deno info
などの各種サブコマンドへの対応 - ロックファイル対応
- ./node_modulesディレクトリをDENO_DIRにシンボリックリンクして互換性を向上するオプションの導入
- TypeScriptサポート(現在はimportしたものが全てany型扱いになっている)
- エディタ連携対応(LSPサポート)
- エラーメッセージの改善
- 主要モジュールの動作保証
が順次行われるようです!
まとめ
この機能の魅力は、なんといっても既存のNode.jsライブラリをDenoのパーミッション管理の下で動かせるという点になります。
これからに期待したいと思います!