0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

npmのinstall-strategyを4種類切り替えてnode_modulesの中身を比べてみた

0
Posted at

はじめに

npmにはnode_modulesの作り方を切り替えるinstall-strategyという設定があります。
hoisted、nested、shallow、linkedの4種類があり、デフォルトはhoistedです。

npm v11以降のドキュメントでは、パッケージ作者は開発中に--install-strategy=linkedを使うことが推奨されていました。
理由は、package.jsonに書いていない依存(いわゆる ファントム依存)を公開前に見つけられるから、とされています。

この推奨文がドキュメントに追加されたのは2026年6月末で、比較的最近のことです(該当のPR)。

また、v10のドキュメントではlinkedに(experimental)と書かれていて、v11以降のドキュメントではその表記がなくなっています。ただ、手元のnpm 11.16.0ではlinkedを使うとEXPERIMENTALの警告が出ました。ドキュメントの表記と実行時の警告が食い違っている状態のようです。

警告の文面では、実験的な機能でバグを含む可能性があるとされているので、本番の環境で使う場合は注意が必要そうです。

普段npm installするときにnode_modulesの形を意識することはほとんどなかったので、4種類を実際に切り替えて、構造やファントム依存の扱いがどう変わるのかを確かめてみました。

環境

項目 バージョン
OS macOS 26.6.2
Node.js v24.18.1
npm 11.16.0
express 4.22.3

ドキュメントによると、install-strategyはnpm v9から使える設定で、それより前は--legacy-bundlingや--global-styleというオプションで似たことをしていたとされています。現在、これらはそれぞれnestedとshallowに置き換えられています。

install-strategyの種類

公式ドキュメントの説明を自分なりにまとめると、次のとおりです。

値 置き方
hoisted(デフォルト) 重複しないパッケージはnode_modules直下に巻き上げ、バージョンが衝突するものだけ下の階層に置く
nested 巻き上げをせず、依存されている場所にそのまま置く(旧--legacy-bundling)
shallow 直接依存だけを直下に置く(旧--global-style)
linked 実体をnode_modules/.storeに置き、必要な場所にシンボリックリンクを張る。巻き上げはしない

linkedにすると、node_modulesはpnpmと近い構造になります。

実験用のプロジェクト

今回、実験用のプロジェクトを作成し依存はexpressだけにして試してみました。
expressは内部でdebugを使っていますが、自分のpackage.jsonにはdebugを書いていません。

package.json
{
  "name": "npm-install-strategy-lab",
  "version": "1.0.0",
  "private": true,
  "main": "index.js",
  "dependencies": {
    "express": "4.22.3"
  }
}

そのうえで、index.jsでわざとdebugをrequireしています。
これがファントム依存です。

index.js
// expressはpackage.jsonに書いてある「宣言済みの依存」
const express = require("express");

// debugはpackage.jsonに書いていない。expressが内部で使っている依存
const debug = require("debug")("lab");

const app = express();
app.get("/", (_req, res) => res.send("ok"));

debug("debug を読み込めました");
console.log("express と debug の読み込みに成功しました");

試してみる

まずはデフォルトの hoisted

$ npm install
$ node index.js
  • 実行結果
➜  npm-install-strategy-lab npm install

added 68 packages, and audited 69 packages in 721ms

18 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
➜  npm-install-strategy-lab node index.js
express と debug の読み込みに成功しました

package.jsonに書いていないdebugが普通に読み込めてしまっています。
node_modulesの直下を見ると、expressの依存が全部並んでいます。

$ ls node_modules
➜  npm-install-strategy-lab ls node_modules
accepts                 cookie-signature        es-object-atoms         get-proto               media-typer             on-finished             send                    type-is
array-flatten           debug                   escape-html             gopd                    merge-descriptors       parseurl                serve-static            unpipe
body-parser             depd                    etag                    has-symbols             methods                 path-to-regexp          setprototypeof          utils-merge
bytes                   destroy                 express                 hasown                  mime                    proxy-addr              side-channel            vary
call-bind-apply-helpers dunder-proto            finalhandler            http-errors             mime-db                 qs                      side-channel-list
call-bound              ee-first                forwarded               iconv-lite              mime-types              range-parser            side-channel-map
content-disposition     encodeurl               fresh                   inherits                ms                      raw-body                side-channel-weakmap
content-type            es-define-property      function-bind           ipaddr.js               negotiator              safe-buffer             statuses
cookie                  es-errors               get-intrinsic           math-intrinsics         object-inspect          safer-buffer            toidentifier

linkedに切り替える

$ rm -rf node_modules package-lock.json
$ npm install --install-strategy=linked
$ node index.js
  • 実行結果
➜  npm-install-strategy-lab rm -rf node_modules package-lock.json
➜  npm-install-strategy-lab npm install --install-strategy=linked
npm warn reify The "linked" install strategy is EXPERIMENTAL and may contain bugs.

up to date, audited 69 packages in 3s

18 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
➜  npm-install-strategy-lab node index.js
node:internal/modules/cjs/loader:1520
  throw err;
  ^

Error: Cannot find module 'debug'
Require stack:
- /Users/user_name/Test/npm-install-strategy-lab/index.js
(スタックトレースは省略)

Node.js v24.18.1

今度はdebugが見つからずエラーになりました。
node_modulesの中を見てみると、直下にはexpressしかなく、実体は.storeに入っています。

$ ls -la node_modules
$ ls node_modules/.store | head
➜  npm-install-strategy-lab ls -la node_modules
total 64
drwxr-xr-x@  5 user_name  staff    160 11月  1 15:24 .
drwxr-xr-x   7 user_name  staff    224 11月  1 15:24 ..
-rw-r--r--@  1 user_name  staff  29580 11月  1 15:24 .package-lock.json
drwxr-xr-x@ 70 user_name  staff   2240 11月  1 15:24 .store
lrwxr-xr-x@  1 user_name  staff     65 11月  1 15:24 express -> .store/express@4.22.3-2-X1KmunwoV-TO9CoUr6nQ/node_modules/express
➜  npm-install-strategy-lab ls node_modules/.store | head
accepts@1.3.8-MFZ0zQBPQ1Dgu0rU7Uit1w
array-flatten@1.1.1-uvGecmHInDboqjADSMWRIw
body-parser@1.20.9-J9Bw7MIqs8DCNnFrPCydBg
bytes@3.1.2-EoFGKJy3eE8TWAM2R97Oyg
call-bind-apply-helpers@1.0.2-x5zNHvahVuGXSMGW9bDw0g
call-bound@1.0.4-bb0H8WPzLbq32i7ubVGIIA
content-disposition@0.5.4-o3c8wZBvHJfn8Clka7gFHg
content-type@1.0.5-wiIOyQW-8S3B5eKYZRNqjA
cookie-signature@1.0.7-LzJyaZYK_H9OCaugU_Md5w
cookie@0.7.2-Szyj7B7h9uUCxoD8euRs_g

linkedなのにdebugが読み込めてしまったら

実は最初にlinkedで試したときはdebugが読み込めてしまいました。

$ rm -rf node_modules package-lock.json
$ npm install --install-strategy=linked
$ node index.js
  • 実行結果
➜  npm-install-strategy-lab rm -rf node_modules package-lock.json
➜  npm-install-strategy-lab npm install --install-strategy=linked
npm warn reify The "linked" install strategy is EXPERIMENTAL and may contain bugs.

up to date, audited 69 packages in 3s

18 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
➜  npm-install-strategy-lab node index.js
express と debug の読み込みに成功しました

まず、node_modulesがちゃんとlinkedの形になっているかを確認しました。

$ ls -la node_modules
  • 実行結果
total 64
drwxr-xr-x@  5 user_name  staff    160 11月  1 15:17 .
drwxr-xr-x   7 user_name  staff    224 11月  1 15:17 ..
-rw-r--r--@  1 user_name  staff  29580 11月  1 15:17 .package-lock.json
drwxr-xr-x@ 70 user_name  staff   2240 11月  1 15:17 .store
lrwxr-xr-x@  1 user_name  staff     65 11月  1 15:17 express -> .store/express@4.22.3-2-X1KmunwoV-TO9CoUr6nQ/node_modules/express

直下にはexpressのリンクと.storeしかなく、構造はlinkedになっています。
それなのにdebugが読み込めるのはおかしいので、require.resolveで読み込み元を調べてみました。

$ node -e "console.log(require.resolve('debug'))"
  • 実行結果
/Users/user_name/node_modules/debug/src/index.js

プロジェクトの外にある、ホームフォルダのnode_modulesから読み込まれていました。
ホームフォルダを見てみると、package.jsonとpackage-lock.jsonがありました。以前ホームフォルダでnpm installしたときの名残だと思います。
そのnode_modulesの中に、jestなどと一緒にdebugも入っていました。

$ ls ~/package.json ~/package-lock.json
  • 実行結果
/Users/user_name/package-lock.json /Users/user_name/package.json

親フォルダの node_modules までたどられる

Node.js のrequireは、見つかるまで親フォルダのnode_modulesを順にたどって探します。今回はこの順番で探して、ホームフォルダで見つかっていました。

/Users/user_name/Test/npm-install-strategy-lab/node_modules  ← debug なし
/Users/user_name/Test/node_modules                           ← debug なし
/Users/user_name/node_modules                                ← ここで見つかった

linkedで見えなくなるのは、あくまでプロジェクトのnode_modulesの中にある、宣言していない依存です。
プロジェクトより上のフォルダにnode_modulesがあると、そこから読み込まれるのは防げません。

回避した方法

今回の回避方法として、ホームフォルダのnode_modulesは使っていなかったので、名前を変えて一時的に退避しました。
消さずに名前を変えただけなので、戻したくなったらmvで元に戻せます。

mv ~/node_modules ~/node_modules.bak

そのうえでlinkedをやり直すと、上で書いたとおりCannot find module 'debug'になりました。

linkedにしてもdebugが読み込めてしまう場合は、node -e "console.log(require.resolve('debug'))"で読み込み元を確認すると原因がわかりやすいと思います。

nestedも試してみる

nestedは巻き上げをせず、依存されている場所にそのまま置く方式です。

$ rm -rf node_modules package-lock.json
$ npm install --install-strategy=nested
$ node index.js
  • 実行結果
➜  npm-install-strategy-lab rm -rf node_modules package-lock.json
➜  npm-install-strategy-lab npm install --install-strategy=nested

added 87 packages, and audited 88 packages in 1s

18 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
➜  npm-install-strategy-lab node index.js
node:internal/modules/cjs/loader:1520
  throw err;
  ^

Error: Cannot find module 'debug'
Require stack:
- /Users/user_name/Test/npm-install-strategy-lab/index.js
(スタックトレースは省略)

Node.js v24.18.1

nestedでもdebugが見つからずエラーになりました。

hoistedでは68個だったパッケージが、nestedでは87個に増えました。
nestedは重複をまとめないので、同じパッケージでも使われている場所ごとに別々で入るためだと思います。

node_modulesの直下を見てみました。

$ ls node_modules
$ ls node_modules/express/node_modules | head
  • 実行結果
➜  npm-install-strategy-lab ls node_modules
express
➜  npm-install-strategy-lab ls node_modules/express/node_modules | head
accepts
array-flatten
body-parser
content-disposition
content-type
cookie
cookie-signature
debug
depd
encodeurl

node_modulesの直下にはexpressしかなく、expressの依存はexpress/node_modulesの中に入っています。
debugもここにあるので、expressからは読み込めても、index.jsからは見えません。

shallowも試してみる

shallowは直接依存だけを直下に置く方式です。

$ rm -rf node_modules package-lock.json
$ npm install --install-strategy=shallow
$ node index.js
  • 実行結果
➜  npm-install-strategy-lab rm -rf node_modules package-lock.json
➜  npm-install-strategy-lab npm install --install-strategy=shallow

added 68 packages, and audited 69 packages in 4s

18 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
➜  npm-install-strategy-lab node index.js
node:internal/modules/cjs/loader:1520
  throw err;
  ^

Error: Cannot find module 'debug'
Require stack:
- /Users/user_name/Test/npm-install-strategy-lab/index.js
(スタックトレースは省略)

Node.js v24.18.1

shallowでもdebugが見つからずエラーになりました。

パッケージ数は68個で、hoistedと同じでした。
nestedは87個に増えましたが、shallowはexpressより下の階層ではまとめて置くので、重複が出ないためだと思います。

こちらもnode_modulesの直下を見てみました。

$ ls node_modules
$ ls node_modules/express/node_modules | head
$ ls node_modules/express/node_modules | grep debug
  • 実行結果
➜  npm-install-strategy-lab ls node_modules
express
➜  npm-install-strategy-lab ls node_modules/express/node_modules | head
accepts
array-flatten
body-parser
bytes
call-bind-apply-helpers
call-bound
content-disposition
content-type
cookie
cookie-signature
➜  npm-install-strategy-lab ls node_modules/express/node_modules | grep debug
debug

node_modulesの直下にexpressしかないのはnestedと同じです。
ただ、express/node_modulesの中を見ると、nestedにはなかったbytesやcall-boundが並んでいます。
これらはexpressの依存の依存で、shallowではexpressより下の階層にまとめて巻き上げられているようです。
debugもここにあるので、nestedと同じくindex.jsからは見えません。

4種類を比べてみると

それぞれのインストール直後に、ファイル数と容量も数えてみました。

$ find node_modules -type f | wc -l
$ du -sh node_modules

結果をまとめると、次のとおりです。

戦略 debugを読み込めたか node_modules直下 ファイル数 容量
hoisted 読み込めた 依存が全部並ぶ 614 4.0M
nested 読み込めなかった expressのみ 875 5.3M
shallow 読み込めなかった expressのみ 614 4.0M
linked 読み込めなかった expressのリンクと.store 614 4.0M

debugを読み込めたのはhoistedだけで、ほかの3つはすべてエラーになりました。
ファイル数と容量は、nestedだけが875ファイル・5.3Mと大きく、残りの3つは614ファイル・4.0Mで同じでした。
nestedは重複をまとめないので、そのぶん増えているのだと思います。

わかったこと

linkedだけでなく、nestedとshallowでも読み込めなかった

linkedを試すつもりで始めましたが、意外だったのはnestedとshallowでもdebugが読み込めなかったことです。
どれもnode_modulesの直下に置かれるのがexpressだけになるので、index.jsからはdebugが見えません。
package.jsonに書いていない依存が読み込めてしまうのは、4つの中ではhoistedだけでした。

エラーになったらpackage.jsonに書く

エラーになったときの対処はシンプルで、自分のコードで使うパッケージをpackage.jsonに書くことです。
今回ならnpm install debugでdebugを自分の依存として追加すれば、どの方式でも読み込めるはずです。
hoistedで読み込めていたのは、expressの依存としてたまたま直下に置かれていたからで、本来はhoistedでも書いておくべき依存でした。

それでもlinkedが勧められている理由を考えてみた

今回の結果だけ見ると、shallowでもファントム依存に気づけて、ファイル数や容量もhoistedと変わりません。
それでも公式がlinkedを勧めているのは、shallowがexpressより下の階層では巻き上げをするからではないかと思います。
shallowのexpress/node_modulesには依存の依存までまとめて並んでいたので、その中では宣言していないパッケージ同士でも見えてしまいそうです。
ただ、今回確かめたのは「自分のコードから見えるか」だけで、依存の依存同士でどう見えるかまでは確かめていません。

linkedにしても容量は変わらなかった

linkedは.storeに実体を置いてリンクを張るので、node_modulesの構造は大きく変わります。
それでも、ファイル数と容量はhoistedと同じ614ファイル・4.0Mでした。
増えたのは、重複をまとめないnested(875ファイル・5.3M)だけです。
容量の面では、linkedに切り替えるハードルは低いと感じました。

linkedでも親フォルダのnode_modulesまでは防げない

linkedにしても、プロジェクトより上のフォルダにnode_modulesがあると、そこから読み込まれてしまいました。
linkedが守ってくれるのは、あくまで自分のプロジェクトのnode_modulesの中だけです。
「linkedにしたから大丈夫」と思い込まず、怪しいときはrequire.resolveで読み込み元を確かめるのが確実だと思いました。

まとめ

普段はnpm installを実行するだけで、node_modulesの中がどうなっているかを気にしたことはほとんどありませんでした。
今回install-strategyを切り替えてみて、同じpackage.jsonでも、置き方ひとつで「コードから何が見えるか」が変わることを実感しました。
これまで当たり前に動いていたコードも、たまたま依存が巻き上げられていたおかげで動いていただけかもしれません。

また、手元の環境に思いがけず残っていたファイルが結果を左右したのも、実際に手を動かしたからこそ気づけたことでした。
ドキュメントを読むだけでは分からなかった部分なので、試してみてよかったと思います。

今後は、自分のプロジェクトでも一度linkedでインストールして、気づかないままファントム依存として使っているパッケージがないかを確かめてみたいと思います。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?