前回に続きnpmの機能について扱います。今回はnpmとコマンドラインツールとの関わりを中心に見ていきます。
注意: Windowsとそれ以外では、npmのフォルダ配置は異なります。Windowsでの挙動についてはnpm-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" フィールドの記述に基づいて生成される
- これはyarnの
$PREFIX/bin
にはNode.jsの実行バイナリ (node
) も入っているので、Node.jsが使えている時点でここにPATHは通っていると仮定してよさそうです。
$PREFIX/lib/node_modules/yarn/bin/yarn.js
は実行可能属性が付与されていて、冒頭は以下のようになっています。1
#!/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はモジュール解決前にシンボリックリンクを解決する2ので、これは node $PREFIX/lib/node_modules/yarn/bin/yarn.js
と同じ意味になります。 (yarn.js
を起点に require
が解決される。)
npm install -g
のインストール先は $PREFIX/lib/node_modules
であって $PREFIX/lib/node
ではありません。Node.jsの探索ルールに含まれているのは前者ではなく後者です3から、これによってライブラリをインストールしても require
から使われることはありません。
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
のエイリアスです。
- v5.2.0~v6に同梱されている
yarn v2 (berry) にはこれに対応する yarn dlx
が組み込まれていますが、本記事では詳しくは述べません。
scripts
npm run
/ npm run-script
/ yarn run
は package.json
に記述されたスクリプトを実行します。つまり、 npm/yarnには簡易的なタスクランナーとしての機能があるといえます。
{
"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
や npm-config
などのマニュアルを参照してください。
npm run
/ yarn run
ともに、後続引数はスクリプト文字列の末尾に連結されます。たとえば、
{
"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の場合、以下の順番に呼ばれます4。
-
preinstall
フック - npmが行うインストール処理
-
install
フック -
postinstall
フック
上記の重要なライフサイクルスクリプトの関係をまとめたのが以下の図です。
ただし、prepublish以下のフックは様々な事情からサポート状況がまちまちです。以下の表を参照してください。
npm2 | npm3 | npm4 | npm5 | npm6 | npm7 | yarn | |
---|---|---|---|---|---|---|---|
prepublish (pack / publish) | ○ | ○ | ○ | ○ | ○ | ○※1 | |
prepublish (git install) | ○ | ||||||
prepublish (local install) | ○ | ○ | ○ | ○ | ○ | ○ | ○ |
prepare (pack / publish) | ○ | ○ | ○ | ○※2 | |||
prepare (git install) | ○ | ○ | ○ | ||||
prepare (local install) | ○ | ○ | ○ | ○ | ○ | ||
prepublishOnly | ○ | ○ | ○ | ○ | ○ | ||
prepack / postpack (pack / publish) | ○ | ○ | ○ | ○ | |||
prepack / postpack (git install) | ○ | ○ | × | × | |||
(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を参照してください。
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つの理由によります。
- 依存元パッケージの
node_modules
を参照できないこと。 - ライブラリ側の
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のようなタスクランナーを使うことができるようです5。ただし、JavaScriptのプロジェクトで行う必要があるタスクは典型的なもの (トランスパイルやバンドリング) が多く、TypeScriptやWebpackなどそれぞれのツールが依存管理を含めたパイプラインを提供しているため、これらで済んでしまうことも多いでしょう。
まとめ
-
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を例に説明します。