為什麼使用 Turborepo 同時需要使用 pnpm workspace?

不能夠只使用 Turborepo 嗎?

Leo Chiu
手寫筆記

--

前言

會想要寫這篇文章的原因是想解惑為什麼 pnpm workspace 作為一個 monorepo 的解決方案還需要跟 Turborepo 一起使用,反過來說不能只用 Turborepo 作為 monorepo 的解決方案嗎?

最近關注到一個 GitHub 專案 TypeHero,我在這個專案中看到了 Turborepo 跟 pnpm workspace 一起使用便有了上述的問題,有種同一件事為什麼要做兩次的困惑。

其實這個問題在官方文件中的 monorepo — What turborepo is not 就有提到「Turborepo 並不會處理套件下載相關的功能」,如果需要下載套件則是用像是 npmyarnpnpm 之類的工具。所以對於 Turborepo 來說,像是 pnpm workspace 這類工具的定位是在幫忙管理各個 worspace 的套件版本,以及處理套件下載相關的事情。

好啦!已經知道了這個問題的答案,接下來還要寫什麼呢?

關於後面的內容會著重在看資料的同時,也看到一些有趣的東西,所以整理成一篇文章跟大家分享。

pnpm 簡介

在了解為什麼要使用 pnpm 之前,先來了解誰在用 pnpm:

  • Vue (pnpm workspace)
  • Nuxt (pnpm workspace)
  • Next.js (pnpm workspace + turbo)
  • Svelte (pnpm workspace)
  • Solid (pnpm workspace + turbo)
  • Astro (pnpm workspace + turbo)
  • Qwik (pnpm workspace)
  • Vite (pnpm workspace)
  • Turbo (pnpm workspace + turbo)

大家看完這些清單應該都矇了吧!不就是現在前端主流的框架都在用 pnpm 了嗎?

為什麼許多開源專案都願意使用 pnpm 作為 package manager 呢?先從 pnpm 解決的問題開始說起,從官方文件中我們可以看到 pnpm 專案的動機有三個。

1. 節省硬碟空間

pnpm 會使用硬連結 (hard link) 將相同版本的套件放到了全域的空間,所以當 A 跟 B 專案都使用了同版本的套件時,就會使用 hard link 存取共同的套件。

而如果使用了不同版本的套件時, pnpm update 會只把有更動的檔案放到該專案的資料夾中,所以如果該套件有 100 個檔案,而變動的只有 1 個檔案,則只會有一個檔案會被放到該專案的資料夾。

2. 加快安裝套件速度

簡單來說,pnpm 會平行化處理分析套件、下載、硬連結套件三個流程,而且因為有硬連結的關係,所以當套件已經存在於全域的空間時,就不用再次下載套件。

https://pnpm.io/motivation

3. 解決扁平化 node_modules 的問題

在 npm3 (2015 年) 之後 node_modules 的檔案結構被攤平了,為了是解決 npm2 是用巢狀的方式安裝套造成巢狀地獄冗余套件的問題。

https://npm.github.io/how-npm-works-docs/npm3/how-npm3-works.html

pnpm 的作者則是覺得攤平 node_modules 並不是唯一的解法,而且攤平其實會造成其他額外的問題:

  • 會有幽靈依賴 (phantom dependencies) 的問題
  • 攤平套件的演算法非常複雜,意味著需要花費大量時間在計算上

幽靈依賴這個問題簡單來說是當我們安裝了 A 套件,而 A 套件依賴了 B 套件,這時候我們就可以在專案中 import B 套件。在使用 npm 或 yarn 經常會發生這個問題,明明沒有安裝這個套件,卻可以使用這個套件,這是因為 npm 跟 yarn 都採用了攤平 node_modules 的演算法。

pnpm 用了不同的方式解決 npm2 巢狀的問題。直接舉一個例子,使用 npm add express 之後會在 node_modules 新增許多 express 的相依套件,但是 pnpm add express 卻只會在 node_modules 的第一層看到 express 這個套件:

node_modules/
.pnpm
.modules.yaml
express

實際上 express 的相依套件都被使用軟連結 (symlinks) 放到了 .pnpm 這個資料夾裡面了,然後 .pnpm 裡面的檔案會再使用硬連結放到全域的空間。在這個設計下 pnpm 不僅解決了幽靈依賴的問題,而且因為使用了軟硬連結的設計,也同時節省了硬碟的空間

在使用 pnpm 時也有可能會發生幽靈依賴的問題,但這是因為我們改了 pnpm link 套件的方式,在 .npmrc 中可以加入以下設定,這兩個設定都會讓 pnpm 發生幽靈依賴的問題,不過因為是自行設定的,所以發生問題也是在預期之中:

node-linker=hoisted
shamefully-hoist=true

pnpm workspace

pnpm 內建 monorepo 的能力,也就是 pnpm workspace,在使用上很直覺,在 pnpm-workspace.yaml 定義 monorepo 的資料夾即可:

packages:
# all packages in direct subdirs of packages/
- 'packages/*'
# all packages in subdirs of components/
- 'components/**'
# exclude packages that are inside test directories
- '!**/test/**'

在 monorepo 中想要使用其他的 workspace 的套件時,可以使用 workspace protocol:

{
"dependencies": {
"foo": "workspace:*",
"bar": "workspace:~",
"qar": "workspace:^",
"zoo": "workspace:^1.5.0"
}
}

如果不使用 workspace protocol 也可以,只是可能會有風險使用到錯誤的套件,例如依賴了 foo^2.0.0 這個套件,如果在 workspace 中有這個套件,pnpm 就會從 workspace 中下載這個套件,但如果沒有的話則會從 npm registry 上下載這個套件。所以為了避免使用到錯誤的套件,建議使用 workspace protocol 指定依賴的套件。

管理 monorepo 套件的發布流程 pnpm 官方推薦使用 changesetsRush 來管理。changesets 已經行之有年,許多開源專案都採用這個管理工具;而 Rush 則是 Microsoft 發布的套件,有公司的支援,也許讓人比較放心。

Next.js migrate 至 pnpm 的原因

在 2022/05 的時候 Next.js 在這個 PR#37259 從 yarn 轉移到了 pnpm,原因是使用 pnpm 幫助他們降低了下載套件的大小,而且找到了一些幽靈依賴,並在 CI 上安裝套件的速度從 4 分鐘降低到了 2 分鐘。

Turborepo 簡介

Turborepo 是 Vercel 基於 Golang 開源的 monorepo 解決方案,其核心理念是想要解決 monorepo 擴展的問題

在 monorepo 中,每個 workspace 會有 test、lint、build 等等不同任務,隨著軟體的增長,這些任務也會隨之增加。

在 CI 中這些任務可能會有執行順序,不同的 packages 也有相互依賴的關係,而 Turborepo 可以幫助我們很好的管理建構流程,透過已經在 package.json 中寫好的 scripts 與 turbo.json就可以讓任務執行的速度最大化

https://turbo.build/repo/docs/core-concepts/monorepos

Package installation

在文章的開頭就已經說過「Turborepo 並不會處理套件下載相關的功能」,而是讓處理這些事情更好的 package manager 來做這件事,Turborepo 支援的 package manager 共有四個:

而 package manager 主要做得有兩件事,分別為管理 workspace安裝套件

但這些 package manager 在 CI 上並沒有辦法很有效率的處理任務,在 CI 中或是在 local 開發有許多相互依賴的任務,Turborepo 內部有針對任務優化的流程,我們只需要在 turbo.json 中定義清楚依賴的關係以及需要執行的 script,Turborepo 就會幫我們處理好所有的事情。

https://turbo.build/repo/docs/core-concepts/monorepos/running-tasks

Caching task

如果單純使用 pnpm workspace,在執行 linttest 這些任務時並沒有 cache 相關的功能,所以重複執行這些任務就是花費等價的時間,而且如果有許多 workspace 都需要執行這些任務時,將會花費更多的時間。

https://turbo.build/repo/docs/core-concepts/monorepos/running-tasks

Turborepo 就像是中控台,會偵測執行這些任務的 stderrstdout ,並在任務變動或是沒有 cache 的時候重新把結果加到 cache 裡面,在下次執行一樣的任務時就不在執行一次,而是從 cache 中把上次執行任務的輸出再次顯示在 terminal 上。

而且同時還具有平行處理的功能,在執行多個 workspace 的任務時會偵測其依賴關係,並且自動執行。

看到這邊與其說 Turborepo 是一個 monorepo 的建構工具,不如說它是讓 package manager 提供的 workspace 功能更加完整而且更好用的工具,實際上提供 monorepo 的功能還是 package manager 本身。

是否要選擇 pnpm?

在過去 pnpm 的下載速度以及許多優點讓很多開源專案使用了 pnpm,但是現在如果是速度的考量,也許 pnpm 已經不再有明顯的優勢,在 2023 年 10 月時 yarn 4 發布後,benchmark 的結果顯示 pnpm 必沒有非常顯著的優勢,其差距只在伯仲之間。

但 pnpm 還是有硬碟空間上的優勢,如果非常在意空間使用的話,pnpm 也許還是可以作為你的第一個選擇。

https://pnpm.io/zh-TW/benchmarks

然而 workspace 並不是 pnpm 的專利,實際上 npm 跟 yarn 也有 workspace 的功能,所以如果是為了 workspace 的功能沒有必要轉移到 pnpm,可使用既有的 package manager。

是否要選擇 Turborepo?

如果近期要導入 monorepo 的話通常還會搜尋到另一個工具 Nx,Nx 在執行任務上的速度從 benchmark 來看是勝過 Turborepo 的,而且 Nx 支援更多的情境而且功能更為豐富。

從下載量來看 Nx 每週的下載量有將近 400 萬,而 Turburepo 是 140 萬左右,如果是一年前 Turborepo 的使用人數還只有幾十萬,可能會是一個考量的點,但是在一年內其下載量多了將近 100 萬,社群成長的速度也是不容小覷的。

https://github.com/vercel/turbo/tree/main/packages

但在使用上 Nx 比 Turborepo 複雜一些,文件在閱讀上也更為辛苦,以上手程度 Turborepo 是更為簡單的。

如果你正要開始導入 monorepo 的話也許可以從 Turborepo 開始嘗試,如果想要讓 CI 以及本地開發有更極致的速度,可以再去看看 Nx 提供的解決方案。

總結

這篇文章主要想解釋的是為什麼 Turborepo 跟 pnpm workspace 一起使用的問題,在看完文章之後你可以知道 Turborepo 並沒有提供下載套件以及管理 workspace 套件的功能,真正提供 monorepo 功能的是 pnpm workspace 本上,而 Turborepo 是讓 monorepo 有更佳的開發體驗,不論是在雲端上或是本地開發上。

在文章中我們還瞭解了 pnpm 想解決的三個問題,分別是節省硬碟空間、加快安裝套件速度、解決扁平化 node_modules 的問題,因為在其軟硬連結的設計上還順便解決了幽靈依賴的問題。

Turborepo 作為 Vercel 開源的 monorepo 解決方案,解決了 workspace 在開發體驗以及速度上瓶頸,可以偵測任務的依賴關係依序執行,並且同時提供平行處理及 cache 的優化,在雲端上同樣也支援 cache 的功能。

現在有蠻多開源專案都使用 pnpm workspace + Turborepo 這個組合,如果想要在專案中導入 monorepo 也許可以從這個組合下手。

--

--

Leo Chiu
手寫筆記

每天進步一點點,在終點遇見更好的自己。 Instragram 小帳:@leo.web.dev