monorepo 中如何管理依赖?

monorepo 的优势在 repo 之间的共享复用、规范统一管理等方面,而随着项目的规模增长,repo 的依赖处理逻辑也会随着迭代而复杂

那 monorepo 中如何管理依赖呢?是将 repo 中的共同依赖安装至根目录的 package.json 中?还是将依赖安装至各 repo 的 package.json 中?

在需要的 repo 中安装依赖

对于大部分 monorepo 项目来说,直接在需要依赖项的 repo 中的 package.json 显示声明,无论是外部依赖还是内部的 repo 之间的依赖,即便 repo 之间的依赖是相同的。

在多个 repo 中安装相同依赖,可以使用以下命令:

npm install jest --workspace=web --workspace=mobile --save-dev

yarn@v1:

yarn workspace web add jest --dev
yarn workspace mobile add jest --dev

yarn@v2

yarn workspaces foreach -R --from '{web,@repo/ui}' add jest --dev
pnpm install jest --save-dev --recursive --filter=web --filter=mobile

需要注意的是:不同的包管理器在选择依赖安装的 node_modules 的位置不同(依赖提升)

优势

  • 可维护性: 每个 repo 中的 package.json 都会声明需要的依赖,开发者可以更容易地理解和处理依赖。
  • 灵活性: 在大型、复杂的 monorepo 中,保持相同版本的依赖是比较困难的。不同的 repo 的迭代优先级不一致,比如 web 和 ui 需要升级 react 版本,而 web 可能仍在功能迭代中,ui 则可以提前发布相关变更。

简洁的根目录依赖

按上述策略安装依赖时,将会减少 workspace 根目录中的依赖。根目录中需要的依赖项是用于管理项目的工具,而用于构建和库的依赖项则安装在各自的包中。

一些适合安装在 workspace 很目录中的依赖:turbo、husky、lint-staged 等

依赖管理参考

https://github.com/vercel/turborepo/tree/main
https://github.com/vuejs/core/tree/main

幽灵依赖

如果你使用的是 npm 或 yarn@v1 时,安装依赖会默认提升至根目录中的 node_modules 中,此时若 mobile 依赖了 lodash 且未声明时,lodash 将成为 mobile 的 幽灵依赖,若后续迭代中,web 取消了对 lodash 的依赖,那么在开发或运行 mobile 时将会遇到错误。

├── apps
│   ├── mobile (dependencies: {axios: '^1.7.7'})
│   └── web (dependencies: {lodash: '4.17.21'})
└── package.json

如何统一依赖版本?

Catalogs

Catalogs 起初是在 Vite Conf 2023 中提出,于今年 7.8 号发布: pnpm@9.5,所以使用 Catalogs 协议请保持你的 pnpm 版本 >= 9.5.0

Catalogs 工作于 workspace,用于将依赖版本的范围定义为可重用的变量,在 pnpm-workspace.yaml 中定义,package.json 中使用。

优势

在 workspace 中,不同的包之间会使用相同的依赖项,使用 Catalogs 协议可有效减少重复工作

  • 统一版本: 在一个 workspace 中,通常希望包之间的依赖可以使用同一个版本,使用 Catalogs 可以更方便的维护版本统一性
  • 减少升级工作量和代码合并冲突: 在升级或降级依赖项时,只需要操作 pnpm-workspace.yaml 而不是每个包的 package.json,可有效减少冲突的发生。

使用

Catalogs 在 pnpm-workspace.yaml 中定义,有两种定义 Catalogs 的方法。通过 catalog:name 在 package.json 中进行引用。

支持协议的字段

  • dependencies
  • devDependencies
  • optionalDependencies
  • pnpm.overrides

默认 Catalog

对于默认 Catalog 可以通过 catalog:default 进行引用,也可以简写为 catalog:。

catalog: 协议可理解为直接编写版本范围 ^18.3.1,

pnpm-workspace.yaml
packages: - packages/* # Define a catalog of version ranges. catalog: react: ^18.3.1 react-dom: ^18.3.1
package.json
{ "name": "@example/app", "dependencies": { "react": "catalog:", // 或者 "react-dom": "catalog:default" } }

可命名的 Catalogs

pnpm-workspace.yaml 顶层中不仅可以定义默认的 catalog 也可以定义具名的 catalogs

pnpm-workspace.yaml
catalog: react: ^16.14.0 react-dom: ^16.14.0 catalogs: # Can be referenced through "catalog:react17" react17: react: ^17.0.2 react-dom: ^17.0.2 # Can be referenced through "catalog:react18" react18: react: ^18.3.1 react-dom: ^18.3.1
package.json
{ "name": "@example/components", "dependencies": { // 使用 默认配置 "react": "catalog:", // 使用 具名配置 "react-dom": "catalog:react18" } }

发布后将变为以下内容:

package.json
{ "name": "@example/components", "dependencies": { "react": "^16.14.0", "react-dom": "^18.3.1" } }

使用 codemod 快速重构项目为 Catalogs 协议

在 workspace 的根目录下运行此命令,可快速将项目中 package.json 的版本协议替换为 默认 catalog:,并在 pnpm-workspace.yaml 修改或添加 catalog

pnpx codemod pnpm/catalog

需要注意!该命令会将 workspace 中的所有依赖都转换为 catalog 的默认协议(同依赖不同版本除外),请谨慎执行

A package.json
{ "name": "@example/A", "dependencies": { "react": "^16.14.0", "react-dom": "^18.3.1" } }
b package.json
{ "name": "@example/B", "dependencies": { "react": "^18.3.1", "react-dom": "^18.3.1" } }

执行完命令后:

pnpm-workspace.yaml
catalog: react-dom: ^18.3.1
A package.json
{ "name": "@example/A", "dependencies": { "react": "^16.14.0", "react-dom": "catalog:" } }

参考链接

ViteConf 2024

ViteConf 2024 活动将于 10 月 3 日举行,感兴趣的到时候可以关注一下。

https://viteconf.org/