TL;DR
- package.jsonの中でbinとして登録しておくと、
npm install
のときにnode_modules/.binの配下に勝手に実行ファイルへのリンクが追加される。 - package.jsonのbinには複数設定できる。
- 複数設定してもいいけど、個人的にはハブとなる実行ファイル1つを登録して、振り分ける方法にした。
はじめに
npmを使ってinstallをおこなうと、node_modules/.binの中に実行ファイルのリンクが貼られます。
そういえば、自分の作ったパッケージでこれを実現する方法を知らなかったな、というわけで調べました。
最終的なプロジェクトの構成図は以下のようになっています。
プロジェクトA(実行ファイルを作成して、npm publish
するパッケージ)
プロジェクトB(プロジェクトAをpackage.jsonのdependenciesに追加してインストールし、動作検証するパッケージ)
npm install
した時にnode_modules/.bin/ディレクトリに実行ファイルを登録
npm package.json 日本語版 取扱説明書にやり方が書いてありました。
例えば npm ではこう指定されています:
{ "bin" : { "npm" : "./cli.js" } }
これで解決ですね。
あとは、個人的にスクリプト系を一つのパッケージにまとめておきたかったので、package.jsonのbinに複数指定できるのか試してみました。
"bin": {
"hoge": "src/hoge.js",
"fuga": "bin/fuga.js"
}
各jsファイルは以下のように適当です。
# !/usr/bin/env node
console.log("Hello");
# !/usr/bin/env node
console.log("World");
配置確認
作業手順
- プロジェクトAを
npm publish
- プロジェクトBを新規作成
- プロジェクトBのpackage.jsonに、プロジェクトAのpackage.jsonのnameで指定した名前をdependenciesに追加
- プロジェクトBで
npm install
して、node_modules/.bin/配下にhoge
とfuga
が入っていれば完了
以上のことから、次の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
を新規作成します。
# !/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も修正します。
"bin":{
"ma2-cmd": "ma2.js"
}
あとは同じようにpublishして更新します。
- プロジェクトAのpackage.jsonのバージョンをあげる。(
npm version patch
とかみたいにコマンドでもいいし、手動であげてもいいです) - プロジェクトAで
npm publish
- プロジェクトBで
npm update
そして、コマンドを打ちます。
$ node_modules/.bin/ma2-cmd hoge
Hello
おまけ
コマンドの短縮
以下のように指定したことで、コマンドを短縮して実行できるようにしています。
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
各コマンドに対する引数の設定
今回のサンプルコードでは使っていませんが、引数を渡せるようにしています。
抜粋すると、このあたりです。
:
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