preview image

我遇到了只會在pnpm上遇到的問題

先說一下背景好了

最近隨著vite對React Server Components的支援,我開始研究vite-rsc, 並基於官方的example造了一個輪自來用,因為我想要放棄維護原本基於Next.js的部落格,改用這個來造自己的個人網站。

隨著專案程式的加入,我遇到一些需求,例如我會想要讓我的rehype/remark plugin獨立於一個套件底下,這時候就得借助pnpm的workspace功能來達成monorepo的效果了。 因此,我開始把我的repo轉成repo,使用我套件的放在apps/底下,基於 vite-rsc的專案放在packages/底下,並使用tsdown打包。檔案結構大概長成這個樣子

tree .

.
├── apps
│   └── ouo
│       ├── eslint.config.ts
│       ├── package.json
│       ├── src/
│       ├── tsconfig.json
│       └── vite.config.ts
├── packages
│   ├── eslint
│   │   ├── package.json
│   │   └── src/
│   └── rpress
│       ├── eslint.config.ts
│       ├── package.json
│       ├── src/
│       ├── test/
│       ├── tsconfig.json
│       └── tsdown.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── package.json
└── README.md

他就是一個 monorepo,然後我把用到RSC寫的網頁放到apps/ouo底下,然後在packages/rpress放了依賴於vite-rsc並增加許多實現的地方。

遷移過程都蠻順利的,直到我把放在root的package.json中對於vite-rsc的依賴刪除,讓他只放在packages中我寫的套件中。

然後問題就出現了。

> @vitejs/plugin-rsc-examples-starter@0.0.0 dev /home/bntw/Programing/website-v6/apps/ouo
> vite

Failed to resolve dependency: @vitejs/plugin-rsc/vendor/react-server-dom/client.browser, present in client 'optimizeDeps.include'
Failed to resolve dependency: @vitejs/plugin-rsc/vendor/react-server-dom/client.edge, present in ssr 'optimizeDeps.include'
Failed to resolve dependency: @vitejs/plugin-rsc/vendor/react-server-dom/server.edge, present in rsc 'optimizeDeps.include'
Failed to resolve dependency: @vitejs/plugin-rsc/vendor/react-server-dom/client.edge, present in rsc 'optimizeDeps.include'

  VITE v7.1.3  ready in 287 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

Debug 過程

到這邊其實我一直摸不著頭緒,一來是 optimizeDeps.include 這個東西應該會在vite-rsc的plugin裡面被設定好的,怎麼我這邊用就沒辦法過, 我也是直接用官方的範例,二來是我在packages中也有安裝這個套件,理論上應該不會有問題才對。

其實解法有幾個

  1. 把 @vitejs/plugin-rsc 的依賴加進 apps/ouo 的 package.json 中,並把 packages/rpress 的 package.json中加入 peer dependency,就像是我們要對react和 react-dom做的那樣
  2. 把 @vitejs/plugin-rsc 的依賴加進 root 的 package

但這樣作就太low了對吧。

所以我就朝著看能不能讓tsdown把這檔案打包進來的方向去找解答,想當然爾,失敗了。

然後我就在 package中無聊看看有什麼option可以跟dependencies有關,ㄟ不說還被我發現了 bundledDependencies。 根據我查到的資料,bundledDependencies 可以用來指定哪些依賴會被打包進去,所以我就把 "@vitejs/plugin-rsc" 加進去packages/rpress的 bundledDependencies 了。

然後問題一樣沒解決,我就用關鍵字pnpm bundledDependencies去查了一下,發現這個選項似乎並不會被pnpm所支援。

接著我就在 pnpm-workspace.yaml 中加入了 nodeLinker: hoisted, bump,問題被我誤打誤撞的解決了。

之後測試發現,就算我不把 @vitejs/plugin-rsc 加進 packages/rpress 的 bundledDependencies 中,問題依然可以被解決了,這就讓我不禁好奇,究竟是為什麼問題造成這種狀況呢?

造成原因

我們都知道 pnpm 的特性,會將所有的依賴都安裝到一個 store 中,然後在每個專案中使用 symlink 的方式來引用這些依賴。 這樣一來可以節省空間,二來也可以避免版本衝突的問題。

另外一個特性就是 node_modules是遞回式的,這樣一來可以讓每個專案都可以有自己的 node_modules,這樣就能避免幽靈依賴的問題。 問題就出在這,有的依賴可能在某個專案中被安裝了,但在另一個專案中卻找不到,這樣就會導致依賴解析失敗。

而 nodeLinker就是把結構扁平化,正如 npm 一樣。

這點我們可以從 指令定 ls -1 node_modules/ | wc -l 中看到,一旦我們這樣設定,如果我們有設定 hoisted,root底下的 node_modules 裡面裝的套件就會非常之多。

我們可以從下面至指令看到相關數據。

$ pnpm install && ls -1 node_modules/ | wc -l # without hoisted

Scope: all 4 workspace projects
Lockfile is up to date, resolution step is skipped
Packages: +300
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 300, reused 300, downloaded 0, added 300, done

devDependencies:
+ prettier 3.6.2
+ turbo 2.5.6

╭ Warning ───────────────────────────────────────────────────────────────────────────────────╮
│                                                                                            │
│   Ignored build scripts: esbuild.                                                          │
│   Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.   │
│                                                                                            │
╰────────────────────────────────────────────────────────────────────────────────────────────╯

Done in 533ms using pnpm v10.11.1
2

$ rm -rf **/node_modules && echo "nodeLinker: hoisted" >> pnpm-workspace.yaml && pnpm install && ls -1 node_modules/ | wc -l # with hoisted

Scope: all 4 workspace projects
Lockfile is up to date, resolution step is skipped
Packages: +301
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 0, reused 300, downloaded 0, added 301, done

╭ Warning ───────────────────────────────────────────────────────────────────────────────────╮
│                                                                                            │
│   Ignored build scripts: esbuild.                                                          │
│   Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.   │
│                                                                                            │
╰────────────────────────────────────────────────────────────────────────────────────────────╯

Done in 579ms using pnpm v10.11.1
221

差別還是挺大的對吧。