1ファイルでコマンドとしても API としても使える npm モジュールを作る

お遊びとして…。

通常、npm モジュールを API として提供する場合は、module.exports で任意の関数等をエクスポートする。コレに対し、npm で CLI (コマンド) を提供する場合は、#!/usr/bin/env node という Shebang から始まるコマンド用の Node.js スクリプトを作る必要がある。

そういうワケで、大抵は lib.js なんかに本体を書き、module.exports するだけの index.js と、CLI を提供する bin.js という合計3ファイルを用意することが多い。package.json の宣言で言えば以下のようになっている状態だ。

{
  "main": "index.js",
  "bin": "bin.js",
  // 中略…
}

しかし、本質は lib.js にまとまっていて、index.jsbin.js は呼び出しだけの些細なファイルであるなら、なんとか3つまとめられないだろうか、というのが今回の主旨。実用性抜きのお遊びである。

こう書いたらできた

いきなりだが結論。

以下のようなコード構成にすれば、この1ファイルを require() もできるし、このファイルをコマンドとして実行もできた。

#!/usr/bin/env node
// ↑1行目には Shebang を書いておく

// 本質となる関数
function myFunc() {
  console.log('my-func!');
}

// npm-scripts よりコマンドで実行された時はココがコマンド名になる
if(process.env.npm_lifecycle_script === 'my-func') {
  // コマンド実行時はコマンド関数を実行する
  myFunc();
}
else {
  // そうでない場合は別のスクリプトから require() された場合なので、必要な関数をエクスポートする
  module.exports = myFunc;
}

コレを index.js として保存し、package.json は以下のようにする。

今回は1ファイルで両方の用途に対応したので、どちらも index.js を指定する。

{
  "name": "my-func",
  "version": "1.0.0",
  "main": "index.js",
  "bin": "index.js",
  // 後略…
}

あとはコレを npm publish で提供すれば良い。ローカルで試したい時は npm link でグローバル npm モジュールとしてシンボリックリンクを張れば良い。

使用する側は、API として利用する際は以下のように使える。

// use.js とする : require() して実行できる
const myFunc = require('my-func');
myFunc();
# このようにして実行可能
$ node use.js

CLI として使用する際は、ローカルインストールして package.json の npm-scripts でパスを通してやれば、コマンドとして使える。

{
  "name": "use-my-func",
  "version": "1.0.0",
  "scripts": {
    "my-func": "my-func"
  },
  "dependencies": {
    "my-func": "1.0.0"
  },
  // 後略…
}
$ npm run my-func
> my-func

my-func!
# コマンドとして実行できる

こんな感じ。

ポイント

1つのファイルの中で、「require() によって読み込まれたのか」「コマンドとして実行されているのか」を判別する必要があり、その方法を何とか探した。

ポイントになったのは、process.env.npm_lifecycle_script というグローバル変数。コマンドとして対象のファイルを実行した場合は、このプロパティの値がコマンド名になるのだ。それ以外のファイルから require() した時は、ココが node use.js といった形で呼び出し時の名前になる。

というワケで、process.env.npm_lifecycle_script の値がコマンド名なら、そのまま本体の関数を実行して終われば良い。一方、そうでなければ、関数はその場で実行せず、module.exports に本体の関数を渡すようにし、呼び出し元で使えるようにしてやる。

1行目に Shebang がないとコマンドとしてうまく実行できなかったので書いておいた。require() した時は無視されるようなのでそのまま書いておいた。

#!/usr/bin/env node

function myFunc() {
  // 本体
}

if(process.env.npm_lifecycle_script === 'my-func') {
  myFunc();
}
else {
  module.exports = myFunc;
}

そういうワケで、コレが最小構成。他に「require() なのかコマンド実行なのか」を判別する良い判別方法があれば教えて欲しい。

実際にパッケージ公開してみた

こうして作ったパッケージを実際に公開してみた。@neos21/req-cmd でインストールできるので、require('@neos21/req-cmd') してみたり、req-cmd コマンドとして利用してみたりして、試してみてほしい。