Windows 的 preserve-symlinks 死亡陷阱:NestJS 被載入了兩份
Monorepo 遷移後最兇的一個雷,症狀完全不知所云:
Nest can't resolve dependencies of ... ModuleRef
Nest can't resolve dependencies of ... ConfigService
DI 容器說找不到 ConfigService——但它明明就註冊在那裡。這種「框架自己的東西對不起來」的錯誤,通常代表一件事:同一個套件被載入了兩份。
破案過程
monorepo 的後端套件透過 node_modules/@forge/* 的 symlink 消費。Node 預設會把模組路徑 resolve 成真實路徑(realpath),而模組快取是以路徑字串當 key。
Windows 的殺著在這裡:fs.realpath 回傳的磁碟機代號是小寫(c:\...),而 process.cwd() 給的是大寫(C:\...)。於是同一個 @nestjs/core,一部分程式從 C:\ 的路徑載入、另一部分從 c:\ 載入——兩個路徑字串、兩份模組實例。NestJS 用 class 當 DI token,你註冊的是 A 份的 ConfigService,要求注入的是 B 份的——當然對不起來。
解法與它的反面
讓 Node 不要 resolve symlink:--preserve-symlinks --preserve-symlinks-main。所有會載入平台套件的執行入口都要帶(dev/prod/migration CLI),單一路徑寫法 → 單一實例。
有趣的是這個 flag 在 Linux 上反過來害人:.bin/ 裡的 CLI 工具本身是 symlink,保留 symlink 路徑後它們內部的相對 require 反而解析不到。所以正解不是「永遠開」,而是「Windows 開發環境開、Linux 不用」——跨平台的坑,經常沒有一體適用的答案,只有搞懂機制後的對症下藥。
這個雷值得記住的原因:錯誤訊息(DI 解析失敗)與根因(檔案系統大小寫)之間隔了四層抽象。遇到「不可能發生的錯誤」時,先懷疑有東西存在兩份。