用 peerDepsExternal 處理 Vite / Rollup 打包時的 peer dependency

用 peerDepsExternal 處理 Vite / Rollup 打包時的 peer dependency

2024-06-21

前言

在開發一個 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,不需要再手動列一遍。


什麼時候該用

有以下情況時,這個插件特別有用:

  1. 開發 npm 套件或 library:不想把 React、Vue、lodash 這類常見依賴打包進去,讓使用者自己安裝。
  2. 避免版本衝突:如果你的 library 和使用者的專案各自打包了一份 React,可能會導致 hook 失效或其他奇怪的問題。
  3. 減少 bundle 大小:把大型依賴排除在外,可以大幅縮小最終輸出的檔案。

如果你是在開發應用程式(application)而不是 library,這個插件通常不需要,因為應用程式本來就要把所有依賴打包進去。


使用的優點

  • 設定簡單:不需要手動維護 external 清單,只要維護好 peerDependencies 就行。
  • 不易出錯:新增或移除 peer dependency 時,不用同步更新 Rollup / Vite 設定。
  • 保持 bundle 乾淨:確保套件的消費者(consumer)能用到自己版本的依賴,不會被強制鎖定你打包時的版本。

如何處理 Node.js 原生模組

如果你的 library 有用到 Node.js 的內建模組,例如 fspathcryptoos 等,這些模組不需要也不應該被打包進去,因為它們在 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 問題。