Gitを使ってnpmパッケージをインストールしてもらうときにビルドを実行させるためにnpm prepareを使う

npmではパッケージのインストール元として公式リポジトリ以外に、別フォルダやGitを選択することができます。

package.jsonのdependenciesを見てみると、以下の例のように、公式のリポジトリの場合にはバージョン番号が書かれており、Gitを用いている場合にはgit+<GitリポジトリのURL>が書かれています。

  "dependencies": {
    "base64url": "^3.0.1",
    "npm-prepare-test-installed": "git+https://github.com/s1r-J/npm-prepare-test-installed.git"
  }

npmのパッケージにはTypeScriptやBabelを用いてビルド後のコードを実行するパッケージも存在します。 このようなビルドが必要なパッケージはnpmのリポジトリにはビルド後のコードがアップロードされており、パッケージを利用する側ではnpm installによって取得したビルド後のコードを実行します。

しかし、Gitを用いてGitリモートリポジトリ(例えばGitHub)からパッケージを取得する場合には、このように同じようにはいきません。 通常、Gitリポジトリにはビルド前のソースコードだけをアップロードし、ビルド後のコードはアップロードしないからです。 そのため、このようなパッケージをインストールしてもビルド後のコードが存在せず、実行することができません。

今回は、Gitを用いてインストールされるビルドが必要なパッケージに少し工夫を施し、快適に利用できるnpm prepareという機能を用いた方法を紹介します。

npm prepareとは

npm prepareについて公式ドキュメントで言及されているのはライフサイクルスクリプトhttps://docs.npmjs.com/cli/v10/using-npm/scripts#life-cycle-scripts )の項目です。

npmにはpackage.jsonのscriptsプロパティ内に特定の文字列をキーにコマンドを設定すると、特定のタイミングでそのコマンドが自動的に実行されます。これがライフサイクルスクリプトです。

例えば、prepublishではパッケージ公開のコマンドであるpublishが実行される前に自動的に呼び出されたり、postinstallではパッケージのインストールをおこなうinstallが実行された後に呼び出されたりします。

では、npm prepareが実行されるタイミングは以下の3つとなっています。

  1. パッケージをパックする(npm publishおよびnpm pack)前
  2. 引数なしでローカルのnpm installを実行したとき
  3. prepublishの後、prepublishOnlyの前

この記事で紹介する方法に関連するのは2になります。 そして、2については更に注釈がつきます。

注意:Gitを介してprepareスクリプトを含むパッケージがインストールされる場合、そのパッケージがパッケージされてインストールされる前に、そのdependenciesおよびdevDependenciesがインストールされ、そしてprepareスクリプトが実行されます。

以下は原文

NOTE: If a package being installed through git contains apreparescript, itsdependenciesanddevDependencieswill be installed, and the prepare script will be run, before the package is packaged and installed.

Gitでインストールされる側のpacakge.jsonのscriptsにprepareスクリプトを設定しておくと、そのパッケージがGitを使ってインストールされるときに設定したコマンドが自動的に実行される、ということです。また、prepareスクリプトdependenciesanddevDependenciesに記載されているパッケージがインストールされた後に実行されます。

つまり、prepareスクリプトでビルド処理を書いておけば、ビルドのために必要なdevDependenciesに記載したパッケージがインストールされ、ビルドを実行した上でインストールしてもらうことができるのです。まさに利用される・インストールされる前に準備をおこなうためのスクリプトだと思います(インストールされる前に実行されるスクリプトpostinstallがありますが、こちらはdevDependenciesのパッケージがインストールされないことが問題になります。記事の余談を参照。)。

おかげで、Gitリポジトリにビルド結果をプッシュしなくて済みます。

ここで気になるのは、インストールされる側のdevDependenciesに記載されたパッケージが、余計なパッケージとして残ってしまうのではないか、という点です。先述の公式ドキュメントにはこれについての記載はありませんでした(たぶん)。

後述のデモでこの点についても検証していますが、先に結論を書いておくと、devDependenciesのパッケージは残りませんでした。

デモ

prepareを使ってパッケージのビルドをどのように設定するのか、前述の気になる点はどうなっているのかをデモで紹介します。

デモで使っているソースコードGitHubに置きました。自由に使ってください(npmには公開していないです)。

サンプルパッケージ

最初に今回デモで使うパッケージの依存関係とそのソースコードについて確認します。

以下の2つのパッケージを利用します。

インストールされるパッケージ(npm-prepare-test-installed)を見てみます。

こちらのパッケージはTypeScriptで書かれており、実行するにはTypeScriptをビルドしてJavaScriptに変換する必要があります。 package.jsonのscriptsのbuildにはTypeScriptのビルドをおこなうtscコマンドが書かれています。また、prepareにはビルドを実施するため、npm run buildを実行するように設定しています。

また、devDependenciesにはtypescriptが記載されており、tscコマンドはこのtypescriptパッケージが存在することで実行できます。

ソースコードの中身は非常に単純で、helloという関数に与えられた引数の人名に挨拶をする文字列を返します。

export default function hello (name: string): string {
    const target = name || 'anonymous';
    return `Hello, ${target}!`;
}

次にnpm-prepare-test-installedを利用するパッケージ(npm-prepare-test)を見ます。

package.jsondependenciesnpm-prepare-test-installedが記載されており、Gitを介したインストールをおこなうように設定されています。

  "dependencies": {
    "npm-prepare-test-installed": "git+https://github.com/s1r-J/npm-prepare-test-installed.git"
  }

ソースコードはこちらも単純です。npm-prepare-test-installedパッケージからhello関数を読み込み、hello関数にJohnという文字列を渡して結果を標準出力に出力するというものです。

const hello = require('npm-prepare-test-installed').default;
console.log(hello('John'));

インストールされるパッケージ(npm-prepare-test-installed)がビルドされ、JavaScriptのコードになっていなければ、hello関数が見つけられず、エラーになります。

インストール・実行

パッケージの確認が済んだところで動作を見ています。

  1. npm-prepare-test-installedを利用するパッケージ(npm-prepare-test)をGitからクローンします
  2. $ git clone https://github.com/s1r-J/npm-prepare-test
  3. ディレクトリを移動します
  4. $ cd ./npm-prepare-test
  5. パッケージをインストールします
  6. $ npm install
  7. npm-prepare-testパッケージを実行します
  8. $ node index.js
  9. 標準出力に以下の出力が確認できます(=npm-prepare-test-installedパッケージがビルドされた)
  10. Hello, John!

5のようにTypeScriptによるビルドが必要なパッケージを呼び出すことができ、npm-prepare-test-installedprepareが想定通りにdevDependenciesのtypescriptパッケージをインストールし、ビルドを実行してくれたことがわかりました。

node_modulesの確認

npm-prepare-test-installedprepareでインストールされたtypescriptパッケージはどのようになったのかを確認します。 npm-prepare-testでは不要なのにtypescriptパッケージが残っていたら無駄です。

  1. node_modulesを確認します
  2. $ ls node_modules
  3. npm-prepare-test-installedしか存在しないことが確認できました
  4. npm-prepare-test-installed/
  5. さらにnpm-prepare-test-installedディレクトリを確認します
  6. $ ls node_modules/npm-prepare-test-installed
  7. typescriptパッケージは存在しないことが確認できました(ちなみにビルド後のファイルはdistディレクトリに入っています)
  8. LICENSE README.md dist/ in-files/ package.json

prepareでインストールされたtypescriptパッケージは、prepareを実行した後(ビルドが完了した後)に削除されるようです。懸念点は解消されました。

これで安心してGitを使ってインストールされるnpmパッケージには、prepareにビルド処理を設定することで快適な開発環境を作ることができます。

まとめ

Gitを介してインストールされるnpmパッケージは、npmスクリプトprepareにビルド処理を記載しておくことで、インストールされる際に自動的にビルドを実行するように設定できます。ビルドに必要なパッケージはdevDependenciesに書いておけば問題なく(=一般的な開発からpackage.jsonを変更する必要はない)、ビルド前に一時的にインストールされ、ビルド完了後にこれらのパッケージが残ることはありません。

余談(postinstallについて)

prepareはパッケージがGitを使ってインストールされるとき、dependenciesanddevDependenciesに記載されているパッケージがインストールされた後に、設定しておいたコマンドが自動的に実行される、と紹介しました。

似た動作をするpostinstallというライフサイクルスクリプトが存在します。これは読んで字のごとく、パッケージがインストールされた後に実行されるスクリプトです。

しかし、postinstallprepareと異なるのはdevDependenciesのパッケージがインストールされるか否かです。

postinstallはあくまで、インストールされた後に実行されるスクリプトでしかなく、何かしらの準備をおこなうprepareとは異なります。 postinstallではビルドに必要なパッケージを記載しているdevDependenciesがインストールされないため、ビルドに必要なモジュールをdependenciesに記載する必要が発生します。 これはパッケージサイズの肥大や保守性の低下を引き起こすことから推奨されません。

参考情報