npm パッケージのなかには、CLI ツールとしての機能を持っているものがある。
ESLint や Mocha、Jest などは、多くの人が使っていると思う。
この記事では、それらのパッケージがどのようにして CLI ツールとして機能しているのか、その仕組みについて説明する。
動作確認に使った npm のバージョンは6.14.5。Yarn は1.22.4。
package.json の bin フィールド
npm パッケージに CLI ツールとしての機能を持たせるためにはまず、package.jsonのbinフィールドでコマンド名とファイル名をマップさせる必要がある。
具体的な記述を見たほうが早いので、題材としてcowsayのv1.4.0をインストールする。
$ npm init -y $ npm i cowsay@1.4.0
node_modulesにcowsayがインストールされているので、そのなかのpackage.jsonを見てみる。
$ less node_modules/cowsay/package.json
binフィールドが、以下の内容になっている。cowsayとcowthinkというコマンド名に対して、cli.jsというファイルがマップされている。
"bin": { "cowsay": "cli.js", "cowthink": "cli.js" },
node_modules/cowsay/cli.jsを見てみると、冒頭に#!/usr/bin/env nodeという shebang が書かれているので、このファイルを直接実行すると Node.js で実行されることが分かる。
早速、引数にfoo(何でもよい)を渡して、実行してみる。
$ ./node_modules/cowsay/cli.js foo
_____
< foo >
-----
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
無事に実行できた。
次に、node_modules/.binを見てみる。
すると、cowsayとcowthinkというファイルが作成されている。
$ ls node_modules/.bin cowsay cowthink
そしてこの 2 つはどちらも、node_modules/cowsay/cli.jsのシンボリックリンクである。$ ls -lで確認できる。
$ ls -l node_modules/.bin total 0 lrwxr-xr-x 1 numb staff 16 6 2 15:03 cowsay -> ../cowsay/cli.js lrwxr-xr-x 1 numb staff 16 6 2 15:03 cowthink -> ../cowsay/cli.js
左端の文字がlのファイルは、シンボリックリンク。そして右端を見ると、どちらもnode_modules/cowsay/cli.jsが本体であることが分かる。
そのため、これらのファイルを実行すると、先程と同じ結果になる。
$ ./node_modules/.bin/cowsay foo
_____
< foo >
-----
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
そしてシンボリックリンクなので、本体であるnode_modules/cowsay/cli.jsを書き換えると、これらの動作も変化する。
試しに、node_modules/cowsay/cli.jsを以下の内容に書き換えてみる。
#!/usr/bin/env node
console.log('bar');
本体が変化すれば当然、シンボリックリンクの挙動も変わる。
$ ./node_modules/cowsay/cli.js bar $ ./node_modules/.bin/cowsay bar
一旦、ここまでの内容をまとめる。
package.jsonのbinフィールドが以下の内容になっている、abcという npm パッケージをインストールすると仮定する。
"bin": { "foo": "./fizz/buzz.js" },
すると、node_modules/.bin/fooというシンボリックリンクが作成される。
そして、このシンボリックリンクが指している本体は、node_modules/abc/fizz/buzz.jsである。
このファイルの shebang として`#!/usr/bin/env nodeを書いておくことで、Node.js で実行されるようになる。
ちなみにシンボリックリンクがどのディレクトリに作成されるかは、$ npm binや$ yarn binで確認できる。
また、既に同名のファイルがnode_modules/.binに存在する場合、上書きされてしまう模様。
上記のケースを例にすると、新しくインストールされた npm パッケージもbinフィールドでfooを定義していた場合、その内容でnode_modules/.bin/fooが上書きされる。
package.json の scripts フィールド
実際には、node_modules/.binに作成されるシンボリックリンクを直接実行することは稀で、多くの場合、package.jsonのscriptsフィールドのなかで使われる。
scriptsには任意のシェルスクリプトを定義することができ、そしてそれを$ npm runや$ yarn runで実行できる。
例えば"one": "echo 1"を定義すれば、$ npm run oneでそれを実行できる。
"scripts": { "one": "echo 1" },
$ npm run one 1
そして、これが最大の特徴だが、通常のパスに加えてnode_modules/.binがパスに追加される。
そのため、node_modules/.binのなかにあるシンボリックリンクを、その名前を書くだけで使用できるようになる。
以下の 2 つのscriptsフィールドは、どちらも同じ意味である。
"scripts": { "one": "cowsay 1!!" },
"scripts": { "one": "./node_modules/.bin/cowsay 1!!" },
どちらで書いても、$ npm run oneの結果は同じになる。
$ npm run one
_____
< 1!! >
-----
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
yarn run と npx
ここまでの内容は、npm と Yarn の両方に共通するものだった。どちらを使うかで挙動に差が出る、ということはない。
だが両者のrunコマンドには、違いも存在する。
まず、 yarn runコマンドには、npm runにはない機能がある。
yarn runでは、指定したスクリプトがscriptsフィールドのなかに存在しなかった場合に、node_modules/.bin/の中も検索し、該当するファイルがあればそれを実行する。
例えば、cowsay。
npm の場合、scriptsフィールドにcowsayが定義されていない場合、エラーになる。
$ npm run cowsay a npm ERR! missing script: cowsay
Yarn の場合、node_modules/.binにcowsayがあるので、それを実行する。
$ yarn run cowsay a
yarn run v1.22.4
___
< a >
---
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
✨ Done in 0.08s.
scriptsフィールドにcowsayがあれば、そちらを優先する。
"scripts": { "cowsay": "echo this is scripts field" },
$ yarn run cowsay yarn run v1.22.4 this is scripts field ✨ Done in 0.03s.
npm では、npxコマンドで同様のことができる。
$ npx cowsay a
___
< a >
---
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
しかもこれはrunとは別のコマンドなので、scriptsフィールドで名前が競合しているかどうかは、動作に影響を与えない。
scriptsフィールドにcowsayが定義されていてもいなくても、$ npx cowasy aは同じ結果を返す。
引数の渡し方
引数の渡し方も、npm runとyarn runでは異なる。
cowsayでは、オプションで目の表示を変えることができる。
$ ./node_modules/.bin/cowsay a -g
___
< a >
---
\ ^__^
\ ($$)\_______
(__)\ )\/\
||----w |
|| ||
以下のスクリプトが定義されているときに、このオプションを使おうとしたとする。
"scripts": { "cowsay": "cowsay a" },
yarn runでは、単純に-gオプションをつければよい。
$ yarn run cowsay -g
yarn run v1.22.4
$ cowsay a -g
___
< a >
---
\ ^__^
\ ($$)\_______
(__)\ )\/\
||----w |
|| ||
✨ Done in 0.07s.
だが同じことをnpm runで行っても、上手くいかない。
$ npm run cowsay -g
___
< a >
---
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
--で区切ってから、オプションを渡す必要がある。
$ npm run cowsay -- -g
___
< a >
---
\ ^__^
\ ($$)\_______
(__)\ )\/\
||----w |
|| ||