🦕

Denoでnpmモジュールを実行してみよう - Zenn CLI編

2023/12/02に公開

Deno Advent Calendar 2023 1日目の記事です。

動機

僕はGitHub上のプライベートリポジトリでZennの記事を管理しており、記事のscaffold作成やプレビューなどはZenn CLIを利用しています。Zenn CLIはNode.js製なので当然Node.jsが必要です。ドキュメントに書いてある通り、以下のようにすればCLIのインストールおよびディレクトリ内のセットアップが完了します。とても簡単です。

$ npm init --yes
$ npm install zenn-cli
$ npx zenn init

今回Advent Calendarの記事を書くにあたり、久しぶりにリポジトリを開き、以前と同じように npx zenn new:article で記事のscaffoldを作成しようとしましたが、ここでふと、「Denoでも普通に動くのでは?」と疑問に思いました。
最近のDenoはNode.js互換性に非常に注力しており、バージョンアップのたびに互換性が向上しています。Zenn CLIというZennユーザーのみなさんにとって身近な例を使って、DenoのNode.js互換性について紹介できればと思います。

実行環境

この記事では以下のバージョンのDenoを利用します。

$ deno --version
deno 1.38.4 (release, aarch64-apple-darwin)
v8 12.0.267.1
typescript 5.2.2

Denoをまだインストールしていない方は
https://docs.deno.com/runtime/manual/getting_started/installation
を参照してください。

やってみる

Denoでnpmにあるモジュールを実行するのは非常に簡単です。Zenn CLIの場合はzenn-cliという名前で公開されているので、たとえば以下のコマンドによって npx zenn --help と同等の結果を得ることができます。

$ deno run npm:zenn-cli@0.1.150 --help

さて、実行してみると、以下のようにプロンプトが表示されます。

first prompt when running zenn-cli with Deno

これがNode.js製のCLIツールをDenoで実行することの利点の1つ目です。Denoはファイル読み込み、ファイル書き込み、環境変数へのアクセス、ネットワークアクセス、などをデフォルトでは制限しています。つまり、明示的に許可を与えない限りは、プログラムがこれらの操作を行うことはできないということです。
上記の画像では、「プログラムが <main_module>[1] への読み取りアクセスを求めているけど、許可してもいい?」と聞かれているということになります。許可して大丈夫であれば y を入力します。

その後も別のファイルに対する読み取り権限や HOME 環境変数に対するアクセスなどが要求されます。いくつか許可していくと、以下のプロンプトが現れます。

full env access is requested

これはすべての環境変数に対するアクセスを要求しているということです。環境変数には機密情報が含まれていることもあると思います。どうしてすべての環境変数にアクセスする必要があるのか?気になるところです。いったん n を入力して、このアクセスを拒否してみます。

deny the request

スタックトレースを見ると、プログラム中のどの部分が環境変数にアクセスしようとしていたのかがわかります。ただ今回のケースでは、どうやらZenn CLIはwebpackを利用してバンドルされているらしく、人間が読むのは厳しい形式になってしまっています。とりあえずフォーマッタをかけた上で、スタックトレースに出現している Object.54662 というやつを探してみると、以下の部分がヒットしました。

54662: (e, t, n) => {
  var a = n(76224), r = n(73837);
    // 省略
    t.inspectOpts = Object.keys(process.env).filter(function (e) {
      return /^debug_/i.test(e);
    }).reduce(function (e, t) {
      var n = t.substring(6).toLowerCase().replace(
          /_([a-z])/g,
          function (e, t) {
            return t.toUpperCase();
          },
        ),
        a = process.env[t];
      return a = !!/^(yes|on|true|enabled)$/i.test(a) ||
        !/^(no|off|false|disabled)$/i.test(a) &&
          ("null" === a ? null : Number(a)),
        e[n] = a,
        e;
    }, {});
    // 省略
},

Object.keys(process.env) がすべての環境変数にアクセスしようとしている箇所のように見えます。後続のコードをじっと睨んでみると、どうやら実際には DEBUG_ から始まる環境変数のみを見ていて、その値が yes とか true とかになっているかをチェックしているような感じに読めます。
で、実はこのコード片には個人的に少し心当たりがあり、debugというnpmモジュールに由来するものだと思われます。以下が該当する箇所のコードです。

https://github.com/debug-js/debug/blob/f66cb2d9f729e1a592e72d3698e3b75329d75a25/src/node.js#L124-L149

結論としては、この環境変数アクセスは問題なさそうであるということがわかりました。もう一度 deno run して、さきほど拒否した環境変数アクセスを許可してみます。

finally help command succeeded

無事にヘルプコマンドの結果が表示されました。Deno特有のパーミッションシステムが絡んでくる以外は、Node.jsで実行した場合と同じ結果が得られました。

コマンドの実行のたびに毎回 y を何度も入力するのは疲れます。いったん一通りの検査が終わったので、次回以降は --allow-all (または -A)オプションを付けて実行することで、すべてのパーミッションを付与することができます。あるいは、面倒でなければ以下のように個別のパーミッションを指定するほうがより好ましいです。

$ deno run --allow-read --allow-env --allow-write npm:zenn-cli@0.1.150 --help

さらに先に行きたい方は、より詳細に、どのファイルへのアクセスを許可するのか、などの粒度で指定することも可能です。詳しくは

https://docs.deno.com/runtime/manual/basics/permissions#permissions-list

を参照してください。

Denoで実行することによるもう1つの利点は、Node.jsのときにやるような npm init --yesnpm install zenn-cli のステップが必要なく、またワーキングリポジトリ内に node_modules ディレクトリが作成されることもないということです。ディレクトリを作って、以下のコマンドを実行するだけで、記事を書き始める準備がすぐに完了します。

deno run --allow-all npm:zenn-cli@0.1.150 init

やってみた結果

特に何の問題もなくDenoでZenn CLIを実行することができました。すべてのサブコマンドを確認したわけではないですが、少なくとも以下のコマンドは問題なく動くことが確認できました。

  • init
  • new:article
  • preview

パーミッションシステムを通してどのような操作が行われているのかを確認することで、未知のプログラムでも比較的安心して実行できることもわかりました。

おわり

よくわからないNode.js製のCLIツールを実行するときは、ぜひDenoを試してみてください。
Zenn CLI編としたので、もしかしたら近いうちに別のCLIツールを実行してみたときの様子もご紹介するかもしれません。

脚注
  1. 今実行されているプログラムのエントリーポイントのこと。例えば deno run foo.ts と実行した場合は foo.ts のこと ↩︎

Discussion