「Denoにはnpmがない」は部分的に正しい

:::note
## （追記）Denoにnpmインポートが実装されました！

Deno v1.25以降、npmインポートが実装され、npmライブラリを使うことができるようになりました。

この記事は、npmインポートが存在しなかった時代に、Node.jsとDenoの間でライブラリの使い方を比較した記事になります。

新しく導入されたnpmインポートについては、[Denoの新機能「npmインポート」について予習する](https://qiita.com/access3151fq/items/e38f78db2d96a1ddd6cc)で詳しく解説しているので、そちらもご覧ください。
:::

----

Node.jsとDenoを比較する際の特徴として、「npmがない」というのがよく言及されています。
これ自体は正しいのですが、まるでパッケージ管理が貧弱かのように聞こえてしまい、誤解を生んでいるので、改めて整理していきたいと思います。



# Denoのパッケージ管理についての誤解

### 誤解①：npmのパッケージが使えない

DenoはランタイムとしてNode.jsとの互換性を切ったものの、Node.js向けのコードをブラウザ向けのコードに変換するツールは沢山あります。Denoはブラウザ向けのコードがそのまま動くので、npmパッケージをDenoで利用することが可能です。

現時点では以下のレジストリを経由して**Denoでnpmパッケージを使用できる**ことが知られています。

- https://esm.sh/
- https://unpkg.com/
- https://www.skypack.dev/
- https://jspm.org/

ただし注意点として、Node.jsの標準ライブラリは[Denoの標準ライブラリでポリフィルされている](https://deno.land/std@0.106.0/node#supported-modules)のですが、このポリフィルの実装が途中です。
なので、importしたいnpmパッケージが未実装の標準ライブラリに依存していると、エラーが出る可能性があります。

ちなみに、「ファイル中に`require()`が含まれていると使えない」というのは古い情報です。

### 誤解②：実行のたびに外部パッケージのURLにアクセスする

外部URLからのimportを用いた場合、**初回実行時のみモジュールのURLにアクセス**し、2回目以降の実行ではローカルのキャッシュを使用します。
キャッシュに使うディレクトリは環境変数`DENO_DIR`で制御することが出来ます。

### 誤解③：パッケージマネージャが無い

Node.jsの場合、npmやyarnといったパッケージマネージャが外部コマンドとして存在します。
Denoの場合はどうでしょうか。

> @ry: URLインポートの考え方は、Denoが独自のパッケージマネージャーとして機能するというものです。補助的なツールの必要性を明確に避けたいと思います。
https://github.com/denoland/deno/issues/47#issuecomment-395405713

上記コメントにある通り、**Deno自体がパッケージマネージャの役割を果たします**。
依存関係の解決、依存関係のダウンロード、依存関係の一覧表示など、Node.jsでは外部コマンドとして存在したものが**Deno本体に組み込まれています**。

パッケージマネージャが無いというよりは、パッケージマネージャの外部コマンドが無いと言ったほうが正しいと思います。


余談ですが、Denoはサードパーティ製ツールの必要性を極力無くすように作られています。フォーマッター、リンター、分散サーバーレスコンピューティング（deno deploy）等がDenoから提供されています。
今まではサードパーティ製ツールの数でエコシステムの成熟度を判断しているような所がありましたが、Denoにおいては公式が提供するものだけで一通り揃うようになっています。

### 誤解④：import文に必ずURLを使用する必要がある

Node.jsとブラウザでは、import文でモジュールを指定する方法が異なります。


```ts:ブラウザ
// 「モジュールが配信されているURL」を指定する
import _ from "https://esm.sh/lodash/";
```

```ts:Node.js
// 「モジュール名」を指定する
import _ from 'lodash'
```

本来、Node.jsとブラウザでは（それぞれに固有のAPIを使用しなければ）全く同じコードが利用できるはずです。それなのに、import文の書き方が違うだけで、バンドラ等を利用して実行前に変換作業を行う必要が出てきます。面倒です。

Denoでは、**URLを利用したimportが導入されたので、ブラウザと同形のコードが利用できます**。バンドラ等の変換プロセスを導入する必要はありません。

```ts:ブラウザ＆Deno
import _ from "https://esm.sh/lodash/";
```

また、Node.jsとブラウザのimportを近づけるための手段として、import-mapも導入されました。これはブラウザとDenoの両方で利用可能です。

```json:import-map.json
{
  "imports": {
    "lodash": "https://esm.sh/lodash/"
  }
}
```

```ts:import-mapを指定したブラウザ＆Deno
import _ from 'lodash'
```

**import-mapを利用すると、Node.jsのようなスタイルでのimportも可能**です。

つまり、

- ブラウザ互換のimport文が書ける
- 必要に応じてNode.jsスタイルのimport文も書ける

ということです。


### 誤解⑤：バージョン管理が面倒

Denoでは、モジュールのバージョン情報もURLで指定するようになっています。

```ts
import { assertEquals } from "https://deno.land/std@0.106.0/testing/asserts.ts";
                                                   ^^^^^^^^
```

モジュールのバージョンを上げたい時はどうすればいいでしょうか。ソースコード中のimportをすべて書き換えるのは骨が折れる作業です。

これを解決するには、JavaScriptの`export * from ...`という構文を利用します。これを使って、プロジェクト内のimport文を一箇所にまとめることが出来ます。

```ts:deps.ts
export * from "https://deno.land/std@0.106.0/testing/asserts.ts";
```

```ts:mod.ts
import { assertEquals } from "./deps.ts"
```

`export * from ...`構文を使って**外部モジュールのimportを一箇所に集約する**ことが、[公式ドキュメントでも推奨されています](https://deno.land/manual@v1.11.3/linking_to_external_code#it-seems-unwieldy-to-import-urls-everywhere)。

また、先ほど紹介したimport-mapも、同様にバージョンの一括指定に利用することが出来ます。

GitHub Actionsを利用して自動アップデートをかけることもできます。詳しくは[Deno版dependabot（UDD）で自動アップデート](https://qiita.com/access3151fq/items/fcf30b81a05af17dbe0e)を参照してください。


### 誤解⑥：ロックファイルが無い

Node.jsには`package.json`のほかに`package-lock.json`がありました。Denoには`package.json`が無いのでロックファイルも無いのでは？と思われるかもしれませんが、**実行時に`--lock`フラグを指定してロックファイルを利用できます**。
https://deno.land/manual/linking_to_external_code/integrity_checking#caching-and-lock-files

# npmにあってDenoに無いもの

「Denoにはnpmがない」の言葉通り、npmだけに存在する機能もあります。

### package.json

Denoには`package.json`がありません。
この理由は、URLからimportする時に、URLのパスを遡って`package.json`を読みに行くというのはパフォーマンスが悪い上に、webの挙動と非互換になってしまうからです。
では、バージョン情報などのメタデータはどうやって指定するのかというと、gitのtagで一括管理することが推奨されています。

なお、`tsconfig.json`や`import-map.json`、`deno.json`（設定ファイル）はDenoでも使用することができます。
これらはどれもプログラムのエントリポイントで指定されるもので、パッケージのメタデータを指定するために利用することはできません。
（deno.jsonはv1.14で登場）

### node_modules

Denoにはnode_modulesがありません。
npmは、「プロジェクト内のnode_modulesフォルダ」にモジュールをダウンロードします。
この挙動により、プロジェクト毎にパッケージがダウンロードされるため、node_modulesフォルダの肥大化が問題になっていました。（インストール先をパッケージ間で共有するように変更したpnpmも存在します。）

一方Denoは、「ローカルのキャッシュフォルダ」にモジュールをダウンロードします。これはpnpmと同じ方式です。ダウンロードされたモジュールは他のプロジェクトと共有されます。

### 中央集権的なモジュールレジストリ

Denoには中央集権的なモジュールレジストリがありません。

Node.jsの場合、npmレジストリが唯一のレジストリです。npmではサーバーが落ちたり、悪意のあるパッケージによってトークンが流出したりするなどの障害が発生していますが、それでもnpmを使い続けなければなりません。また、一度公開されたモジュールが削除されるなどの問題も発生しています。
対してDenoは、任意のレジストリを使用することができます。ユーザーは https://deno.land/x や https://esm.sh や https://www.skypack.dev/ などの中から好きなレジストリを選ぶことが出来ます。レジストリ間で資本主義的な競争原理が働くことで、エコシステム全体の耐障害性や安全性、永続性が向上する方向に働くことが狙いのようです。

# まとめ

### Denoにあるもの

- パッケージマネージャ
- 分散型のモジュールレジストリ
- ブラウザ互換のimport
- npmから利用可能な豊富なモジュール群
- URLの代わりにモジュール名を使用してimportする方法
- ロックファイル


### Denoにないもの

- 中央集権的なモジュールレジストリ
- モジュール管理のために必要な外部コマンド
- package.json
- node_modules

### 感想

Denoのモジュール管理方式に対しては、「import文にURLを使うの不便じゃないの？」が一番大きな疑問じゃないかと思います。
結論から言うと、とても便利です。私はローカルで雑用作業に使っている言語(実行環境)をPythonからDenoに切り替えましたが、「ローカルにどのバージョンのパッケージがインストールされているか」を考えなくて済むというのが大きいです。他にも、「URLをctrl-clickすればドキュメントに飛べる」というのも便利です。
最新のエディタ拡張機能を使っていれば未知のURLも自動で補完されるので、入力の面倒さは大分改善されています。

~~逆に不便に感じた事としては、~~

- ~~「`node_modules`内のファイルを弄って挙動を確認する」のような事が面倒~~
- ~~npm scriptに相当するものが無い~~

~~というのがあります。しかし、普通の使い方をする上では問題ないと思っています。~~

:::note
（追記）これらは全てDenoに機能追加されて解決されました。

- 「`node_modules`内のファイルを弄って挙動を確認する」は`deno vendor`コマンドで、
- 「npm scriptに相当するもの」は`deno task`コマンドで

それぞれ実行することができます！便利ですね～
:::

# 参考

- たぶんパッケージ管理ツールが必要です https://github.com/denoland/deno/issues/47
- モジュールのバージョン管理、依存関係の管理など https://github.com/denoland/deno/issues/4574
- パッケージの提案 https://github.com/denoland/deno/issues/288
- 『URLから直接モジュールをロードするのはとてもかわいいです(※)』 https://github.com/denoland/deno/issues/195

※ライアン・ダール(作者)が「Node.jsに関して後悔している10の事」という講演で、`index.js`のモジュール解決について
> index.htmlみたいでかわいいと思った。しかし、モジュール解決が不必要に複雑になってしまった。

と語った事のオマージュ？ということらしい。