はじめに
こんにちは。システム開発推進部第4Gの西村です。
「皆さんはnpmについてどのくらい理解していますか?」恥ずかしながら、私はnpmコマンドを打つだけで内部挙動はまったく知りませんでした。そんな私が先日、モノレポ構成のプロジェクトで思いがけないエラーに遭遇しました。
今回はその経験をもとに、npmの内部挙動(hoisting)について整理してみました。
対象読者
- npmの内部挙動を理解したい方
- モノレポ構成で開発している方
ディレクトリ構成
今回のプロジェクトは以下図のような構成でした。
aichat-admin (ルートプロジェクト)
├─ aichat(OSS)
│ ├─ package.json
│ ├─ node_modules/
│ └─ packages
│ └─ data-schemas (共通ライブラリ)
│ ├─ package.json
│ ├─ node_modules/
│ └─ ...
├─ 管理画面(Client)
│ ├─ package.json
│ ├─ node_modules/
│ └─ ...
├─ 管理画面(Server)
│ ├─ package.json
│ ├─ node_modules
│ │ ├─ aichat/packages/data-schemas // シンボリックリンクによる参照
│ │ └─ ...
│ └─ ...
├─ package.json
├─ node_modules/
└─ ...
モノレポ構成としてAIチャットアプリ(OSS), 管理画面(Client), 管理画面(Server)を管理しており、 私は社内利用されているAIチャットアプリ(OSS)の管理画面機能の開発を担当していました。
発生したエラー
ある日、動作確認などの都合でいくつかの子プロジェクトでnpm installを実行した後、
いつものように管理画面(Server)のローカルサーバーを起動しようとしたらエラーが発生しました。
$ npm run dev:server
/Users/.../aichat-admin/aichat/packages/data-schemas/src/config/winston.ts:47
new winston.transports.DailyRotateFile({
^
TypeError: winston.transports.DailyRotateFile is not a constructor
at Object. (/Users/.../aichat-admin/aichat/packages/data-schemas/src/config/winston.ts:47:3)
at Module._compile (node:internal/modules/cjs/loader:1706:14)
at Object.transformer (/Users/.../aichat-admin/node_modules/tsx/dist/register-D46fvsV_.cjs:3:1104)
at Module.load (node:internal/modules/cjs/loader:1441:32)
at Function._load (node:internal/modules/cjs/loader:1263:12)
at TracingChannel.traceSync (node:diagnostics_channel:328:14)
at wrapModuleLoad (node:internal/modules/cjs/loader:237:24)
at Module.require (node:internal/modules/cjs/loader:1463:12)
at require (node:internal/modules/helpers:147:16)
at (/Users/.../aichat-admin/server/src/repositories/rcUserExtensionRepository.ts:1:106)
該当エラー箇所
import winston from 'winston';
import 'winston-daily-rotate-file';
// winston.transports.DailyRotateFile を使用
new winston.transports.DailyRotateFile({
// ...設定
});
エラーメッセージを見る限り、winston-daily-rotate-fileパッケージの初期化で失敗しているようでした。
単純にnpm installを実行しただけでエラーが発生したため、原因が分からず、かなり困惑しました。
しかし調査を進めるうちに、「npm の巻き上げ(hoisting)」が関係していることが分かりました。
npmの巻き上げ(hoisting)機能とは?
npmのhoisting機能はパッケージ管理効率化のためにnpm v3で導入された機能です。 npmの簡単な歴史をもとに、なぜそんな機能が生まれたかを見ていきたいと思います。
npm v2 までの依存構造
npm v2 では、依存関係はモジュールごとにネストされていました。
project
└─ node_modules
├─ moduleA
│ └─ node_modules
│ └─ moduleB
└─ moduleC
└─ node_modules
└─ moduleB
しかしこの構造には以下の問題がありました。
- 同じパッケージが複数回展開され、node_modules のサイズが大きくなる
- Windows の MAX_PATH 制限(260 文字)に引っかかってしまう可能性がある
npm v3以降の依存構造
そこでnpm v3では、hoisting機能が実装されました。
project
└─ node_modules
├─ moduleA
├─ moduleB
└─ moduleC
hoistingにより、同じ依存関係は上位のnode_modulesにまとめられるようになりました。 この仕組みにより、重複インストールの削減とパス長の問題を回避できるようになりました。
エラーが発生した仕組み
では、このhoistingの機能がどのように今回のエラーを引き起こしたかを当時の手順をもとに解説したいと思います。
1. 初期状態(エラー発生前)
全ての依存関係はroot配下のnode_modulesで管理していました。
import winston from 'winston'; // aichat-admin/node_modules/winston を参照
import 'winston-daily-rotate-file'; // aichat-admin/node_modules/winston を参照
// winston.transports.DailyRotateFile を使用
new winston.transports.DailyRotateFile({
// ...設定
});
2. 各子プロジェクトでnpm install
子プロジェクトでnpm installを実行したことで、新しい依存関係がインストールされ、hoistingが実行されました。
3. hoistingによるモジュールの再配置
変更前(正常動作時)
aichat-admin/
├─ node_modules/
│ ├─ winston/
│ └─ winston-daily-rotate-file/
└─ aichat/
└─ node_modules/
└─ (他の依存関係)
変更後(エラー発生時)
aichat-admin/
├─ node_modules/
│ ├─ winston/ (バージョンA)
│ └─ winston-daily-rotate-file/
└─ aichat/
└─ node_modules/
└─ winston/ (バージョンB)
4. モジュール解決の不整合
このhoisting後、以下コードでエラーが発生しました。
import winston from 'winston';
// aichat-admin/aichat/node_modules/winston を参照
import 'winston-daily-rotate-file';
// aichat-admin/node_modules/winston-daily-rotate-file を参照
// この内部でrequire('winston')が実行されると同一ディレクトリ内にあるaichat-admin/node_modules/winston を参照
// コード中でimportしているwinstonとは異なるインスタンス
// importしているwinstonにはwinston-daily-rotate-fileが追加されていないためエラー
new winston.transports.DailyRotateFile({
// ...設定
});
コマンドでモジュールの参照先を確認
node -e "console.log(require.resolve('winston'))"
/Users/.../aichat-admin/aichat/node_modules/winston/lib/winston.js
node -e "console.log(require.resolve('winston-daily-rotate-file'))"
/Users/.../aichat-admin/node_modules/winston-daily-rotate-file/index.js
つまり、hoistingによってwinstonの異なるインスタンスが参照されるようになり、winston-daily-rotate-fileの拡張が、期待するwinstonインスタンスに適用されなかったことが根本原因でした。
エラーは各node_modulesを削除した後、root配下でnpm installすれば解決しました。
まとめ
本記事では、モノレポ構成で起こったエラーをもとに、npmのhoistingついて深掘りしました。
主なポイントは以下です。
- npmのhoistingは依存関係を効率化する一方で、予期しない問題を引き起こす場合がある
- モノレポ構成では依存関係の統一管理が重要である
依存解決に関しては奥が深く、npmの内部挙動を理解して初めてエラーの原因に辿り着くことができました。
最近ではnpmの代替パッケージマネージャーであるpnpmなどもあり、依存解決の効率化やセキュリティ機能が充実しているとのことなので、今後はpnpmについても調べてみたいと思います。
皆さんも是非興味あれば調べてみてください。最後まで読んでいただきありがとうございました。
参考文献
西村拓海