😵‍💫

npm install --productionみたいなの色々ありすぎ問題

2023/12/11に公開

まえおきと結論

npmで依存関係を管理するプロジェクトを考えます。その際、プロダクションビルドには

npm install --production

を使うんだよ、という話は多くの方がご存知かと思います。オプションもそれっぽいですし、なんだか安心して使ってしまいます。

しかし、その詳細について調べていると似たような項目が山ほどあることがわかりました。今回は混乱の記録をまとめます😵‍💫

結論から言うと、このコマンドは最新のnpmにおいて非推奨です。 とくに問題なく動きますが、その違いを意識したうえで

npm install --omit=dev

ないしは

npm ci --omit=dev

に移行することを検討しましょう。

検証条件

調べたこと

package.jsonpackage-lock.jsonについての確認

npmを用いてプロジェクトを構成する場合、そこで利用するライブラリの一覧はpackage.jsonに記述されます。すなわち、package.jsonは依存関係の管理を行っているわけですね。

このpackage.jsonには、利用するライブラリを以下の2つの属性に分けて記載します[1]

属性 記載するもの
dependencies 実行時に必要なライブラリ
devDependencies 開発時にのみ必要なライブラリ

たとえば、日付を操作するためにMoment.jsを導入したとします。これはプロダクションコードの中で使用したいので前者に記載しましょう。なにも指定せずにnpm installすれば前者に入ります。

# 事前にnpm initを実施しておく
npm install moment

一方、ユニットテストを行うためにJestを導入する場合を考えます。これはそれ自体がプロダクションコードとして配布されるわけではなく、あくまでそれを書いたり保守したりするためのものですね。このようなライブラリは--save-dev-Dを付して実行することでdevDependenciesに追加します。

npm install --save-dev jest

さて、いまの状態はこんな感じですね。実行のタイミングでバージョンは変わるはずなのでご了承ください。

$ tree . -a -L 1
.
├── node_modules
├── package-lock.json
└── package.json

$ cat package.json
{
  # 省略
  "dependencies": {
    "moment": "^2.29.4"
  },
  "devDependencies": {
    "jest": "^29.7.0"
  }
}

package-lock.jsonには、以下に対して実際にインストールされたバージョンが記述されます。

  • package.jsonに記載された依存先のライブラリ
  • そのライブラリの依存先すべて

実際のソースコードはnode_modulesにあり、もし削除してしまってもpackage.jsonにある情報をもとにnpm installから復元できます。

しかし、これでは問題があります。npm installでインストールされるバージョンは、そのタイミングで条件を満たす最新のものです。ライブラリのバージョンが好き勝手に変化してしまうと、アプリの動作が保証できなくなってしまいます![2]

そこで依存先すべてのバージョンが記載されたpackage-lock.jsonを共有し、これに基づいてインストールを行えば、すべてのライブラリについてまったく同じソースコードを利用することができるようになるわけですね😛

これはnpm ciから実行できます。再現性が大切なプロダクションビルドやCI/CD環境ではこちらを利用することが一般的です。

混乱したところ

まえおきはここまでにして、以上の説明の中から今回取り上げたいトピックは2つです。

  1. プロダクションビルド時にはdevDependenciesのライブラリは無視する
  2. package-lock.jsonに基づいてインストールするにはnpm ciを利用する

そして、この1.を実現するにはnpm installnpm ci時に以下を実行すればOKなようです。

  • --productionオプションをつける
  • --only=prodオプションをつける
  • --omit=devオプションをつける
  • 環境変数をNODE_ENV=productionとして実行する

したがって以下の2×4通りあることになります。多いって!!

  1. npm install --production
  2. npm install --only=prod
  3. npm install --omit=dev
  4. 環境変数をNODE_ENV=productionとしてnpm install
  5. npm ci --production
  6. npm ci --only=prod
  7. npm ci --omit=dev
  8. 環境変数をNODE_ENV=productionとしてnpm ci

ということで、これらの違いとそれぞれどのようなケースに利用するのかを考えてみます😩

devDependenciesを除くためのオプションあれこれ

さて、改めて確認すると以下の選択肢がありました。

  • --productionオプションをつける
  • --only=prodオプションをつける
  • --omit=devオプションをつける
  • 環境変数をNODE_ENV=productionとして実行する

はじめに結論をいうと、推奨されるオプションは--omit=devです。 確認すると、これはdevDependenciesを無視してdependenciesのみをインストールします。

実際にやってみましょう!

$ npm install --omit=dev
up to date, audited 2 packages in 689ms
found 0 vulnerabilities

$ ls node_modules
@ampproject/    @babel/         @bcoe/          @istanbuljs/    @jest/          @jridgewell/    @sinclair/      @sinonjs/       @types/         moment/

あれ?@babelとか@jestも含まれちゃってるじゃん、と思ったかもしれません。npmでは、特定のパッケージがインストールされる際に、その名前空間に基づいて事前にディレクトリが作成されます。 つまり、@jestのようなスコープ付きパッケージ名を持つライブラリの場合、まずその名前空間に対応する親ディレクトリ(@jest)が作成されてしまうのですね。

実際に確認すると、node_modules/moment以外は空になっていることがわかります👻

$ tree . -a -L 3
.
├── node_modules
│   ├── (省略)
│   ├── @jest
│   ├── @jridgewell
│   ├── @sinclair
│   ├── @sinonjs
│   ├── @types
│   └── moment
│       ├── (省略)
│       └── ts3.1-typings
├── package-lock.json
└── package.json

さて、次に--only=prod--productionですが、これらは--omit=devのエイリアスでありDEPRECATEDです。 ドキュメントにも明記されていますね![3]

only
DEPRECATED: Use --omit=dev to omit dev dependencies from the install.
When set to prod or production, this is an alias for --omit=dev.
production
DEPRECATED: Use --omit=dev instead.
Alias for --omit=dev

試しに実行してみると「代わりに--omit=devを使ってね」というWarningが出ます。

$ npm install --production
npm WARN config production Use `--omit=dev` instead.
$ npm install --only=prod
npm WARN config only Use `--omit=dev` to omit dev dependencies from the install.

つまり以下の3つはまったく同じです!

  1. npm install --omit=dev
  2. npm install --only=prod
  3. npm install --production

一方、環境変数を利用するケースも確認しましょう。確かにNODE_ENVを設定するとインストールされるライブラリの数が大幅に減っていることが確認できますね。

$ npm install

added 291 packages, and audited 293 packages in 1s
32 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

$ export NODE_ENV=production
$ npm install

up to date, audited 2 packages in 541ms
found 0 vulnerabilities

$ unset NODE_ENV # 戻しておく

環境変数を指定するケースとオプションを指定するケース、なにか違いはあるのでしょうか? --omit=devのドキュメントには次のように記載があります。

If the resulting omit list includes 'dev', then the NODE_ENV environment variable will be set to 'production' for all lifecycle scripts.

なるほど、--omit=devとした場合は一連の処理がNODE_ENV=productionとして実行されるようです。ということは結局、4つはまったく同じ意味であることになります。

異なるのはアプローチだけですね。細かい違いを言えば、--omit=dev

  • いちいち環境変数を汚さずに済む
  • --omitは引数で他の操作もできるので拡張性がある

くらいでしょうか。

じゃあnpm install --omit=devってどこで使うの?

プロダクションビルドでは開発環境の再現が大切です。そのためpackage-lock.jsonに基づいてビルドするため、npm ciを使用することが一般的です。その際には不必要なライブラリをインストールしたくないので--omit=devオプションを指定します。

では、npm install --omit=devを使うケースはどこでしょうか?🤔

たとえば

  • 本番環境でpackage-lock.jsonを更新せざるをえない場合
  • 本番環境でpackage-lock.jsonが存在せず、新規に生成したい場合

などが思いつきましたが、この状況に陥ることはまずないと思います。その他に強いて言えばmasterブランチ上で開発を行わざるを得ない状況かもしれません。

たとえば、Gitflowではmasterブランチで問題が発生した場合、そこからhotfixブランチを切って修正します。このブランチは本番環境に極めて近いものなので、その検証にはdevDependenciesの影響を与えたくありません。そのため新規にライブラリを追加してデバッグを行う場合にnpm install --omit=devを使うべきかもしれないですね。

まとめ

  • 以下の4つの動作はまったく同じであり、アプローチが違うだけ
    • --omit=devオプションをつける
    • --only=prodオプションをつける
    • --productionオプションをつける
    • 環境変数をNODE_ENV=productionとして実行する
  • いずれもdevDependenciesを無視してdependenciesに記載のライブラリのみをインストールする
  • 推奨されるのは--omit=devオプションか環境変数の指定
    • --production, --only=prodは非推奨であり実行時にWarningが表示される
  • プロダクションビルドにはnpm ci --omit=devを使用しましょう

補足:--includeオプションについて

似たものにincludeオプションがあります。ドキュメントには--omit=<type>の逆(inverse)だと書かれていますね。

Option that allows for defining which types of dependencies to install.
This is the inverse of --omit=<type>.
Dependency types specified in --include will not be omitted, regardless of the order in which omit/include are specified on the command-line.

この「逆」というのがちょっとわかりにくいのですが、たとえば

npm install --include=prod

としたときにnpm install --omit=devと同じ処理になるわけではありません

これは「もともと除外されている項目を引数に指定して、それを含めてインストールする」という意味になります。たとえば、NODE_ENV=productionとして次のコマンドを実行するとdevDependenciesも含めてインストールされるというわけですね。

$ export NODE_ENV=production
$ npm install --include=dev

up to date, audited 293 packages in 689ms
32 packages are looking for funding
  run `npm fund` for details

こちらはDEPRECATEDではありません。もし利用する場合は--onlyとの違いを意識したいですね!

脚注
  1. これらの他にoptionalDependenciespeerDependenciesなどもあります。前者は利用可能ならインストールされるが、もし利用不可であっても他の処理を中断したくない場合に使用するもの、後者は指定したバージョンを満たしてなくても警告を出さなくするための機能です。が、実際のところ頻繁に利用されるものではないので本記事ではスルーして解説は公式ドキュメントに譲ります。 ↩︎

  2. たとえば、上述のpackage.jsonでは"moment": "^2.29.4"、すなわち2.x.x系を指定しており、これを満たすものなら何でもOKだと宣言していることになってしまいます。そこで"moment": "2.29.4"のように特定のバージョンを指定することもできます。しかし、このmomentが依存するライブラリのバージョンまでは指定できません。そのため、すべての依存先のバージョンをpackage-lock.jsonに記載しておくのですね。 ↩︎

  3. このドキュメント、npm installの項目ではなくconfigの一覧にありました。見つけにくいよ! ↩︎

Discussion