JSエコシステムぶらり探訪(4): npmとコマンドライン

前回に続きnpmの機能について扱います。今回はnpmとコマンドラインツールとの関わりを中心に見ていきます。

[←前](https://qiita.com/qnighy/items/677bf64063c666b21034) [目次](https://qiita.com/qnighy/items/16fdd8e58309a1f706a0) [次→](https://qiita.com/qnighy/items/3d2496e9cf48f16cee05)

注意: Windowsとそれ以外では、npmのフォルダ配置は異なります。Windowsでの挙動については[npm-folders](https://docs.npmjs.com/files/folders)を参照してください。

## グローバルインストール

npmは通常、Node.jsの配布物に同梱されていますが、yarnは同梱されていません。yarnを使う場合は次のようなコマンドを実行します。

```
npm install -g yarn
```

これによって以下のような効果が発生します。

- `$PREFIX/lib/node_modules/yarn` 以下にyarnの中身がインストールされる。
- `$PREFIX/lib/node_modules/yarn/node_modules` 以下にyarnの依存関係がインストールされる。
- `$PREFIX/bin` にシンボリックリンク `yarn`, `yarnpkg` が生成される。どちらも `../lib/node_modules/yarn/bin/yarn.js` を参照している。
  - これはyarnの `package.json` の "bin" フィールドの記述に基づいて生成される

`$PREFIX/bin` にはNode.jsの実行バイナリ (`node`) も入っているので、Node.jsが使えている時点でここにPATHは通っていると仮定してよさそうです。

`$PREFIX/lib/node_modules/yarn/bin/yarn.js` は実行可能属性が付与されていて、冒頭は以下のようになっています。[^shebang]

[^shebang]: [`#!` がOKな理由についてはこちらの記事](https://zenn.dev/qnighy/articles/d2712de8e4585f749b30)を参照。

```javascript
#!/usr/bin/env node

/* eslint-disable no-var */
/* eslint-disable flowtype/require-valid-file-annotation */
'use strict';
```

つまり、nodeにパスが通った状態で `yarn` を実行すると `node $PREFIX/bin/yarn` が実行されます。Node.jsはモジュール解決前にシンボリックリンクを解決する[^symlink-resolution]ので、これは `node $PREFIX/lib/node_modules/yarn/bin/yarn.js` と同じ意味になります。 (`yarn.js` を起点に `require` が解決される。)

[^symlink-resolution]: シンボリックリンクの解決については[第3回を参照](https://qiita.com/qnighy/items/677bf64063c666b21034#%E3%82%B7%E3%83%B3%E3%83%9C%E3%83%AA%E3%83%83%E3%82%AF%E3%83%AA%E3%83%B3%E3%82%AF%E3%81%AE%E8%A7%A3%E6%B1%BA)。

`npm install -g` のインストール先は `$PREFIX/lib/node_modules` であって **`$PREFIX/lib/node` ではありません**。Node.jsの探索ルールに含まれているのは前者ではなく後者です[^nodejs-non-relative-resolution]から、これによってライブラリをインストールしても `require` から使われることはありません。

[^nodejs-non-relative-resolution]: Node.jsの探索ルールについては[第3回を参照](https://qiita.com/qnighy/items/677bf64063c666b21034#%E3%83%91%E3%82%B9%E3%81%8B%E3%82%89%E3%81%AE%E7%99%BA%E8%A6%8B)。

### yarnの場合

- `npm install -g` のかわりに `yarn global add` を使います。
- インストール先はnpmとは異なり、バイナリは `~/.yarn/bin` に、パッケージ本体は `~/.config/yarn/global` にそれぞれインストールされます。そのため、**npmと違い、node用のパスとは別にPATHを通しておく必要があります。**
- また、上記のインストール先はyarnでは変更可能です。

## npm installの古い挙動

`-g` をつけない場合、 `npm install` はローカルインストールの挙動になります。

npm v4までは、npm installはデフォルトでは `./node_modules` への展開のみを行い、 `package.json` を更新しませんでした。そのため、 `npm install -S` と書くのが一般的でした。npm v5以降では `-S` が自動的に仮定されます。

## アプリケーションのローカルインストール, yarn exec, npx

現代のJavaScript開発ではprettier, eslint, typescript (tsc), webpackなど多くのCLIツールを使います。これらはグローバルインストールすることもできますが、以下のような懸念があります。

- これらのツールのバージョンが開発者ごとにバラバラだと、再現性の低いトラブルに遭遇しやすくなる。
- eslintやwebpackはプラグインも含めて使うことが多く、これらを含めた全てのパッケージを個別にインストールさせるのはセットアップの手間につながる。

このため、プロジェクトで使うアプリケーションは `package.json` に指定してローカルインストールほうが主流になっています。方法は簡単で、**ライブラリと同様に `npm install (-S)` または `yarn add` するだけ**です。

グローバルインストール時には `$PREFIX/bin` 以下にシンボリックリンクが作成されますが、ローカルインストールの場合は同様に `./node_modules/.bin` 以下にシンボリックリンクが作成されます。通常このディレクトリにはPATHは通っておらず、 **ローカルインストールされたアプリケーションは、通常のコマンドと同様に呼び出すことはできない**ため、npm/yarnを経由して使います。以下のコマンドを経由して使われるのが一般的でしょう。

- `npx` (`npm exec`) / `yarn exec`
- `npm run` / `npm run-scripts` / `yarn run` (`run` が省略された場合も含む)

### yarn exec

`yarn exec` はyarn共通のセットアップを行ったあと所与のコマンドを実行します。このセットアップには `PATH` の設定が含まれているため、ローカルインストールされたアプリケーションの実行が可能です。

```
yarn exec prettier -w 'src/**/*.js'
```

### npx / `npm exec`

`npx` / `npm exec` も `yarn exec` と同様の目的で使用することができますが、ローカルインストールされたパッケージがない場合は自動的にnpmレジストリからパッケージを探し、一時的にインストールしてから実行します。

```
npx prettier -w 'src/**/*.js'
```

`prettier` がない状態で上記を実行すると、一時的なインストールが行われます。 `~/.npm/_npx/` 以下に `prettier` への依存が記述されたダミーパッケージが作られ、そこで `npm install` が行われてからローカルパッケージが実行されます。

`npx` / `npm exec` をサポートするバージョンは以下の通りです。

- `npm exec` はnpm v7以降に存在します。挙動は `npx` とほぼ同じです。
- `npx` はnpm v5.2.0以降に同梱されていますが、独立した `npx` パッケージとしても提供されています。
  - v5.2.0～v6に同梱されている `npx` の実装は `libnpx` パッケージを使っているため、 `npx` パッケージと共通です。
  - v7に同梱されている `npx` は `npm exec` のエイリアスです。

yarn v2 (berry) にはこれに対応する `yarn dlx` が組み込まれていますが、本記事では詳しくは述べません。

## scripts

`npm run` / `npm run-script` / `yarn run` は `package.json` に記述されたスクリプトを実行します。つまり、 npm/yarnには簡易的なタスクランナーとしての機能があるといえます。

```json:package.json
{
  "scripts": {
    "build": "tsc",
    "test": "jest",
    "fmt": "prettier -w src/**/*.ts"
  }
}
```

`npm run` は `npm run-script` のエイリアスです。また、曖昧性がない場合は `yarn run` の `run` は省略できます。 (`yarn build` / `yarn fmt` など)

`scripts` 内のスクリプトは `./node_modules/.bin` にパスが通った状態で実行されるため、 `node_modules/.bin/webpack` のように明示する必要はありません。

npm/yarnの管理下でコマンドが実行されるときは、PATH以外にも `NODE` や `npm_lifecycle_event`, `npm_config_registry` などいくつかの環境変数がセットされます。これについては [`npm-run-script`](https://docs.npmjs.com/cli-commands/run-script.html) や [`npm-config`](https://docs.npmjs.com/misc/config) などのマニュアルを参照してください。

`npm run` / `yarn run` ともに、後続引数はスクリプト文字列の末尾に連結されます。たとえば、

```json:package.json
{
  "scripts": {
    "lint": "eslint 'src/**/*.ts'"
  }
}
```

という記述があるとき、 `yarn lint --fix` は `eslint 'src/**/*.ts' --fix` を実行します。

## ライフサイクルスクリプト

`scripts` で定義されるスクリプトの中には特別な意味を持つものがあります。これらをライフサイクルスクリプト (lifecycle scripts) と呼びます。ライフサイクルスクリプトを使うとnpmの処理にフックをかけることができます。

ライフサイクルスクリプトのうち重要なのは以下の2種類です。

- prepublish / prepublishOnly / prepare / **prepack** / postpack / publish / postpublish ... パッケージをレジストリに上げるときに呼ばれます。
- preinstall / **install** / postinstall ... パッケージがインストールされるときに呼ばれます。「依存関係として」「ローカルパッケージとして」「アプリケーションとして」の区別を問いません。

原則として、「preのついているフック」→「フック対象の処理」→「接頭辞なしのフック」→「postのついているフック」の順で呼ばれます。たとえばinstallの場合、以下の順番に呼ばれます[^preinstall-later]。

[^preinstall-later]: npm7ではpreinstallの動作が例外的になり、インストール処理よりも後になったようです。

- `preinstall` フック
- npmが行うインストール処理
- `install` フック
- `postinstall` フック

上記の重要なライフサイクルスクリプトの関係をまとめたのが以下の図です。

![図: 重要なライフサイクルスクリプト](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31409/ef2d8b90-58a1-5360-5c73-a79e4e9d490e.png)

ただし、prepublish以下のフックは様々な事情からサポート状況がまちまちです。以下の表を参照してください。

| |npm2|npm3|npm4|npm5|npm6|npm7|yarn|
|---|---|---|---|---|---|---|---|
| prepublish (pack / publish) |○|○|○|○|○| |○<sup>※1</sup>|
| prepublish (git install) | | | | | |○| |
| prepublish (local install) |○|○|○|○|○|○|○|
| prepare (pack / publish) | | |○|○|○|○<sup>※2</sup>| |
| prepare (git install) | | | |○|○|○| |
| prepare (local install) | | |○|○|○|○|○|
| prepublishOnly | | |○|○|○|○|○|
| prepack / postpack (pack / publish) | | | |○|○|○|○|
| prepack / postpack (git install) | | | |○|○|[×](https://github.com/npm/cli/issues/1865)|[×](https://github.com/yarnpkg/yarn/issues/5047)|
| (pre-, post-)shrinkwrap | | | |○|○| | |

※ バグと思われるものには×をつけている
※1 prepublishは `yarn publish` では実行されるが `yarn pack` では実行されない
※2 npm7では、pack/publish内の `prepare` は `prepack` の直前ではなく直後に実行される

これらの状況を踏まえて、以下のようにスクリプトを配置するのがよいでしょう。

- トランスパイルが必要なパッケージではトランスパイルを `prepack` で行う。ただし、git依存関係の `prepack` 呼び出しは現時点でnpm/yarnともに盛大にバグっているので、将来的なバグ修正に期待するしかないでしょう。
- ネイティブパッケージのビルドは `install` で行う。

その他、npmが規定するライフサイクルスクリプトとして以下があります。

- preuninstall, uninstall, postuninstall ... `npm uninstall` にフックします。
- preversion, version, postversion ... `npm version` にフックします。
- preshrinkwrap, shrinkwrap, postshrinkwrap ... `npm shrinkwrap` にフックします。
- pretest, test, posttest ... `npm test` のときに呼ばれます。
- prestart, start, poststart ... `npm start` のときに呼ばれます。
- prestop, stop, poststop ... `npm stop` のときに呼ばれます。
- prerestart, restart, postrestart ... `npm restart` のときに呼ばれます。

また、 `npm run` / `yarn run` も実際にはpre/postスクリプトを実行します。たとえば `npm run build` は `prebuild`, `build`, `postbuild` の3つのスクリプトを実行することになります。 (`preprebuild` などは実行しません) 上に挙げたうちtest/start/stop/restartは単に `npm run` の `run` を省略できるケースともみなせます。

また、npmにはいくつか既定のスクリプトが存在します。

- `start`: `node server.js` (`server.js` というファイルがある場合のみ)
- `install`: `node-gyp rebuild` (`binding.gyp` というファイルがあり、 `install`/`preinstall` がどちらも定義されていない場合のみ)
- `restart`: `stop` してから `start` するのがデフォルトの挙動です。

なお、yarn v2 (berry) ではライフサイクルスクリプトの扱いが大幅に整理されていて、使えるスクリプトも限定されているようです。詳しくは[Lifecycle Scripts](https://yarnpkg.com/advanced/lifecycle-scripts)を参照してください。

## npm link / yarn link

npm/yarnはデフォルトではシンボリックリンクを使いませんが、シンボリックリンク操作のためのコマンドとして `npm link` / `yarn link` が存在しています。主に以下の2つの用途があります。

- 開発中のコマンドラインアプリケーションをグローバルに利用可能な状態にする (`npm link` のみ)
- 開発中のライブラリを別のパッケージから利用可能な状態にする (`npm link` / `yarn link`)

### アプリケーションのリンク

`package.json` のあるディレクトリで `npm link` を無引数で実行すると `$PREFIX/lib/node_modules` 以下に作業中のパッケージ (カレントディレクトリ) へのシンボリックリンクが作成されます。また、 `$PREFIX/bin` に対応するシンボリックリンクが作成されます。これらのディレクトリは `npm install -g` が使用しているものと同じなので、実質的に**ローカルパッケージのコマンドをグローバルに利用可能な状態にしている**ことになります。

無引数の `npm link` は「シンボリックリンクであること」以外は `npm install -g` と同じなので、 `npm uninstall -g <パッケージ名>` で元に戻せます。 (なお、 `npm unlink` は `npm uninstall` のエイリアスです)

### ライブラリのリンク (npm link)

`npm link` にパッケージ名を引数にして実行すると、 `$PREFIX/lib/node_modules/$package_name` が指していたディレクトリへのシンボリックリンクが `./node_modules/$package_name` として作成されます。 (元々 `./node_modules/$package_name` に展開されていたファイルは消滅します)

このコマンドは通常、「無引数の `npm link`」と組み合わせて以下のように使うことが想定されています。

```
cd /path/to/foo1
npm link
cd /path/to/bar1
npm link foo1 # foo1 は/path/too/foo1/package.jsonに記載のパッケージ名
# 以降はbar1は /path/to/foo1のソースを参照するようになる
```

この操作の正しい取り消し方法はわかりませんが、以下のようにすると良さそうです。なお、 `npm unlink` は `npm uninstall` のエイリアスであり、 `npm link` の逆を行ってくれるわけではありません。

```
npm uninstall --no-save $package_name
npm install --force
```

## ライブラリのリンク (yarn link)

`yarn link` も同様の目的で使うことができます。

- 無引数の `yarn link` は `~/.config/yarn/link/$package_name` というシンボリックリンクを作成します。
- 引数つきの `yarn link $package_name` は `./node_modules/$package_name` を消し、 `~/.config/yarn/link/$package_name` へのシンボリックリンクで置き換えます。
  - `npm link` が作成するシンボリックリンクは直接参照ですが、 `yarn link` が作成するシンボリックリンクは間接参照になるようです。

使い方は `npm link` と同じです。

```
cd /path/to/foo1
yarn link
cd /path/to/bar1
yarn link foo1 # foo1 は/path/too/foo1/package.jsonに記載のパッケージ名
# 以降はbar1は /path/to/foo1のソースを参照するようになる
```

`yarn link` を取り消すには以下のようにします。 (npmとは異なり、 `yarn unlink` は `link` の逆をするためのコマンドです)

```
yarn unlink
```

`yarn link $package_name` を取り消すには以下のようにします。

```
yarn unlink foo1 # foo1 はyarn linkしていた依存関係のパッケージ名
yarn install --force
```

## linkの問題点

linkを使うと、利用側パッケージの動作を確認しながらライブラリを編集できるようになります。しかしNode.jsの `require` の挙動上、linkした依存関係は通常の依存関係とは異なる挙動をすることがあります。これは以下の2つの理由によります。

1. 依存元パッケージの `node_modules` を参照できないこと。
2. ライブラリ側の `node_modules` が優先されてしまうこと。

上記の理由により、以下のような現象が発生する可能性があります。

- 間接依存関係のバージョンが一致しない。 (ライブラリ側が使うバージョンはライブラリ側の `package-lock.json` / `yarn.lock` に基づいて決まるため)
- peerDependenciesに書かれたパッケージが見つからない。 (peerDependenciesは依存元パッケージ側の `node_modules` に存在するため)
- 間接依存関係のバージョンが同じで、巻き上げの条件を満たしていても、二重requireが発生する。

`node_modules` を用いた古典的なパッケージ管理を使っている限り、これらを綺麗に解決するのは難しいですが、 1. については `--preserve-symlinks` オプションを使うことで緩和できる可能性があります。Node.jsは通常シンボリックリンクを明示的に展開しますが、 `--preserve-symlinks` が指定されたときはこの挙動がスキップされます。これにより、 `../node_modules` や `../../node_modules` を参照するときの親ディレクトリの計算結果が変化し、ライブラリ側から依存元パッケージの `node_modules` がrequireできるようになります。

## gulp / grunt

`package.json` の `scripts` では賄いきれないような複雑な処理 (タスク定義の共通化や依存管理) を定義したい場合は、GulpやGruntのようなタスクランナーを使うことができるようです[^author-no-gulp-or-grunt]。ただし、JavaScriptのプロジェクトで行う必要があるタスクは典型的なもの (トランスパイルやバンドリング) が多く、TypeScriptやWebpackなどそれぞれのツールが依存管理を含めたパイプラインを提供しているため、これらで済んでしまうことも多いでしょう。

[^author-no-gulp-or-grunt]: タスクランナーの詳細については省きます。また、筆者はGulpもGruntもきちんと使ったことがないのですが、認識に間違いがあったらすみません……

## まとめ

- `npm install -g` / `yarn global add` を使うと、パッケージをグローバルにインストールし、CLIツールとして使うことができる。
- `npm install` / `yarn add` で追加したパッケージも `node_modules/.bin` にPATHを通すことでCLIツールとして使うことができる。 `npm` / `yarn` の内部で呼ばれるコマンドはこのディレクトリにPATHが通った状態で呼ばれる。
- `yarn exec` / `npx` / `npm exec` を使うと、任意のコマンドを `node_modules/.bin` にPATHが通った状態で実行することができる。また、 `npx` / `npm exec` に存在しないコマンドを渡した場合は、自動インストールが行われる。
- `npm run` (`npm run-script`) / `yarn run` を使うと、 `package.json` の `scripts` に登録されたコマンドを実行できる。 `yarn run` の `run` は省略できる。
- `scripts` の中には特別なコマンド名がいくつかあり、npm/yarnの特定の処理にフックすることができる。どのフックが呼ばれるかはバージョンによる挙動の違いが激しいので注意が必要。
  - ネイティブ拡張のビルドが必要な場合は `install` か `postinstall` で行うのがよい。
  - トランスパイルは `prepack` で行うのがよいが、現状ではnpm/yarnともにバグがあってあまりうまく動かない。
- `npm link` / `yarn link` を使うと、別のディレクトリにあるパッケージを直接使うことができる。アプリケーションの動作を確認しながらライブラリの開発するのに有用だが、依存解決の観点からはlinkによって異なる挙動をする可能性があるため、注意が必要。
- `npm run` / `yarn run` は単なるスクリプト実行機能しかないので、Makefileのようなより複雑なタスク管理が必要ならGulpやGruntなどのタスクランナーを使うのがよい。ただし、トランスパイルやアセットのビルドなど典型的なものであれば、Webpackなどそれに特化したツールで目的を達成できることも多い。

次回はモジュールバンドラーの基本的な役割と実装について、webpackを例に説明します。

[←前](https://qiita.com/qnighy/items/677bf64063c666b21034) [目次](https://qiita.com/qnighy/items/16fdd8e58309a1f706a0) [次→](https://qiita.com/qnighy/items/3d2496e9cf48f16cee05)