はじめに
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を書いていません。
{
"name": "npm-install-strategy-lab",
"version": "1.0.0",
"private": true,
"main": "index.js",
"dependencies": {
"express": "4.22.3"
}
}
そのうえで、index.jsでわざとdebugをrequireしています。
これがファントム依存です。
// 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でインストールして、気づかないままファントム依存として使っているパッケージがないかを確かめてみたいと思います。