每份 lockfile 都會被重建成依賴圖。npm 的每個 entry 以 node_modules 路徑記錄它裝在哪裡,依賴的解析方式就是 Node 自己的規則——先找自己底下的 node_modules,找不到就往上一層找,直到 root。pnpm 的圖本來就是平的:每個安裝實體以名稱加版本當鍵,依賴直接指向其他鍵。接著兩張圖依套件名比對,每次跳版分成 major/minor/patch/prerelease,並對每個變更從 root 做廣度優先搜尋,找出通往該套件的最短依賴鏈。鏈上第一個在兩版之間版本不同的節點,就是把這個變更帶進來的那一個。
major 跳級表示升級改到了第一段版號——那是慣例上唯一允許破壞性變更的級別,即使是 transitive 也值得看一眼。新進 transitive 表示這個套件沒有任何人在 package.json 宣告它,是跟著別的依賴進來的。N 個版本並存表示 resolver 沒辦法用一份副本滿足所有 range,同一個套件被裝了不止一次——多半無害,偶爾會造成狀態重複或 instanceof 對不上。來源改變表示 tarball 換了 registry 主機,合併前值得確認一下。
lockfile 常常帶著私有 registry 位址與內部套件名。解析、比對與溯源全部在你的瀏覽器裡執行——不上傳、不儲存、不進 analytics。貼上時打開 network 分頁就能驗證。
關於這個主題的常見疑問與實用解答。
左邊貼舊的、右邊貼新的。在 PR 裡,分別打開 base 分支與 head 分支的那份檔案(或用 git show main:package-lock.json 與 git show HEAD:package-lock.json)。本機 install 改了 lockfile 之後,git show HEAD:package-lock.json 就是變更前、工作目錄裡的是變更後。兩邊必須是同一種 lockfile——npm 對 npm、pnpm 對 pnpm。
直接依賴是你在 package.json 裡宣告的;transitive 依賴是 lockfile 裡其他所有東西——它們在那裡,是因為某個直接依賴(或它的依賴)需要。lockfile 大部分都是 transitive,所以 package.json 改一行就能產生幾百行變動。表格裡的 direct 標籤標的是你自己宣告的套件;沒有標籤的都是經由某條鏈進來的,溯源鏈會告訴你是哪一條。
npm 的 package-lock.json(lockfileVersion 1、2、3)與 pnpm-lock.yaml(lockfileVersion 5、6、9)。npm-shrinkwrap.json 與 package-lock.json 同格式,也能用。yarn.lock(不論是舊版 v1 文字格式或 Berry 的 YAML 格式)會被辨識出來但尚不支援,工具會明說而不是亂猜。另外 npm lockfileVersion 1 沒有 root 依賴清單,這類檔案的 direct 標籤是從頂層位置推估、不是讀 package.json。
git diff 看的是行不是套件:升級一個套件會動到它的 version、resolved、integrity 三行,加上每個引用它的 entry,同一個變更散落在整份檔案各處。GitHub 的依賴檢視會列出新增、移除、改版的套件,但只在 GitHub 上、而且只是一份平面清單——它不會告訴你 minimatch 跳 major 是因為 rimraf 升級了。這個工具吃任何你貼得出來的兩份 lockfile,依套件分組變更、分級跳版,並顯示解釋它的那條鏈。
對每個變更的套件,工具從 root 往外搜尋變更後的 lockfile(移除的情況則搜尋變更前),保留能抵達該套件的最短鏈。接著沿最短鏈從 root 往下走,停在第一個在另一版裡版本不同的節點——那個節點就是回報的原因,例如「由 rimraf 從 3.0.2 改為 6.0.1 帶動」。如果套件本身是直接宣告的,原因就是 package.json。如果鏈上完全沒有節點改版,多半是 resolver 重新解析或去重造成的位置變動,工具會照實說、不會硬編一個原因。
本身不是。當兩個依賴要求的 range 沒有任何單一版本能同時滿足時,套件管理器會兩個都裝、把其中一個巢狀放進去;lockfile 會分別記錄每份副本,這個工具把它列成「N 個版本並存」。這是正常的、多半無害。當這個套件持有共用狀態、匯出會被 instanceof 檢查的 class,或重複的那份大到影響 bundle 體積時,才值得處理。看溯源鏈可以找出是哪個依賴堅持要舊的 range。