Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8691

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

$
0
0

前回に続き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以下のフックは様々な事情からサポート状況がまちまちです。以下の表を参照してください。

npm2npm3npm4npm5npm6npm7yarn
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もきちんと使ったことがないのですが、認識に間違いがあったらすみません…… 


Viewing all articles
Browse latest Browse all 8691

Trending Articles