【package.json】dependencies, devDependencies の使い分けを考える

## 本記事で対象となる開発

本記事では、これらを前提として狭い範囲に対して主張を行います :sunglasses:

### Webアプリケーションを開発する

- **ライブラリは本記事の主張からは対象外**とする

### JavaScript で書かれたアプリケーションコードは利用者のブラウザで実行される

- Node.js で実行されるサーバーアプリケーションではない

### ビルド済みのファイルは Git で管理しない

- ビルド済みファイルで発生する差分はソースファイル、ビルド環境の差分でしかない
- コンフリクトしやすい
- テストは Git  のリモートリポジトリを clone し、CI サービスで実行する
- デプロイは Git のリモートリポジトリを clone し、CD サービスでビルドしたものを動作環境に設置する

## dependencies, devDependencies を使いわけるとなにが変わるか

`npm install --production` を有効に使うことができます。

`npm install --production` は `dependencies` に含まれるパッケージだけをインストールします。

> With the --production flag (or when the NODE_ENV environment variable is set to production), npm will not install modules listed in devDependencies.
>
> [npm\-install \| npm Documentation](https://docs.npmjs.com/cli/install)

これによりインストール時間の短縮、ディスク使用量の削減が期待できます。

## アプリケーションにはどのようなステージがあるか

`dependencies`, `devDependencies` を使い分けることの直接的な効能は `npm install` 時の `--production` オプションによってインストールするパッケージを限定できるということです。よって、使い分けの基準を考えるにあたって `npm install` が実行される場面を考えます。

今日では GitHub Actions や CircleCI といった CI/CD サービスを使うようになり、それはテスト、ビルド、リリースといったステージごとにコンピューティングリソースを分けるようになりました。つまり `npm install` もアプリケーション開発のステージごとに実行されるようになりました。

アプリケーション開発のステージのうち、コードが関係してくるのは以下です。

- コーディング
- テスト
- ビルド
- 実行

このうち、`node_modules` をパッケージ単位で扱うの必要があるのは、上から「実行」を除いた以下の3つです。

- コーディング
- テスト
- ビルド

実行時は Webpack をはじめとしたバンドラーによって、各パッケージはファイルにまとめられ、ファイル単位ではパッケージという概念が消え去るからです。

## ステージごとに必要なパッケージを考える

以下のパッケージを用いたアプリケーション開発を例に、`dependencies`, `devDependencies`  の使い分けを考えます。

|パッケージ名|簡単な説明|
|---|---|
|[react](https://www.npmjs.com/package/react)|UIライブラリ|
|[@babel/core](https://www.npmjs.com/package/@babel/core)|トランスパイラー|
|[webpack](https://www.npmjs.com/package/webpack)|バンドラー|
|[webpack\-dev\-server](https://www.npmjs.com/package/webpack-dev-server)|webpack を使うときのライブリロード用のサーバー|
|[eslint](https://www.npmjs.com/package/eslint)|リンター|
|[jest](https://www.npmjs.com/package/jest)|テストツール|

アプリケーション開発をステージに分け、各ステージでどのパッケージが必要か考えます

### コーディング（開発）

コーディングと言っていますが普段のローカル開発のステージです。デバッグ実行し、このステージでテストをすることもあります。
基本的にすべてのパッケージを使用します。

- react
- @babel/core
- webpack
- webpack-dev-server
- eslint
- jest

### テスト

テスト時も、Git のリモートリポジトリから取得したソースコードを `@babel/core` でトランスパイルします。
今回は `jest` を使った単体テストのみを期待しているため、`webpack`, `webpack-dev-server` はテスト時には不要かと思います。

- react
- @babel/core
- eslint
- jest

### ビルド

`webpack-dev-server`, `eslint`, `jest` は必要ありません。

- react
- @babel/core
- webpack

## 一旦まとめ

どのステージでも共通して `react`, `@babel/core` が使用されることが分かりました。

`dependencies`, `devDependencies` を使い分ける目的は、`npm install --production` オプションによって `dependencies` に含まれるパッケージのみをインストールし、負荷をへらすことでした。

そのため、どのステージにも共通して使用される `react`, `@babel/core` は、`dependencies` に該当することが分かります。`devDependencies` として振り落としたい場面がないからです。

逆に、「コーディング」ステージのみで使用されるパッケージである `webpack-dev-server` は、インストールすることが無駄であるステージがいくつもあります。真っ先に `npm install --production` で振り落としたい対象でしょうから `devDependencies` に含めるとよいことが分かります。

今回の例では「コーディング」ステージに該当するのは `webpack-dev-server` のみですが、他には例えば「git のコマンドに特定の処理をフックする [husky](https://www.npmjs.com/package/husky)」や「コードフォーマッターの [prettier](https://www.npmjs.com/package/prettier)（「テスト」ステージでチェックしなければ）」も当てはまります。

さて、残りはビルドステージで使われる `webpack`、テストステージで使われる `eslint`, `jest` です。

ここから、

- ビルドステージ・テストステージ両方のパッケージを `dependencies` に含めるのか
- ビルドステージ・テストステージ両方のパッケージを `devDependencies` に含めるのか
- ビルドステージに該当するパッケージを `dependencies` に含め、テストステージに該当するパッケージを `devDependencies` に含めるのか

などを考えていきます。

そもそも `dependencies`, `devDependencies` を使い分ける目的は、インストール時間の短縮、ディスク使用量の削減でした。

この目的が至上命令であるならば、使用するパッケージがどのステージに該当するかを考え、ステージごとのパッケージサイズの数・ファイルサイズから、開発するアプリケーションごとに、`dependencies`, `devDependencies` を使い分けを考えることになります。

例えば

- 今回はコーディングステージに属するパッケージは数が多いから、ビルド時にもテスト時にもインストールされたくない。よって、ビルド・テストステージで使うパッケージは `dependencies` にしよう
- 今回はビルドステージに属するパッケージは数が多いから、テスト時にビルドステージのパッケージにインストールかかる負荷が気になる。ビルドステージは `dependencies` にしよう

といったふうにです。

しかし、アプリケーション開発に導入するパッケージは、用件のたびに増減・変更されます。上記のように使い分けを厳格なルールとすると、パッケージの変更のたびに調査・使い分けの変更が発生し、プロジェクト開発において、より負担がかかることになるでしょう。

例えばテストステージで使うパッケージのサイズがビルドステージで使うパッケージのサイズを上回ったので、ビルドステージのパッケージを `devDependencies` に移動し、テストステージのパッケージを `dependencies` に移動するようなことが起こります。

これから分かるように、「インストール時間の短縮、ディスク使用量の削減」という目的を完璧に果たすことは大変です。また、果たしたとして何秒のインストール時間の節約になり何バイトディスクが節約されるでしょうか。

これ以降は「インストール時間の短縮、ディスク使用量の削減」という目的からは外れないように、**管理しやすい `dependencies`, `devDependecies` を使い分ける目安**を提案していきます。

## 使い分けの目安：ビルドステージは `dependencies`, テストステージは `devDependencies`

**ビルドステージに該当するパッケージは `dependencies` に、テストステージに該当するパッケージは `devDependencies`に含めてください。**

理由を述べます。

### `dependencies` にはランタイムに必要なパッケージを含めることにすると直感的

`devDependencies` には、その名前から開発用のパッケージが含まれることが期待されます。

`dependencies` はその逆ですので、ランタイムに必要なパッケージが含まれることを期待します。ビルド済みのファイルを Git 管理下に含めない場合、ビルドステージで使うパッケージはランタイムに必要なパッケージといえます。

そのためビルドステージで使うパッケージは `dependencies` に含めます。

### E2E テストを導入するとパッケージサイズが膨大になる（ならない？）

[puppeteer](https://www.npmjs.com/package/puppeteer) を導入すると Chromium がダウンロードされ、100MB~200MB になり、それだけで他のパッケージを圧倒する負荷になります。

`puppeteer` を `devDependencies` に含めると `npm install --production` ではインストールされません。テストステージで使うパッケージは膨大になることが予想されるために `devDependenncies` に含めます。

って言っても、今日日 CI では Chromium インストール済みイメージを使うから、npm install 時の Chromium ダウンロードはスキップするんですよね……

## まとめ

冒頭で示した前提に該当するアプリケーション開発においては、導入するパッケージは、アプリケーション開発のどのステージで使用されるかを考え、そのステージによって `dependencies`, `devDependencies` どちらに含めるか判断しようと述べました。

考えるべきステージは以下の3つがあり、

- コーディング
- テスト
- ビルド

`dependencies`, `devDependencies` への判断の**目安**は以下です

- `devDependencies`
    - コーディングステージのみで使用するパッケージ
    - ビルドステージでは使用しない、テストステージで使用するパッケージ
- `dependencies`
    - すべてのステージで使用するパッケージ
    - ビルドステージで使用するパッケージ

そもそも Webアプリケーション開発においてビルドツールが `devDependencies` に含まれることに違和感があり、ビルドツールは `dependencies` ではないかということに思いを巡らせた結果、本記事の主張に至りました。

「前提」で、かなり場面を限定化しているので一般的に適用できる内容か怪しいのですが。

-----

本記事が `dependencies`, `devDependencies` の使い分けに困っている方の手助けになれば幸いです。ここまで読んでいただきありがとうございました。

## 参考

- [dependenciesにbabelを入れる \- 橋本商会](https://scrapbox.io/shokai/dependencies%E3%81%ABbabel%E3%82%92%E5%85%A5%E3%82%8C%E3%82%8B)