Help us understand the problem. What is going on with this article?

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

前回に続き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" フィールドの記述に基づいて生成される

$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 execyarn 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に同梱されている npxnpm exec のエイリアスです。

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

scripts

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

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

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

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

npm/yarnの管理下でコマンドが実行されるときは、PATH以外にも NODEnpm_lifecycle_event, npm_config_registry などいくつかの環境変数がセットされます。これについては npm-run-scriptnpm-config などのマニュアルを参照してください。

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

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

という記述があるとき、 yarn lint --fixeslint '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内の prepareprepack の直前ではなく直後に実行される

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

  • トランスパイルが必要なパッケージではトランスパイルを 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 buildprebuild, build, postbuild の3つのスクリプトを実行することになります。 (preprebuild などは実行しません) 上に挙げたうちtest/start/stop/restartは単に npm runrun を省略できるケースともみなせます。

また、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 unlinknpm 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 unlinknpm 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 unlinklink の逆をするためのコマンドです)

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.jsonscripts では賄いきれないような複雑な処理 (タスク定義の共通化や依存管理) を定義したい場合は、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.jsonscripts に登録されたコマンドを実行できる。 yarn runrun は省略できる。
  • scripts の中には特別なコマンド名がいくつかあり、npm/yarnの特定の処理にフックすることができる。どのフックが呼ばれるかはバージョンによる挙動の違いが激しいので注意が必要。
    • ネイティブ拡張のビルドが必要な場合は installpostinstall で行うのがよい。
    • トランスパイルは prepack で行うのがよいが、現状ではnpm/yarnともにバグがあってあまりうまく動かない。
  • npm link / yarn link を使うと、別のディレクトリにあるパッケージを直接使うことができる。アプリケーションの動作を確認しながらライブラリの開発するのに有用だが、依存解決の観点からはlinkによって異なる挙動をする可能性があるため、注意が必要。
  • npm run / yarn run は単なるスクリプト実行機能しかないので、Makefileのようなより複雑なタスク管理が必要ならGulpやGruntなどのタスクランナーを使うのがよい。ただし、トランスパイルやアセットのビルドなど典型的なものであれば、Webpackなどそれに特化したツールで目的を達成できることも多い。

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

←前 目次 次→


  1. #! がOKな理由についてはこちらの記事を参照。 

  2. シンボリックリンクの解決については第3回を参照。 

  3. Node.jsの探索ルールについては第3回を参照。 

  4. npm7ではpreinstallの動作が例外的になり、インストール処理よりも後になったようです。 

  5. タスクランナーの詳細については省きます。また、筆者はGulpもGruntもきちんと使ったことがないのですが、認識に間違いがあったらすみません…… 

qnighy
wantedly
「シゴトでココロオドル」ためのビジネスSNS「Wantedly」の開発・運営をしています。
https://wantedlyinc.com/ja/presentations
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away