JavaScript
Node.js
npm

自作パッケージの実行ファイルをnpm installした際にnode_modules/.bin/に登録する

TL;DR

  • package.jsonの中でbinとして登録しておくと、npm installのときにnode_modules/.binの配下に勝手に実行ファイルへのリンクが追加される。
  • package.jsonのbinには複数設定できる。
  • 複数設定してもいいけど、個人的にはハブとなる実行ファイル1つを登録して、振り分ける方法にした。

はじめに

npmを使ってinstallをおこなうと、node_modules/.binの中に実行ファイルのリンクが貼られます。
そういえば、自分の作ったパッケージでこれを実現する方法を知らなかったな、というわけで調べました。

最終的なプロジェクトの構成図は以下のようになっています。

プロジェクトA(実行ファイルを作成して、npm publishするパッケージ)
ma2-cmd.png

プロジェクトB(プロジェクトAをpackage.jsonのdependenciesに追加してインストールし、動作検証するパッケージ)
ma2-cmd-runner.png

npm installした時にnode_modules/.bin/ディレクトリに実行ファイルを登録

npm package.json 日本語版 取扱説明書にやり方が書いてありました。

例えば npm ではこう指定されています:
{ "bin" : { "npm" : "./cli.js" } }

これで解決ですね。
あとは、個人的にスクリプト系を一つのパッケージにまとめておきたかったので、package.jsonのbinに複数指定できるのか試してみました。

package.json
"bin": {
    "hoge": "src/hoge.js",
    "fuga": "bin/fuga.js"
}

各jsファイルは以下のように適当です。

src/hoge.js
#!/usr/bin/env node
console.log("Hello");
bin/fuga.js
#!/usr/bin/env node
console.log("World");

配置確認

作業手順
1. プロジェクトAをnpm publish
2. プロジェクトBを新規作成
3. プロジェクトBのpackage.jsonに、プロジェクトAのpackage.jsonのnameで指定した名前をdependenciesに追加
4. プロジェクトBでnpm installして、node_modules/.bin/配下にhogefugaが入っていれば完了

以上のことから、次の2点がわかりました。

  1. 実行ファイルは複数指定できる
  2. パスがあっていれば別にsrcディレクトリだろうが、binディレクトリだろうが関係なし

ちなみに私はverdaccioを使ってプライベートのリポジトリを構築した状態で試したので、ゴミのような履歴が残るとか気にせず何度もnpm publishしながら試しました。
npmのアカウントを作って、npm publishをすると公開されるので、それが嫌な方はご注意を。

実行ファイルの実行確認

$ npm install
$ node_modules/.bin/hoge
Hello

はい、動きました。

実行ファイルを1つにしたい

個人的にはhogeとかfugaとか名前もかぶりそうですし、グローバルにインストールするのは難しいなと感じました。
解決策として、名前がかぶらなさそうな実行ファイル名をつけて、その引数としてhogeとfugaを指定する方法を取ることにしました。

何はともあれ、起点となるjsファイルとして、プロジェクトAにma2-cmd.jsを新規作成します。

ma2-cmd.js
#!/usr/bin/env node
const childProcess = require("child_process");
const execSync = childProcess.execSync;

const cmdPattern = 
    "  hoge || h\n" +
    "  fuga || f\n";

const argv = process.argv.slice(2);
if (argv == null || argv.length < 1) {
    throw new Error("実行するコマンドを指定してください。\n" + cmdPattern);
}

const map = {
    "hoge": "src/hoge.js",
    "h": "src/hoge.js",
    "fuga": "bin/fuga.js",
    "f": "bin/fuga.js"
};

const cmdTarget = map[argv[0]];
if (cmdTarget == null) {
    throw new Error("指定されたコマンド名に紐づくコマンドがありません。\n" + cmdPattern);
}

let args = "";
if (argv.length > 1) {
    args = " " + argv.slice(1)
        .join(" ");
}

const res = execSync("node node_modules/${プロジェクト名}/" + cmdTarget + args).toString();
console.log(res);

package.jsonも修正します。

package.json
"bin":{
    "ma2-cmd": "ma2.js"
}

あとは同じようにpublishして更新します。

  1. プロジェクトAのpackage.jsonのバージョンをあげる。(npm version patchとかみたいにコマンドでもいいし、手動であげてもいいです)
  2. プロジェクトAでnpm publish
  3. プロジェクトBでnpm update

そして、コマンドを打ちます。

$ node_modules/.bin/ma2-cmd hoge
Hello

おまけ

コマンドの短縮

以下のように指定したことで、コマンドを短縮して実行できるようにしています。

ma2.js
const map = {
    "hoge": "src/hoge.js",
    "h": "src/hoge.js",
    "fuga": "bin/fuga.js",
    "f": "bin/fuga.js"
};

つまり、以下のコマンドは同じファイルを実行します。

$ node_modules/.bin/ma2 hoge
$ node_modules/.bin/ma2 h

各コマンドに対する引数の設定

今回のサンプルコードでは使っていませんが、引数を渡せるようにしています。

抜粋すると、このあたりです。

ma2-cmd.js
:
const argv = process.argv.slice(2);
:
const cmdTarget = map[argv[0]];
:
let args = "";
if (argv.length > 1) {
    args = " " + argv.slice(1)
        .join(" ");
}


const res = execSync("node node_modules/${プロジェクト名}/" + cmdTarget + args).toString();
console.log(res);

jsファイルの先頭行のnode宣言

先頭行の宣言、最初必要だと知りませんでした。

#!/usr/bin/env node

他の.binに入っている先人の知恵を物色したところ発見し、調べたらnodeを使うという宣言であると知りました。
参考:Node.jsを使ってCommand line ツールことはじめ

ちなみに、先頭に指定しなかった場合はエラーが出ます。

$ node_modules/.bin/ma2-cmd hoge

node_modules/.bin/ma2: line 1: syntax error near unexpected token `('
node_modules/.bin/ma2: line 1: `const childProcess = require("child_process");`

先頭にnodeを付けると動きます。

$ node node_modules/.bin/ma2-cmd hoge
Hello