30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

npm link で OSS 活動の効率を上げる

npm にはlinkというコマンドが用意されており、これを使うことで npm パッケージの開発効率が上がる。
既存のパッケージに手を加えた際の動作確認にも使えるので、OSS 活動の効率も上がる。
この記事では、npm linkの仕組みと、それをどのように利用できるのかについて説明する。

動作確認に使った npm のバージョンは6.14.5
Node.js のバージョンは12.17.0。これ以前のバージョンだと以下の動作確認でエラーが出るので注意。

サンプル用のパッケージとアプリを作る

まずはパッケージを作成する。
my-package-dirというディレクトリを作り、そこに以下の内容のpackage.jsonを作成する。

{
  "name": "my-package",
  "type": "module",
  "main": "main.js"
}

そして、以下の内容のmain.jsを作る。

export default (arg1, arg2) => arg1 + arg2;

これで、my-packageをパッケージとして公開すると、main.jsexportしている関数を使えるようになる。
だが、公開する前にローカル環境で確認したい。こういうケースでnpm linkが使える。

取り敢えず、確認用のアプリを作る。
my-package-dirとは別にmy-app-dirを作り、そこに移動。
以下のpackage.jsonapp.jsを作る。

{
  "name": "my-app",
  "type": "module"
}
import myPackage from 'my-package';

console.log(myPackage(2, 3));

この状態で$ node app.jsを実行すると、my-packageをインストールしていないので当然エラーになる。

$ node app.js
(node:19472) ExperimentalWarning: The ESM module loader is experimental.
internal/modules/run_main.js:54
    internalBinding('errors').triggerUncaughtException(
                              ^

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'my-package' imported from /Users/numb/my-app-dir/app.js

npm link でシンボリックリンクを作成する

ここから、npm linkを使う。

まずはmy-package-dirに移動。そこでnpm linkを実行する。
そうすると、シンボリックリンクが作成されたことが分かる。

$ npm link

/Users/numb/.ndenv/versions/v12.17.0/lib/node_modules/my-package -> /Users/numb/my-package-dir

/Users/numb/.ndenv/versions/v12.17.0の部分は、環境によって異なる。これ以降は{prefix}と表記する。

{prefix}/lib/node_modules/を見てみると、確かにmy-packageが作られている。

$ ls -l {prefix}/lib/node_modules
total 0
lrwxr-xr-x   1 numb  staff   26  6  5 00:48 my-package -> /Users/numb/my-package-dir
drwxr-xr-x  24 numb  staff  768  6  2 14:46 npm

{prefix}/lib/node_modules/は、グローバルインストールしたパッケージがインストールされる場所。
つまり、npm パッケージをグローバルインストールすると、ここに格納される。

$ npm i -g cowsay

$ ls -l {prefix}/lib/node_modules
total 0
drwxr-xr-x  11 numb  staff  352  6  5 00:54 cowsay
lrwxr-xr-x   1 numb  staff   26  6  5 00:48 my-package -> /Users/numb/my-package-dir
drwxr-xr-x  24 numb  staff  768  6  2 14:46 npm

$ npm un -g cowsay

$ ls -l {prefix}/lib/node_modules
total 0
lrwxr-xr-x   1 numb  staff   26  6  5 00:48 my-package -> /Users/numb/my-package-dir
drwxr-xr-x  24 numb  staff  768  6  2 14:46 npm

そして、{prefix}/lib/node_modules/my-packagemy-package-dirのシンボリックリンクなので、同じものを指す。

$ ls /Users/numb/.ndenv/versions/v12.17.0/lib/node_modules/my-package
main.js         package-lock.json   package.json

$ ls /Users/numb/my-package-dir
main.js         package-lock.json   package.json

シンボリックリンクの名前はディレクトリ名ではなくpackage.jsonnameの値なので、注意する。

npm link {パッケージ名} でパッケージをインストールする

これで、my-packageをインストールする準備が整った。
my-app-dirに移動し、そこで$ npm link my-packageを実行する。

$ npm link my-package
/Users/numb/my-app-dir/node_modules/my-package -> /Users/numb/.ndenv/versions/v12.17.0/lib/node_modules/my-package -> /Users/numb/my-package-dir

そうすると、node_modulesmy-packageというファイルが作られる。
これは先程作成された{prefix}/lib/node_modules/my-packageにリンクされている。そして先程確認したように、このファイルはmy-package-dirにリンクされている。
これにより、my-app-dir/node_modulesmy-package-dirをインストールしたのと同じ効果を得られるのである。

確認のため、app.jsを実行してみる。

$ node app.js
5

無事に動いていることを確認できた。

そして、node_modules/my-packageはシンボリックリンクなので、my-package-dirと常に同期している。my-package-dirの変更はリアルタイムに反映される。

例えば、my-package-dir/main.jsexportしている関数を変更して、引数を 3 つ取るようにする。

export default (arg1, arg2, arg3) => arg1 + arg2 + arg3;

そしてmy-app-dir/main.jsを以下のように書き換える。

import myPackage from 'my-package';

console.log(myPackage(2, 3, 4));

そうすると、my-package-dir/main.jsの変更が反映されていることが分かる。

$ node app.js
9

このように、パッケージに手を加えてもその都度インストールし直す必要はない。そのため、クローンしてきた既存のパッケージに手を加えて試行錯誤する作業も、やりやすくなる。

CLI ツールの動作確認を行う

npm linkは、CLI 機能の動作確認にも役に立つ。
npm linkによって、CLI 機能をローカルで試せるようになる。

npm パッケージの CLI 機能の基本については、以下の記事に書いた。

numb86-tech.hatenablog.com

my-packageに CLI 機能を追加して、それをローカルで試す。

まず、my-package-dir/hello.jsを作成する。

#!/usr/bin/env node

console.log('Hello!!!');

そして、my-package-dir/package.jsonを、以下の内容に書き換える。

{
  "name": "my-package",
  "type": "module",
  "main": "main.js",
  "bin": {
    "hello": "hello.js"
  }
}

この状態で、my-package-dir$ npm linkを実行する。

$ npm link

{prefix}/bin/hello -> {prefix}/lib/node_modules/my-package/hello.js
{prefix}/lib/node_modules/my-package -> /Users/numb/my-package-dir

シンボリックリンクが 2 つ作られた。後者は、最初に$ npm linkしたときと同じもの。
それに加えて、{prefix}/bin/helloが作られた。

まず、{prefix}/bin/について説明しておく。
グローバルインストールした npm パッケージに CLI 機能がついていた場合、そのコマンドのシンボリックリンクがここに作成されていく。

また、最初からnpmnpxというシンボリックリンクが作成されており、これにより、$ npm$ npxというコマンドを利用できるようになっている。

$ ls -l /Users/numb/.ndenv/versions/v12.17.0/bin/
total 92424
lrwxr-xr-x  1 numb  staff        39  6  5 01:26 hello -> ../lib/node_modules/my-package/hello.js
-rwxr-xr-x  1 numb  staff  47320288  5 30 04:04 node
lrwxr-xr-x  1 numb  staff        38  6  2 14:46 npm -> ../lib/node_modules/npm/bin/npm-cli.js
lrwxr-xr-x  1 numb  staff        38  6  2 14:46 npx -> ../lib/node_modules/npm/bin/npx-cli.js

同じ要領で、helloコマンドも利用できるようになった。
ただ、筆者のようにndenvを使っている場合は、$ ndenv rehashを行う必要がある。

$ hello
-bash: hello: コマンドが見つかりません

$ ndenv rehash

$ hello
Hello!!!

無事に実行できた。

グローバルインストールされたのと同じ状態なので、どのディレクトリからも利用できる。
そして、先程と同じようにシンボリックリンクなので、my-package-dir/hello.jsの変更は同期されている。

#!/usr/bin/env node

console.log('Cowabunga!!!');
$ hello
Cowabunga!!!

npm unlink でシンボリックリンクを削除する

動作確認が終わったら、npm unlinkでシンボリックリンクを削除できる。

まず、my-app-dir/node_modulesにシンボリックリンクがあるので、これを削除する。

$ ls node_modules/
my-package

my-app-dir$ npm unlink my-packageを実行すれば削除される。

次に、{prefix}/lib/node_modules{prefix}/binに作成されているシンボリックリンクを削除する。

$ ls {prefix}/lib/node_modules/
my-package  npm
$ ls {prefix}/bin
hello   node    npm npx

my-package-dir$ npm unlinkを実行すると、これが削除される。

$ ls {prefix}/lib/node_modules/
npm
$ ls {prefix}/bin
node    npm npx

先に{prefix}/lib/node_modules/my-package{prefix}/bin/helloを削除してしまうと、my-app-dir/node_modules/my-packageを削除できない。
このファイルのリンク先である{prefix}/lib/node_modules/my-packageを先に削除してしまったために、上手く動作しないのだと思われる。

参考資料

npm パッケージを CLI ツールとして機能させる仕組みについて

npm パッケージのなかには、CLI ツールとしての機能を持っているものがある。
ESLint や Mocha、Jest などは、多くの人が使っていると思う。
この記事では、それらのパッケージがどのようにして CLI ツールとして機能しているのか、その仕組みについて説明する。

動作確認に使った npm のバージョンは6.14.5。Yarn は1.22.4

package.json の bin フィールド

npm パッケージに CLI ツールとしての機能を持たせるためにはまず、package.jsonbinフィールドでコマンド名とファイル名をマップさせる必要がある。

具体的な記述を見たほうが早いので、題材としてcowsayv1.4.0をインストールする。

$ npm init -y
$ npm i cowsay@1.4.0

node_modulescowsayがインストールされているので、そのなかのpackage.jsonを見てみる。

$ less node_modules/cowsay/package.json

binフィールドが、以下の内容になっている。cowsaycowthinkというコマンド名に対して、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を見てみる。
すると、cowsaycowthinkというファイルが作成されている。

$ 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.jsonbinフィールドが以下の内容になっている、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.jsonscriptsフィールドのなかで使われる。

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/.bincowsayがあるので、それを実行する。

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

参考資料