前回に続き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を例に説明します。
