用 peerDepsExternal 處理 Vite / Rollup 打包時的 peer dependency
前言
在開發一個 library 或工具套件時,常常會想把 React、Vue 這類框架或其他大型依賴列在 peerDependencies,而不是直接打包進去。這樣做的好處是讓安裝的人自己提供這些依賴,不會造成版本衝突,也不會讓 bundle 變得臃腫。
但光是在 package.json 裡列出 peerDependencies 是不夠的。打包工具(Vite 或 Rollup)並不會自動知道這些模組要 external 掉,還是需要手動去設定 external。這時 rollup-plugin-peer-deps-external 就派上用場了。
什麼是 peerDepsExternal
rollup-plugin-peer-deps-external 是一個 Rollup 插件,會自動讀取 package.json 裡的 peerDependencies,並把這些套件標記為 external,讓它們不會被打包進最終的 bundle。
Vite 的 library mode 底層是 Rollup,所以這個插件在 Vite 裡也可以直接用。
安裝
pnpm install --save-dev rollup-plugin-peer-deps-external
基本用法
Vite(Library Mode)
// vite.config.ts
import { defineConfig } from 'vite'
import peerDepsExternal from 'rollup-plugin-peer-deps-external'
export default defineConfig({
plugins: [
peerDepsExternal(),
],
build: {
lib: {
entry: 'src/index.ts',
name: 'MyLib',
formats: ['es', 'cjs'],
},
},
})
純 Rollup
// rollup.config.js
import peerDepsExternal from 'rollup-plugin-peer-deps-external'
export default {
input: 'src/index.ts',
output: [
{ file: 'dist/index.cjs.js', format: 'cjs' },
{ file: 'dist/index.esm.js', format: 'es' },
],
plugins: [
peerDepsExternal(),
],
}
只要 package.json 裡有 peerDependencies,插件就會自動把它們全設為 external,不需要再手動列一遍。
什麼時候該用
有以下情況時,這個插件特別有用:
- 開發 npm 套件或 library:不想把 React、Vue、lodash 這類常見依賴打包進去,讓使用者自己安裝。
- 避免版本衝突:如果你的 library 和使用者的專案各自打包了一份 React,可能會導致 hook 失效或其他奇怪的問題。
- 減少 bundle 大小:把大型依賴排除在外,可以大幅縮小最終輸出的檔案。
如果你是在開發應用程式(application)而不是 library,這個插件通常不需要,因為應用程式本來就要把所有依賴打包進去。
使用的優點
- 設定簡單:不需要手動維護
external清單,只要維護好peerDependencies就行。 - 不易出錯:新增或移除 peer dependency 時,不用同步更新 Rollup / Vite 設定。
- 保持 bundle 乾淨:確保套件的消費者(consumer)能用到自己版本的依賴,不會被強制鎖定你打包時的版本。
如何處理 Node.js 原生模組
如果你的 library 有用到 Node.js 的內建模組,例如 fs、path、crypto、os 等,這些模組不需要也不應該被打包進去,因為它們在 Node.js 環境下本來就存在。
peerDepsExternal 不會自動處理這些內建模組,需要另外設定。
方法一:手動在 external 裡列出
// vite.config.ts
import { defineConfig } from 'vite'
import peerDepsExternal from 'rollup-plugin-peer-deps-external'
export default defineConfig({
plugins: [peerDepsExternal()],
build: {
lib: {
entry: 'src/index.ts',
formats: ['es', 'cjs'],
},
rollupOptions: {
external: ['fs', 'path', 'os', 'crypto', 'stream', 'url'],
},
},
})
方法二:使用正則表達式自動排除所有 node: 前綴的模組
Node.js 新版(v14.18.0+)支援用 node: 前綴來引入內建模組,例如 import fs from 'node:fs'。可以用正則來一次處理:
rollupOptions: {
external: [/^node:/],
}
如果你的程式碼同時用了有前綴和沒前綴的寫法,可以兩個都加:
import { builtinModules } from 'module'
rollupOptions: {
external: [
...builtinModules,
...builtinModules.map(m => `node:${m}`),
],
}
builtinModules 是 Node.js 提供的所有內建模組清單,這樣可以確保不遺漏任何一個。
完整範例
// vite.config.ts
import { defineConfig } from 'vite'
import peerDepsExternal from 'rollup-plugin-peer-deps-external'
import { builtinModules } from 'module'
export default defineConfig({
plugins: [peerDepsExternal()],
build: {
lib: {
entry: 'src/index.ts',
formats: ['es', 'cjs'],
fileName: (format) => `index.${format}.js`,
},
rollupOptions: {
external: [
...builtinModules,
...builtinModules.map(m => `node:${m}`),
],
},
},
})
搭配這個設定,peerDependencies 裡的第三方套件和所有 Node.js 內建模組都會被正確排除在 bundle 之外。
小結
rollup-plugin-peer-deps-external 做的事情很單純:把 peerDependencies 自動設為 external。簡單、省事,而且幾乎是開發 library 時的標配。
搭配 builtinModules 來排除 Node.js 原生模組,基本上就覆蓋了大多數 library 打包時會遇到的 external 問題。