# pnpm
- 套件管理工具小背景
- **先驅者npm**
2010 年1 月發布
2020 年,GitHub 收購了npm,原則上 npm 現在歸微軟管理
- 關於npm冷知識
Q:npm 縮寫是 node package manager?
A:npm 不是任何短語的縮寫
> npm 官方闢謠:
>

- **創新者Yarn Classic (V1)**
在2016 年10 月,由Facebook 宣布與Google 和其他公司合作開發
- **Yarn Berry (>V2)**
Yarn 2 於2020年1月發布
主要創新是[即插即用(PnP)](https://yarnpkg.com/features/pnp)方法,為修復node_modules的策略
1. 不生成`node_modules` ,生成一個帶有依賴查找表的`.pnp.cjs` 文件(非嵌套結構)
2. 每個package都以**zip 文件**的形式存儲在文件夾內替代`.yarn/cache/` (省磁盤空間)
⇒ 要求維護者更新現有的package
⇒ 許多知名開發人員[公開批評Yarn 2](https://www.youtube.com/watch?v=bPae4Z8BFt8)
⇒ 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***
```html
├── docs # Documents
│ ├── api # API spec
│ ├── archived # Archived documents
│ └── ...
├── README.md
├── services
│ ├── ui
│ ├── api
│ └── ...
├── packages
│ ├── util
│ ├── core
│ └── ...
└── package.json
```
***polyrepo ⇒* dependencies 的版本管理複雜/重覆配置/共用程式碼維護成本高**
```html
ui
├── docs # Documents
│ └── ...
├── README.md
├── src
│ ├── util
│ ├── components
│ ├── containers
│ └── ...
└── package.json
api
├── docs # Documents
│ └── ...
├── README.md
├── src
│ ├── util
│ ├── router
│ ├── model
│ └── ...
└── package.json
```
****4. 安全性高****

[https://github.com/pnpm/benchmarks-of-javascript-package-managers](https://github.com/pnpm/benchmarks-of-javascript-package-managers)
## [The State of JS 2021 results : ****monorepo****](https://2021.stateofjs.com/en-US/libraries/monorepo-tools/)
## ****npm/yarn/pmpm****
- 在npm1、npm2中=>**嵌套結構**
```html
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被安裝,文件體積超大
```html
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)
```html
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?
```html
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
```html
{
"dependencies": {
"A": "^1.0.0",
"C": "^1.0.0"
}
}
```
由於B 在安裝時被提升到了和A 同樣的層級,所以在專案中引用B 能正常運作。
如果某天某個版本的A 依賴不再依賴B 或者B 的版本有所變化,那麼就會造成依賴缺失問題。
- ****依賴分身Doppelgangers****
ex: 再安裝依賴@B2.0 的D ⇒這兩個重複安裝的B 就叫doppelgangers
```html
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`。

(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官網](https://pnpm.io/)
[sample pnpm project](https://github.com/pnpm/sample-project)
[https://blog.logrocket.com/javascript-package-managers-compared/](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://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/352437367)
[https://zhuanlan.zhihu.com/p/526257537](https://zhuanlan.zhihu.com/p/526257537)