pnpm

  • 套件管理工具小背景
    • 先驅者npm

      2010 年1 月發布

      2020 年,GitHub 收購了npm,原則上 npm 現在歸微軟管理

      • 關於npm冷知識

        Q:npm 縮寫是 node package manager?

        A:npm 不是任何短語的縮寫

        npm 官方闢謠:

        Image Not Showing Possible Reasons
        • The image file may be corrupted
        • The server hosting the image is unavailable
        • The image path is incorrect
        • The image format is not supported
        Learn More →

    • 創新者Yarn Classic (V1)

      在2016 年10 月,由Facebook 宣布與Google 和其他公司合作開發

    • Yarn Berry (>V2)

      Yarn 2 於2020年1月發布

      主要創新是即插即用(PnP)方法,為修復node_modules的策略

      1. 不生成node_modules ,生成一個帶有依賴查找表的.pnp.cjs 文件(非嵌套結構)
      2. 每個package都以zip 文件的形式存儲在文件夾內替代.yarn/cache/ (省磁盤空間)

      ⇒ 要求維護者更新現有的package

      ⇒ 許多知名開發人員公開批評Yarn 2

      ⇒ JS生態系統為PnP 提供了越來越多的支持

      Yarn Berry 還很年輕

    • 更高效的pnpm

      由Zoltan  Kochan於2017 年發布

      Fast, disk space efficient package manager

      performant npm,定義為快速的,節省磁盤空間的packages管理工具

      1. 速度快

      2. 高效率利用磁碟空間

      3. 支援monorepo ⇒ 軟體開發策略

      • 簡述

        以前後端分離的 web 開發為例,monorepo 就是把前端與後端的原始碼都放在同個 repository;而 polyrepo 則是把前端與後端分為兩個不同的 repository。

        monorepo

        ​​​​​​​​​​​​├── docs                # Documents
        ​​​​​​​​​​​​│   ├── api             # API spec
        ​​​​​​​​​​​​│   ├── archived        # Archived documents
        ​​​​​​​​​​​​│   └── ...
        ​​​​​​​​​​​​├── README.md
        ​​​​​​​​​​​​├── services
        ​​​​​​​​​​​​│   ├── ui
        ​​​​​​​​​​​​│   ├── api
        ​​​​​​​​​​​​│   └── ...
        ​​​​​​​​​​​​├── packages
        ​​​​​​​​​​​​│   ├── util
        ​​​​​​​​​​​​│   ├── core
        ​​​​​​​​​​​​│   └── ...
        ​​​​​​​​​​​​└── package.json
        

        polyrepo ⇒ dependencies 的版本管理複雜/重覆配置/共用程式碼維護成本高

        ​​​​​​​​​​​​ui
        ​​​​​​​​​​​​├── docs                # Documents
        ​​​​​​​​​​​​│   └── ...
        ​​​​​​​​​​​​├── README.md
        ​​​​​​​​​​​​├── src
        ​​​​​​​​​​​​│   ├── util
        ​​​​​​​​​​​​│   ├── components
        ​​​​​​​​​​​​│   ├── containers
        ​​​​​​​​​​​​│   └── ...
        ​​​​​​​​​​​​└── package.json
        ​​​​​​​​​​​​
        ​​​​​​​​​​​​api
        ​​​​​​​​​​​​├── docs                # Documents
        ​​​​​​​​​​​​│   └── ...
        ​​​​​​​​​​​​├── README.md
        ​​​​​​​​​​​​├── src
        ​​​​​​​​​​​​│   ├── util
        ​​​​​​​​​​​​│   ├── router
        ​​​​​​​​​​​​│   ├── model
        ​​​​​​​​​​​​│   └── ...
        ​​​​​​​​​​​​└── package.json
        

      4. 安全性高

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

https://github.com/pnpm/benchmarks-of-javascript-package-managers

The State of JS 2021 results : monorepo

npm/yarn/pmpm

  • 在npm1、npm2中=>嵌套結構

    ​​​​node_modules
    ​​​​├── A@1.0.0
    ​​​​│   └── node_modules
    ​​​​│       └── B@1.0.0
    ​​​​└── C@1.0.0
    ​​​​    └── node_modules
    ​​​​        └── B@2.0.0
    
    • 依賴地獄Dependency Hell,文件路徑過長
    • 重複的package被安裝,文件體積超大
    ​​​​node_modules
    ​​​​├── A@1.0.0
    ​​​​│   └── node_modules
    ​​​​│       └── B@1.0.0
    ​​​​├── C@1.0.0
    ​​​​│   └── node_modules
    ​​​​│       └── B@2.0.0
    ​​​​└── D@1.0.0
    ​​​​    └── node_modules
    ​​​​        └── B@1.0.0
    
  • 在npm3 、yarn (V1)中=>扁平化依賴(將子依賴提升hoist)

    ​​​​node_modules
    ​​​​├── A@1.0.0
    ​​​​├── B@1.0.0
    ​​​​└── C@1.0.0
    ​​​​    └── node_modules
    ​​​​        └── B@2.0.0
    
    • 新問題產生

      • 依賴結構的不確定性(Non-Determinism)

      不確定性是指:同樣的package.json,安裝後可能得到不同node_modules 的目錄結構。

      Q:A 依賴B@1.0,C 依賴B@2.0,安裝後究竟應該提升B 的1.0 還是C的2.0?

      ​​​​​​​​node_modules
      ​​​​​​​​├── A@1.0.0
      ​​​​​​​​├── B@1.0.0
      ​​​​​​​​└── C@1.0.0
      ​​​​​​​​    └── node_modules
      ​​​​​​​​        └── B@2.0.0
      ​​​​​​​​
      ​​​​​​​​node_modules
      ​​​​​​​​├── A@1.0.0
      ​​​​​​​​│   └── node_modules
      ​​​​​​​​│       └── B@1.0.0
      ​​​​​​​​├── B@2.0.0
      ​​​​​​​​└── C@1.0.0
      

      A:都有可能,取決於用戶的安裝順序。取決於A 和C 在 package.json中的位置,如果A 聲明在前面,那麼就是前面的結構。

      lockfile 解決不確定性

      無論是package-lock.json(npm 5.x才出現)還是yarn.lock,都是為了保證install 之後都產生確定的node_modules結構。

      lockfile 裡記錄了依賴,以及依賴的子依賴,依賴的版本,獲取地址與驗證模塊完整性的hash。

      即使是不同的安裝順序,相同的依賴關係在任何的環境和容器中,都能得到穩定的node_modules 目錄結構,保證了依賴安裝的確定性。

      • 幽靈依賴Phantom dependencies :

      幽靈依賴是指:在package.json 中未定義的依賴,但項目中依然可以正確地被引用到。

      比如上方的示例其實我們只安裝了A和C

      ​​​​​​​​{
      ​​​​​​​​  "dependencies": {
      ​​​​​​​​    "A": "^1.0.0",
      ​​​​​​​​    "C": "^1.0.0"
      ​​​​​​​​  }
      ​​​​​​​​}
      

      由於B 在安裝時被提升到了和A 同樣的層級,所以在專案中引用B 能正常運作。

      如果某天某個版本的A 依賴不再依賴B 或者B 的版本有所變化,那麼就會造成依賴缺失問題。

      • 依賴分身Doppelgangers

      ex: 再安裝依賴@B2.0 的D ⇒這兩個重複安裝的B 就叫doppelgangers

      ​​​​​​​​node_modules
      ​​​​​​​​├── A@1.0.0
      ​​​​​​​​├── B@1.0.0
      ​​​​​​​​├── C@1.0.0
      ​​​​​​​​│   └── node_modules
      ​​​​​​​​│       └── B@2.0.0
      ​​​​​​​​└── D@1.0.0
      ​​​​​​​​    └── node_modules
      ​​​​​​​​        └── B@2.0.0
      
  • pnpm =>非扁平化依賴 (硬鏈接hard link + 符號鏈接symlink ****)

    A hard link is actually the same file that it links to but with a different name. ⇒ contents

    A symbolic link is special file that points to another pre-existing file (original file). ⇒ path

​​​​將packages安裝在全局store 中,在引用項目node_modules 的依賴時,會通過硬鏈接與符號鏈接在全局store 中找到這個文件。為了實現此過程,**node_modules 下會多出非扁平化結構的 `.pnpm` 目錄**。
​​​​
​​​​- 基於**硬鏈接機制(hard link,**類似副本**)**提升package的安裝速度
​​​​
​​​​> 硬鏈接可以理解為,安裝的其實是副本,它使得用戶可以通過路徑引用查找到全局store 中的源文件。
​​​​> 
​​​​
​​​​                                                                                  
​​​​
​​​​- 使用**符號鏈接(symlink,** 類似於windows快捷方式)的非扁平的node_modules結構
​​​​
​​​​> 僅將項目的直接依賴項添加到node_modules 的根目錄下, 通過符號連接查找虛擬磁盤目錄.pnpm下的依賴包
​​​​> 
​​​​
​​​​ex: 安裝了依賴於 `foo@1.0.0` 的`bar@1.0.0`。
​​​​![](https://i.imgur.com/MCL296e.png)


​​​​
​​​​(1)pnpm 會將兩個packages硬鏈接到 `node_modules` 如下所示:
​​​​
​​​​```html
​​​​node_modules
​​​​└── .pnpm
​​​​    ├── foo@1.0.0
​​​​    │   └── node_modules
​​​​    │       └── foo -> <store>/foo
​​​​    │           ├── index.js
​​​​    │           └── package.json
​​​​    └── bar@1.0.0
​​​​        └── node_modules
​​​​            └── bar -> <store>/bar
​​​​                ├── index.js
​​​​                └── package.json
​​​​```
​​​​
​​​​> `<store>/xxx`開頭的路徑是硬鏈接,指向全局store 中安裝的依賴。
​​​​> 
​​​​
​​​​> 這是 `node_modules` 中的only "real" files。真正的文件位置 : 都是在`<package-name>@version/node_modules/<package-name>`這種目錄結構中
​​​​> 
​​​​
​​​​> 一旦所有package都硬鏈接到`node_modules`,就會創建symbolic links 來構建嵌套的依賴關係圖結構。
​​​​> 
​​​​
​​​​(2)處理symbolic links依賴 : `foo`將被符號鏈接到 `bar@1.0.0/node_modules` 文件夾:
​​​​
​​​​⇒ bar平級目錄創建foo,foo指向foo @1.0.0底下的foo
​​​​
​​​​```html
​​​​node_modules
​​​​└── .pnpm
​​​​    ├── foo @1.0.0
​​​​    │   └── node_modules
​​​​    │       └── foo  -> <store>/foo
​​​​    └── bar@1.0.0
​​​​        └── node_modules
​​​​            ├── bar -> <store>/bar
​​​​            └── foo -> ../../foo @1.0.0/node_modules/foo 
​​​​```
​​​​
​​​​(3)處理直接依賴:`bar`將被符號鏈接至根目錄的 `node_modules` 文件夾
​​​​
​​​​⇒ 頂層node_modules目錄下創建bar,指向bar@1.0.0下的bar。
​​​​
​​​​```html
​​​​node_modules
​​​​├── bar -> ./.pnpm/bar@1.0.0/node_modules/bar
​​​​└── .pnpm
​​​​    ├── foo@1.0.0
​​​​    │   └── node_modules
​​​​    │       └── foo -> <store>/foo
​​​​    └── bar@1.0.0
​​​​        └── node_modules
​​​​            ├── bar -> <store>/bar
​​​​            └── foo -> ../../foo@1.0.0/node_modules/foo
​​​​```
​​​​
​​​​(4)添加 `qar@2.0.0` 作為 `bar` 和 `foo` 的依賴項
​​​​
​​​​```html
​​​​node_modules
​​​​├── bar -> ./.pnpm/bar@1.0.0/node_modules/bar
​​​​└── .pnpm
​​​​    ├── foo@1.0.0
​​​​    │   └── node_modules
​​​​    │       ├── foo -> <store>/foo
​​​​    │       └── qar -> ../../qar@2.0.0/node_modules/qar
​​​​    ├── bar@1.0.0
​​​​    │   └── node_modules
​​​​    │       ├── bar -> <store>/bar
​​​​    │       ├── foo -> ../../foo@1.0.0/node_modules/foo
​​​​    │       └── qar -> ../../qar@2.0.0/node_modules/qar
​​​​    └── qar@2.0.0
​​​​        └── node_modules
​​​​            └── qar -> <store>/qar
​​​​```
​​​​
​​​​> 無論依賴項的數量和依賴關係圖的深度如何,即使圖形現在更深(`bar >foo > qar`),目錄深度仍然相同。
​​​​> 
​​​​
​​​​與Node 的模塊解析算法兼容,在解析模塊時,Node會忽略symlinks 執行realpath,因此當 `bar@1.0.0/node_modules/bar/index.js` 需要 foo 時,Node 不會使用在 `bar@1.0.0/node_modules/foo` 的foo,相反,foo是被解析到其實際位置(`foo@1.0.0/node_modules/foo`)。因此,只有真正在依賴項中的package才能訪問。
​​​​
​​​​解決:
​​​​
​​​​1. 幽靈依賴問題:只有直接依賴會平鋪在node_modules 下,子依賴不會被提升,不會產生幽靈依賴。
​​​​2. 依賴分身問題:packages被存儲在一個全局的store目錄下,不同的項目依賴同一個package(版本相同)時,會硬鏈接至此位置,無需重新安裝。
​​​​
​​​​## 回到最初的ex: if A 依賴B@1.0,C 依賴B@2.0
​​​​
​​​​```html
​​​​node_modules
​​​​├── .pnpm
​​​​│   ├── A@1.0.0
​​​​│   │   └── node_modules
​​​​│   │       ├── A => <store>/A@1.0.0
​​​​│   │       └── B => ../../B@1.0.0
​​​​│   ├── B@1.0.0
​​​​│   │   └── node_modules
​​​​│   │       └── B => <store>/B@1.0.0
​​​​│   ├── B@2.0.0
​​​​│   │   └── node_modules
​​​​│   │       └── B => <store>/B@2.0.0
​​​​│   └── C@1.0.0
​​​​│       └── node_modules
​​​​│           ├── C => <store>/C@1.0.0
​​​​│           └── B => ../../B@2.0.0
​​​​│
​​​​├── A => .pnpm/A@1.0.0/node_modules/A
​​​​└── C => .pnpm/C@1.0.0/node_modules/C
​​​​```
​​​​
​​​​(1)pnpm 會將all packages硬鏈接到 `node_modules`
​​​​
​​​​(2)處理symbolic links依賴 : B 將被符號鏈接到 `A@1.0.0/node_modules` 和`C@1.0.0/node_modules` 文件夾
​​​​
​​​​(3)處理直接依賴:A,C將被符號鏈接至根目錄的 `node_modules` 文件夾

參考資料:

pnpm官網

sample pnpm project

https://blog.logrocket.com/javascript-package-managers-compared/

https://medium.com/@mitalisg/what-is-the-difference-between-a-hard-link-and-a-symbolic-link-e4043996047a

https://zhuanlan.zhihu.com/p/352437367

https://zhuanlan.zhihu.com/p/526257537