deno_graphでES Modulesの依存関係を分析する

deno_graphというモジュールを使って、ブラウザで使っているES Modulesの依存関係を分析してみたいと思います。

https://deno.land/x/deno_graph@0.22.0

（余談ですが、Node.jsやTypeScriptで動くnode_modulesからのimportもES Modulesと呼ばれているようです。ここではブラウザやDenoで動く、標準のES Modulesについて扱います。）

## deno_graphとは何か

deno_graphは、Denoの内部で依存関係分析に使われているRustライブラリです。
GitHubリポジトリを見ると分かりますが、主にDenoコアチームによって保守・管理されています。

https://deno.land/x/deno_graph では、そのRustライブラリをwasm化したものが配信されています。これを使うことで、コードからES Modulesの依存関係を解析することが可能です。

## deno_graphの使い方

公式ドキュメントは https://doc.deno.land/https://deno.land/x/deno_graph/mod.ts を参照してください。

まずは、`https://deno.land/x/deno_graph@0.22.0/mod.ts`から`createGraph`関数をimportします。

```ts
import { createGraph } from "https://deno.land/x/deno_graph@0.22.0/mod.ts";
```

importした`createGraph`関数を使って依存関係を分析していきます。ここでは、https://deno.land/std@0.125.0/streams/mod.ts の依存関係を分析してみたいと思います。

`await createGraph("<解析したいモジュールのURL>")`でモジュールグラフを作成します。

```ts
const graph = await createGraph("https://deno.land/std@0.125.0/streams/mod.ts");
```

`createGraph`関数の返り値は[ModuleGraphクラス](https://doc.deno.land/https://deno.land/x/deno_graph@0.22.0/mod.ts/~/ModuleGraph)です。

#### 依存関係を整形して出力する

`graph.toString()`でモジュールの依存関係を整形した文字列を取得できます。

```ts
console.log(graph.toString());
```

```:output
type: TypeScript
dependencies: 10 unique (total 67KB)

https://deno.land/std@0.125.0/streams/mod.ts (169B)
├─┬ https://deno.land/std@0.125.0/streams/conversion.ts (14.83KB)
│ └─┬ https://deno.land/std@0.125.0/io/buffer.ts (30.95KB)
│   ├── https://deno.land/std@0.125.0/_util/assert.ts (405B)
│   ├── https://deno.land/std@0.125.0/bytes/bytes_list.ts (3.95KB)
│   ├─┬ https://deno.land/std@0.125.0/bytes/mod.ts (4.47KB)
│   │ └── https://deno.land/std@0.125.0/bytes/equals.ts (1.43KB)
│   └── https://deno.land/std@0.125.0/io/types.d.ts (3.26KB)
├─┬ https://deno.land/std@0.125.0/streams/delimiter.ts (4.13KB)
│ └── https://deno.land/std@0.125.0/bytes/bytes_list.ts *
└─┬ https://deno.land/std@0.125.0/streams/merge.ts (1.96KB)
  └── https://deno.land/std@0.125.0/async/deferred.ts (1.45KB)
```

#### 依存関係一覧を取り出す

`graph.modules`で依存関係の一覧を取得できます。

```ts
for (const module of graph.modules) {
  console.log("URL: ", module.specifier);
  console.log("サイズ: ", module.size);
  console.log("種類: ", module.kind);
  console.log("メディアタイプ: ", module.mediaType);
  // console.log("ソースコード: ", module.source);
  console.log("型定義のみの依存関係かどうか: ", module.typesDependency);
  console.log();
}
```

```:output
URL:  https://deno.land/std@0.125.0/_util/assert.ts
サイズ:  405
種類:  esm
メディアタイプ:  TypeScript
型定義のみの依存関係かどうか:  undefined

URL:  https://deno.land/std@0.125.0/async/deferred.ts
サイズ:  1489
種類:  esm
メディアタイプ:  TypeScript
型定義のみの依存関係かどうか:  undefined

URL:  https://deno.land/std@0.125.0/bytes/bytes_list.ts
サイズ:  4048
種類:  esm
メディアタイプ:  TypeScript
型定義のみの依存関係かどうか:  undefined

URL:  https://deno.land/std@0.125.0/bytes/equals.ts
サイズ:  1467
種類:  esm
メディアタイプ:  TypeScript
型定義のみの依存関係かどうか:  undefined

URL:  https://deno.land/std@0.125.0/bytes/mod.ts
サイズ:  4573
種類:  esm
メディアタイプ:  TypeScript
型定義のみの依存関係かどうか:  undefined

URL:  https://deno.land/std@0.125.0/io/buffer.ts
サイズ:  31690
種類:  esm
メディアタイプ:  TypeScript
型定義のみの依存関係かどうか:  undefined

URL:  https://deno.land/std@0.125.0/io/types.d.ts
サイズ:  3339
種類:  esm
メディアタイプ:  Dts
型定義のみの依存関係かどうか:  undefined

URL:  https://deno.land/std@0.125.0/streams/conversion.ts
サイズ:  15187
種類:  esm
メディアタイプ:  TypeScript
型定義のみの依存関係かどうか:  undefined

URL:  https://deno.land/std@0.125.0/streams/delimiter.ts
サイズ:  4234
種類:  esm
メディアタイプ:  TypeScript
型定義のみの依存関係かどうか:  undefined

URL:  https://deno.land/std@0.125.0/streams/merge.ts
サイズ:  2011
種類:  esm
メディアタイプ:  TypeScript
型定義のみの依存関係かどうか:  undefined

URL:  https://deno.land/std@0.125.0/streams/mod.ts
サイズ:  169
種類:  esm
メディアタイプ:  TypeScript
型定義のみの依存関係かどうか:  undefined
```

#### 特定のモジュールの情報を取得する

`graph.get("<URL>")`で特定のモジュールの情報を取得できます。

```ts
const module = graph.get("https://deno.land/std@0.125.0/io/buffer.ts");
if (module) {
  console.log("URL: ", module.specifier);
  console.log("サイズ: ", module.size);
  console.log("種類: ", module.kind);
  console.log("メディアタイプ: ", module.mediaType);
  // console.log("ソースコード: ", module.source);
  console.log("型定義のみの依存関係かどうか: ", module.typesDependency);
}
```

```:output
URL:  https://deno.land/std@0.125.0/io/buffer.ts
サイズ:  31690
種類:  esm
メディアタイプ:  TypeScript
型定義のみの依存関係かどうか:  undefined
```



#### まとめてJSON化する

`JSON.stringify`すると、モジュールの依存関係がJSON形式の文字列で取得できます。

```ts
console.log(JSON.stringify(graph, null, 2));
```

```json:output
{
  "roots": [
    "https://deno.land/std@0.125.0/streams/mod.ts"
  ],
  "modules": [
    {
      "kind": "esm",
      "size": 405,
      "mediaType": "TypeScript",
      "specifier": "https://deno.land/std@0.125.0/_util/assert.ts"
    },
    {
      "kind": "esm",
      "size": 1489,
      "mediaType": "TypeScript",
      "specifier": "https://deno.land/std@0.125.0/async/deferred.ts"
    },
    {
      "kind": "esm",
      "size": 4048,
      "mediaType": "TypeScript",
      "specifier": "https://deno.land/std@0.125.0/bytes/bytes_list.ts"
    },
    {
      "kind": "esm",
      "size": 1467,
      "mediaType": "TypeScript",
      "specifier": "https://deno.land/std@0.125.0/bytes/equals.ts"
    },
    {
      "dependencies": [
        {
          "specifier": "./equals.ts",
          "code": {
            "specifier": "https://deno.land/std@0.125.0/bytes/equals.ts",
            "span": {
              "start": {
                "line": 180,
                "character": 23
              },
              "end": {
                "line": 180,
                "character": 36
              }
            }
          }
        }
      ],
      "kind": "esm",
      "size": 4573,
      "mediaType": "TypeScript",
      "specifier": "https://deno.land/std@0.125.0/bytes/mod.ts"
    },
    {
      "dependencies": [
        {
          "specifier": "../_util/assert.ts",
          "code": {
            "specifier": "https://deno.land/std@0.125.0/_util/assert.ts",
            "span": {
              "start": {
                "line": 1,
                "character": 23
              },
              "end": {
                "line": 1,
                "character": 43
              }
            }
          }
        },
        {
          "specifier": "../bytes/bytes_list.ts",
          "code": {
            "specifier": "https://deno.land/std@0.125.0/bytes/bytes_list.ts",
            "span": {
              "start": {
                "line": 2,
                "character": 26
              },
              "end": {
                "line": 2,
                "character": 50
              }
            }
          }
        },
...(以下略)
```

## まとめ

deno_graphの使い方の一部を紹介しました。
（実際は他の関数もあるのですが、使い方がよく分かりませんでした…）

勘のいい方はお気づきだと思いますが、これは実際には`deno info`コマンドの内部実装です。
元々は`Deno.info()`関数を導入して`deno info`コマンドをプログラム上から直接使用できるようにする事が検討されていました（[#10758](https://github.com/denoland/deno/pull/10758)）。
しかし、この関数をDeno組み込みのAPIにするよりは、wasm化して別の場所からリリースしたほうがバージョン管理しやすいという意見があり（[#11497](https://github.com/denoland/deno/discussions/11497)）、外部モジュールとしてリリースされました。
こういう経緯があって、deno_graphモジュールはDeno以外の環境からも利用可能な形態になっています。

`deno info`コマンドと`deno_graph`モジュールの使い分けですが、コマンドラインから依存関係分析を行いたい時は前者、プログラムから行いたい時は後者、という使い分けがいいのかなと思いました。