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 | || ||