首页 / Bun 入门教程 / 工作区 Workspaces

Bun 入门教程

工作区 Workspaces

本教程共 34 篇 · 第 13 篇 · 更新于 2026-08-06

BunWorkspaces工作区monorepoworkspace 协议Catalogs跨包依赖--filter

本节目标:

  • 理解 Workspaces 解决的场景:在一个仓库里开发多个相互独立的包(monorepo)。
  • 掌握根 package.jsonworkspaces 字段的写法,包括 glob 与负向排除。
  • 会用 workspace: 协议在包之间互相引用,并理解发布时版本如何被替换。
  • 了解 --filter 选择性安装/运行、依赖去重,以及用 Catalogs 统一管理多包版本。

13.1 什么是 Workspaces

Workspaces(工作区)是 npm 生态里的一套约定,Bun 也完整支持。它的核心诉求是:把多个彼此独立、却又相互依赖的包,放在同一个 Git 仓库里一起开发——也就是常说的 monorepo。

一个典型的工作区目录结构长这样:

<root>
├── README.md
├── bun.lock
├── package.json
├── tsconfig.json
└── packages
    ├── pkg-a
    │   ├── index.ts
    │   ├── package.json
    │   └── tsconfig.json
    ├── pkg-b
    │   ├── index.ts
    │   ├── package.json
    │   └── tsconfig.json
    └── pkg-c
        ├── index.ts
        ├── package.json
        └── tsconfig.json

根目录的 package.json 通过 "workspaces" 字段声明哪些子目录是工作区成员,按惯例放在 packages/ 下:

{
  "name": "my-project",
  "version": "1.0.0",
  "workspaces": ["packages/*"],
  "devDependencies": {
    "example-package-in-monorepo": "workspace:*"
  }
}

13.2 glob 与负向排除

workspaces 字段支持完整的 glob 语法,包括负向(排除)模式。这在”大部分都纳入、个别目录例外”时很方便:

{
  "name": "my-project",
  "version": "1.0.0",
  "workspaces": ["packages/**", "!packages/**/test/**", "!packages/**/template/**"]
}

上面这行表示:把所有 packages/ 下的子目录都作为工作区,但排除各自的 test/template/ 目录。

13.3 用 workspace: 协议引用本地包

每个工作区都有自己的 package.json。当包 A 依赖同仓库里的包 B 时,不要从 npm 下载,而是用 workspace: 协议(可带版本范围)来指向本地包:

// packages/pkg-a/package.json
{
  "name": "pkg-a",
  "version": "1.0.0",
  "dependencies": {
    "pkg-b": "workspace:*"
  }
}

运行 bun install 时,Bun 会把你本地的 packages/pkg-b 目录”安装”到 node_modules 里(而不是去 registry 拉取),从而让 pkg-a 直接引用本地的源码。如果 pkg-apkg-b 都依赖同一个第三方包,Bun 会把它**提升(hoist)**到根 node_modules,节省磁盘、减少版本碎片。

workspace: 协议支持几种写法,含义不同:

"workspace:*"    -> 发布时替换为 pkg 当前版本,如 "1.0.1"
"workspace:^"    -> 替换为 "^1.0.1"
"workspace:~"    -> 替换为 "~1.0.1"
"workspace:1.0.2" -> 替换为指定的 "1.0.2"(即使当前版本是 1.0.1)
Note

当你用 bun pm pack 或发布包时,Bun 会自动把 workspace: 版本替换成目标包 package.json 里真实的版本号。这让”本地用源码、发布用版本”的体验非常顺滑,无需手动改来改去。

13.4 选择性安装与脚本:—filter / —workspaces

在 monorepo 里,你可能只想给某几个包装依赖,或只在这些包里跑脚本。用 --filter 即可:

# 给所有以 pkg- 开头、但排除 pkg-c 的工作区安装依赖
bun install --filter "pkg-*" --filter "!pkg-c"

# 也可以用路径表达,等价
bun install --filter "./packages/pkg-*" --filter "!pkg-c"

--filter 既能作用于 bun install,也能作用于 bun run(运行脚本)、bun outdated 等命令,让你精确控制作用范围;如果不加过滤、想对所有工作区统一跑脚本,用 --workspaces 即可。

实际工作中,--filter 用得最多的场景其实不是安装,而是跑脚本

bun run --filter "pkg-a" build          # 只构建 pkg-a
bun run --filter "@myorg/*" build       # 构建 @myorg 作用域下的所有包
bun run --filter "*" test               # 所有工作区都跑一遍 test
bun run --filter "./apps/web" dev       # 按路径过滤,只启动 apps/web

过滤器既接受包名(子包 package.json 里的 name 字段,支持 * 通配与 ! 取反),也接受路径(以 ./ 开头)。两种写法可以混用;多个 --filter 之间是叠加关系——先用正向模式选出候选集合,再用负向模式从中剔除。这样你就能表达”除了这两个包之外全部构建”这类需求,而不必把包名一个个列出来。

13.5 依赖去重的好处

Workspaces 带来的几个实际收益:

  • 代码可拆分成逻辑单元:包与包之间用 package.json 的依赖字段显式声明关系,bun install 自动把本地包接进来。
  • 依赖去重:A 和 B 共享的第三方依赖会被提升到根 node_modules,既省磁盘,也减少”同一个包装了多个版本”的依赖地狱。
  • 多包脚本编排:配合 --filter / --workspaces,一次性在多包之间运行 build、test、lint 等任务。
Tip

官方文档给出的性能参考:Bun 在 Linux 上安装 Remix 的 monorepo 大约只要 500ms,相比 npm install 约快 28 倍、相比 yarn install(v1)约 12 倍、相比 pnpm install 约 8 倍。大仓库里安装速度的优势同样明显。

13.6 用 Catalogs 统一版本

当很多包需要同一个依赖的同一版本时,重复书写既啰嗦又容易漂移。Bun 支持在根 package.json 里定义一份 catalog(目录),各工作区用 catalog: 协议引用它:

// 根 package.json
{
  "workspaces": {
    "packages": ["packages/*"],
    "catalog": {
      "react": "^18.0.0",
      "typescript": "^5.0.0"
    },
    "catalogs": {
      "build": {
        "webpack": "^5.0.0",
        "babel": "^7.0.0"
      }
    }
  }
}
// 子包 package.json
{
  "dependencies": {
    "react": "catalog:",
    "webpack": "catalog:build"
  }
}

catalog: 引用根目录的默认目录,catalog:build 引用名为 build 的命名目录。以后只要改根目录里的版本,所有引用它的包都会跟着更新——这在维护几十个包的 monorepo 时尤其省心。

Warning

pnpm 的 catalog: 协议在从 pnpm 迁移时会被保留(见第 12 章),但语义需以 Bun 文档为准。若你同时维护多套包管理器配置,建议以 bun install 实际生成/更新后的 bun.lock 为真相来源。

13.7 一个最小可运行的跨包示例

光看配置可能还不够直观,下面给出一组能直接落地的文件,演示”包 A 引用本地包 B”。

package.json 声明工作区:

{
  "name": "my-monorepo",
  "version": "1.0.0",
  "workspaces": ["packages/*"]
}

packages/pkg-b/package.json——它是一个被依赖的库:

{
  "name": "pkg-b",
  "version": "1.0.0",
  "module": "index.ts"
}

packages/pkg-a/package.json——它依赖本地 pkg-b

{
  "name": "pkg-a",
  "version": "1.0.0",
  "dependencies": {
    "pkg-b": "workspace:*"
  }
}

运行一次 bun install,Bun 会把 packages/pkg-b 这个本地目录接入 pkg-anode_modules,于是 pkg-a 的代码里 import { ... } from "pkg-b" 就能直接解析到同仓库的源码,而不是去 registry 下载。你在 pkg-b 里改一行,pkg-a 立刻就能看到效果——这正是 monorepo 开发体验的核心:多包共享一份源码、一处修改全局生效,发布时再各自换成版本号。

Tip

想确认某个工作区到底被解析到了哪里,可以配合 bun pm ls 查看已安装包的来源与版本;怀疑链接关系不对时,删掉 node_modules 重新 bun install 通常能解决大部分”改了不生效”的问题。

13.8 常见坑与问答

工作区配置出问题时,报错信息往往不够直白。下面几个是新手最容易踩的。

坑一:子包漏写 name,或者两个子包重名。 Bun 是通过每个子目录 package.json 里的 name 字段来识别工作区成员的,workspaces 里的 glob 只负责”去哪里找”。漏写 name,这个目录就不会被当成工作区;两个子包重名,则会导致链接指向错误的目录。每个工作区包都必须有唯一的 name

坑二:glob 写错,包根本没被纳入。 "packages/*" 只匹配一层子目录。如果你的结构是 packages/group/pkg-a 这样两层,它不会被选中,需要写成 "packages/*/*""packages/**"。不确定配置有没有生效时,跑一次 bun install 后用 bun pm ls 看看本地包有没有被链接进来,比盯着 glob 反复猜要快得多。

坑三:根包忘记标记 "private": true monorepo 的根 package.json 通常只是个容器,并不打算发布。习惯上应该把它标为私有,避免某次手滑把整个仓库发布到 registry 上去。

Q:monorepo 默认用哪种 linker?

新建的工作区项目默认使用 isolated(类似 pnpm 的严格隔离)。这意味着某个包只能引用它自己声明过的依赖,从根本上杜绝了”幽灵依赖”——即某个包能跑起来只是因为它的兄弟包碰巧装了同一个库,一旦兄弟包移除依赖它就莫名其妙挂掉。旧项目为了向后兼容会保持 hoisted。想切换用 --linker 或写进 bunfig.toml 都可以。

Q:monorepo 安装太慢有什么办法?

从 Bun v1.3.14 起,isolated 策略新增了实验性的全局虚拟存储:同一个包在全局的 links 目录里只落一份实体,各项目的 node_modules 用符号链接指过去。这样热安装(锁文件在、缓存在、只是 node_modules 被清空,也就是最典型的 CI 路径)时,每个包只需要一次 symlink(),而不是逐个文件拷贝。在 bunfig.toml 里打开即可:

[install]
linker = "isolated"
globalStore = true

它默认关闭且仍在实验阶段,请先在非关键流水线上验证。收益在 CI 场景最明显,记得把缓存目录一并做持久化。

Q:整个仓库是一份锁文件还是每个包一份?

只有根目录会生成一份 bun.lock,它覆盖所有工作区。所以无论你要给哪个子包增删依赖,都应该回到仓库根目录执行 bun install,并把根锁文件一起提交。在某个子包目录里单独安装,容易让锁文件与真实依赖图脱节,这也是 CI 里冻结安装失败的常见来源。

13.9 小结

  • Workspaces 让你在单一仓库里管理多个独立包,适合 monorepo 场景。
  • package.jsonworkspaces 字段支持 glob 与负向排除;包间用 workspace: 协议引用本地源码。
  • bun install 会自动接入本地包并做依赖去重;--filter / --workspaces 用于选择性安装与脚本编排。
  • 发布时 workspace: 版本会被替换为真实版本;多包共享版本可用 Catalogs 统一管理。

下一章我们转向更进阶的依赖管理话题:全局缓存、生命周期脚本、overrides/resolutions 与为依赖打补丁。