airi 的 pnpm Workspaces 实战:monorepo 工作区配置、workspace 协议与依赖过滤
本篇指南以 airi 仓库内的 pnpm Workspaces 参考文档为骨架,系统讲解 pnpm 对 monorepo(多包仓库)的原生支持:如何在仓库根目录定义工作区、如何用 workspace: 协议引用内部包、如何用 --filter 精确控制命令执行范围,以及工作区级配置项(Workspace Settings)与发布时的协议转换规则。结合 airi 这一包含 40 余个内部包的 Vue/TypeScript monorepo 的实际配置文件,你将掌握一套可直接复制到大型前端 monorepo 的 pnpm 工作区管理与构建编排方案。
工作区的定义:pnpm-workspace.yaml
pnpm 通过工作区(workspaces)内置支持 monorepo。启用方式是在仓库根目录创建 pnpm-workspace.yaml,用 glob 模式声明哪些目录是工作区成员。一个典型的最小配置如下(来自参考文档的通用示例):
packages:
# Include all packages in packages/ directory
- 'packages/*'
# Include all apps
- 'apps/*'
# Include nested packages
- 'tools/*/packages/*'
# Exclude test directories
- '!**/test/**'
其中 glob 支持 *、** 等通配符,以 ! 开头表示排除。packages/** 这类写法会递归匹配目录下所有含 package.json 的项目。
airi 的实际工作区定义
airi 根目录的 pnpm-workspace.yaml 展示了一个真实大型 monorepo 的工作区声明:
shellEmulator: true
packages:
- packages/**
- plugins/**
- integrations/**
- services/**
- examples/**
- docs/**
- engines/**
- apps/**
- server/**
- '!**/dist/**'
几个值得注意的点:
- 九个顶层目录(
packages/、apps/、plugins/、integrations/、services/、docs/、engines/、server/等)全部按**递归纳入,最终形成 40 余个内部包; - 最后一行
'!**/dist/**'是排除规则,防止把构建产物目录误认为工作区成员——这是文档示例中'!**/test/**'排除语法在 airi 中的对应实践; - 同一份 package.json(根包,名为
@proj-airi/root,"private": true)里还保留了一个"workspaces"字段,声明了与pnpm-workspace.yaml完全一致的九个 glob。pnpm-workspace.yaml是 pnpm 的权威来源,package.json中的workspaces字段则服务于 npm/yarn 生态的工具识别,两者保持同构可以让不同工具链对"哪些目录是工作区"达成一致。
工作区声明只是第一步。airi 把这一层做得更完整:同一个 pnpm-workspace.yaml 中还集中了 catalog/catalogs(版本目录)、overrides(依赖覆盖)、patchedDependencies(补丁依赖)、allowBuilds(允许的 postinstall 构建)等配置,全部集中在一处管理,避免了多个配置文件散落的问题。这与下文最佳实践第 6 条"把所有 pnpm 设置放进 pnpm-workspace.yaml"的思路一致。
workspace: 协议:引用本地包
工作区内引用内部包不应写死版本号(一改版本就要逐包同步),而应使用 workspace: 协议,让 pnpm 自动解析并链接到本地路径:
{
"dependencies": {
"@myorg/utils": "workspace:*",
"@myorg/core": "workspace:^",
"@myorg/types": "workspace:~"
}
}
协议变体与发布行为
| 协议 | 行为 | 发布时转换为 |
|---|---|---|
workspace:* |
任意版本 | 实际版本(如 1.2.3) |
workspace:^ |
兼容版本 | ^1.2.3 |
workspace:~ |
补丁版本 | ~1.2.3 |
workspace:^1.0.0 |
明确的 semver 范围 | ^1.0.0 |
airi 中协议的实际用法
airi 各应用普遍采用 workspace:^ 来引用共享库。例如 apps/stage-pocket/package.json 中声明了十几个内部依赖:
{
"dependencies": {
"@proj-airi/audio": "workspace:^",
"@proj-airi/ccc": "workspace:^",
"@proj-airi/i18n": "workspace:^",
"@proj-airi/pipelines-audio": "workspace:^",
"@proj-airi/server-sdk": "workspace:^",
"@proj-airi/stage-ui": "workspace:^",
"@proj-airi/stage-ui-live2d": "workspace:^",
"@proj-airi/stage-ui-three": "workspace:^",
"@proj-airi/stream-kit": "workspace:^",
"@proj-airi/ui": "workspace:^"
}
}
而 apps/stage-tamagotchi/package.json(Electron 桌面应用)则同时出现了两种变体:"@proj-airi/server-sdk": "workspace:*" 与其余包的 "workspace:^"。这恰好对应协议变体表中的两行:workspace:* 发布时落成具体版本号,workspace:^ 发布时保留 caret 范围。
在 lockfile 层面,这些协议依赖被完整记录。pnpm-lock.yaml(lockfileVersion: '9.0')的 importers 段中可以看到每个包名与其协议 specifier 的映射:
'@proj-airi/audio':
specifier: workspace:^
这保证了任何一台机器执行 pnpm install 时,内部依赖一律链接到本地工作区包,而不是从 registry 拉取同名包——这是"发布前始终是本地源码、发布后才变成 registry 版本"这一机制的落盘形态。
过滤包:--filter 与递归执行
大型工作区不可能对每次命令都遍历所有包,--filter(简写 -F)用于把命令限制在指定包上。
基础过滤方式
# By package name
pnpm --filter @myorg/app build
pnpm -F @myorg/app build
# By directory path
pnpm --filter "./packages/core" test
# Glob patterns
pnpm --filter "@myorg/*" lint
pnpm --filter "!@myorg/internal-*" publish
# All packages
pnpm -r build
pnpm --recursive build
按名称、按目录路径(./ 前缀)、按 glob、按取反排除(! 前缀)四种方式可以覆盖绝大多数场景;-r / --recursive 则等价于"全部工作区包"。
基于依赖关系的过滤
# Package and all its dependencies
pnpm --filter "...@myorg/app" build
# Package and all its dependents
pnpm --filter "@myorg/core..." test
# Both directions
pnpm --filter "...@myorg/shared..." build
# Changed since git ref
pnpm --filter "...[origin/main]" test
pnpm --filter "[HEAD~5]" lint
前后缀 ... 表示沿依赖图向"上游"(依赖方向)或"下游"(被依赖方向)递归;[git-ref] 形式则用于增量构建——只挑选自某个 git 引用以来发生过变更的包(及其依赖闭包)。
airi 根脚本中的过滤用法
airi 根 package.json 的 scripts 是过滤语法的一组真实用例:
"dev": "pnpm -r -F @proj-airi/stage-web dev",
"dev:apps": "pnpm -rF=\"./apps/*\" run --parallel dev",
"dev:packages": "pnpm -rF=\"./packages/*\" --parallel run dev",
"typecheck": "pnpm -rF=\"./packages/*\" -F=\"./apps/*\" -F=\"./server/**\" -F=\"./docs\" --parallel typecheck",
"test-stage-tamagotchi:run": "vitest run --config apps/stage-tamagotchi/vitest.config.ts --project browser"
可以看到三类用法并存:
-F @proj-airi/stage-web按包名过滤,配合-r递归执行对应脚本;-F="./apps/*"、-F="./packages/*"按目录 glob 过滤,一次覆盖整个目录族;- 多
-F参数叠加(typecheck一行串了四个 filter),并加--parallel让各包并行执行。
此外 airi 的顶层 build 并未直接用 pnpm -r build,而是交给 Turbo 做拓扑编排:
"build": "turbo run build -F=\"./packages/*\" -F=\"./apps/*\" -F=\"./server/**\"",
"build:packages": "turbo run build -F=\"./packages/*\""
配套的 turbo.json 通过 "dependsOn": ["^build"] 声明"先构建上游依赖包再构建自身",并对个别包(如 @proj-airi/electron-vueuse 依赖 @proj-airi/electron-eventa)做了显式依赖补强。这对应最佳实践第 4 条"构建时按依赖过滤以保证正确顺序"——pnpm 的 --filter 与 Turbo 的 dependsOn 在这里形成互补。
工作区命令:安装、脚本与 exec
安装依赖
# Install all workspace packages
pnpm install
# Add dependency to specific package
pnpm --filter @myorg/app add lodash
# Add workspace dependency
pnpm --filter @myorg/app add @myorg/utils
pnpm install 在仓库根目录执行一次即可安装全部工作区成员;--filter 定位到具体包后执行 add,pnpm 会判断目标是否为本工作区包——是则写入 workspace: 协议,否则走 registry。
运行脚本
# Run in all packages with that script
pnpm -r run build
# Run in topological order (dependencies first)
pnpm -r --workspace-concurrency=1 run build
# Run in parallel
pnpm -r --parallel run test
# Stream output
pnpm -r --stream run dev
四个关键开关:-r 限定"有该脚本的包"、--workspace-concurrency 控制并发度(设为 1 即严格串行,配合拓扑顺序保证依赖先行)、--parallel 忽略拓扑全并行、--stream 把各包输出实时混流到终端。
执行任意命令
# Run command in all packages
pnpm -r exec pwd
# Run in specific packages
pnpm --filter "./packages/**" exec rm -rf dist
exec 用于执行非 npm script 的任意 shell 命令,适合批量清理 dist/ 这类产物目录(与 airi 工作区声明中 '!**/dist/**' 的排除规则互为呼应:dist 是各包产物,既不属于工作区,也常被 exec 批量清理由此管理)。
工作区设置:pnpm-workspace.yaml 中的 camelCase 配置项
参考文档指出一个重要的演进点:工作区级设置从 .npmrc 迁出,统一写入 pnpm-workspace.yaml 并使用 camelCase 键名。完整示例如下:
packages:
- 'packages/*'
# Link workspace packages automatically
linkWorkspacePackages: true
# Prefer workspace packages over registry
preferWorkspacePackages: true
# Single lockfile for the whole workspace (recommended)
sharedWorkspaceLockfile: true
# Workspace protocol handling on publish
saveWorkspaceProtocol: rolling
# Concurrent workspace scripts
workspaceConcurrency: 4
# Use root deps to resolve peers of all projects
resolvePeersFromWorkspaceRoot: true
# Scripts required in every project (else `pnpm -r run <name>` fails)
requiredScripts:
- build
各项作用:
linkWorkspacePackages:依赖匹配到工作区内的包时自动改走本地链接,而不依赖显式workspace:前缀;preferWorkspacePackages:版本范围命中时优先选工作区包而非 registry 版本;sharedWorkspaceLockfile:整个工作区共用根目录一份pnpm-lock.yaml(推荐,保证一致性);saveWorkspaceProtocol:控制发布时协议转换策略(如rolling随版本滚动);workspaceConcurrency:-r脚本默认并发数;resolvePeersFromWorkspaceRoot:用根目录依赖统一解析各项目的 peerDependencies,减少 peer 冲突告警;requiredScripts:声明每个项目必须具备的脚本,缺失时pnpm -r run <name>直接失败,可防止"部分包漏配脚本"造成的静默跳过。
airi 的 pnpm-workspace.yaml 印证了这条演进路径:仓库中不存在 .npmrc 文件,工作区配置全部集中在 pnpm-workspace.yaml,例如顶层的 shellEmulator: true,以及 pnpm-lock.yaml settings 段记录的 autoInstallPeers: true、excludeLinksFromLockfile: false。单份根级 lockfile(lockfileVersion: '9.0')即"共享 lockfile"的体现。
另外,airi 的 pnpm-workspace.yaml 还启用了文档未展开的进阶能力,可作为"工作区单文件配置"思路的延伸:
catalog/catalogs:根 package.json 的devDependencies大量使用"catalog:"与"catalog:vitest"引用(如"typescript": "catalog:"、"vitest": "catalog:vitest"),版本号只在一处维护,对应最佳实践第 5 条"用 catalogs 统一管理共享外部依赖版本";overrides:全工作区强制指定依赖版本或别名(如axios: npm:feaxios@^0.0.23),一次生效于所有包;patchedDependencies:把 patches/ 目录下的补丁文件挂到指定依赖上(如mineflayer-pathfinder、pixi-live2d-display);allowBuilds:逐个声明哪些依赖允许执行 install 构建脚本(如electron: true、better-sqlite3: false),把构建权限收敛到显式白名单。
按包配置:packageConfigs
pnpm 不再提供子项目级 .npmrc,包粒度的差异配置从根文件的 packageConfigs 下发:
packageConfigs:
project-1:
saveExact: true
project-2:
savePrefix: '~'
例如让某包依赖一律写精确版本(saveExact: true),另一包保存时加 ~ 前缀。适合"大部分包统一策略、个别包特殊化"的场景。
发布工作区:协议转换与 CI 发布
pnpm publish 时会把 workspace: 协议转换为可发布的 semver 版本,外部用户看到的是常规版本范围:
// Before publish
{ "dependencies": { "@myorg/utils": "workspace:^" } }
// After publish
{ "dependencies": { "@myorg/utils": "^1.2.3" } }
CI 环境通常没有干净的 git 状态校验,发布时需要跳过:
pnpm publish -r --no-git-checks
结合前文协议变体表即可推断完整的发布链路:工作区内开发时依赖走本地链接,发布时 workspace:^ 变 ^x.y.z、workspace:* 变 x.y.z,消费方从 registry 安装时则与普通包无异。
最佳实践与项目结构
参考文档给出的六条最佳实践,逐条在 airi 仓库中都能找到对应物:
- 内部依赖一律使用
workspace:协议 —— airi 各应用对@proj-airi/*依赖全部采用workspace:^或workspace:*(见 apps/stage-pocket/package.json); - 启用
linkWorkspacePackages自动链接 —— 配合 airi 的overrides/catalog 体系,内部包解析始终收敛在工作区内; - 使用共享 lockfile 保持一致 —— 全仓库仅根目录一份 pnpm-lock.yaml;
- 构建时按依赖过滤保证顺序 —— 顶层
build用 Turbo 的dependsOn: ^build拓扑排序(turbo.json),脚本层则大量使用pnpm -rF目录级过滤; - 用 catalogs 统一共享外部依赖版本 ——
catalog(400 余项)与具名catalogs(如vitest)定义在 pnpm-workspace.yaml,各包以catalog:引用; - 所有 pnpm 设置集中在
pnpm-workspace.yaml(camelCase) —— 仓库无.npmrc,工作区设置、overrides、补丁、构建白名单同处一文件。
文档最后给出的示例项目结构:
my-monorepo/
├── pnpm-workspace.yaml
├── package.json
├── pnpm-lock.yaml
├── packages/
│ ├── core/
│ │ └── package.json
│ ├── utils/
│ │ └── package.json
│ └── types/
│ └── package.json
└── apps/
├── web/
│ └── package.json
└── api/
└── package.json
airi 的结构是该模板的超集:packages/、apps/ 之外还纳入了 plugins/、integrations/、services/、engines/、server/ 等更多顶层目录,且每个成员包都遵循"目录 + package.json"的最小单元约定(如 packages/audio、apps/stage-web)。
适用前提小结
- 上述
pnpm-workspace.yamlcamelCase 设置项(含packageConfigs、catalogs、allowBuilds)依赖较新版本的 pnpm。airi 根 package.json 通过"packageManager": "pnpm@11.24.0"锁定了 pnpm 11 系列,lockfile 为 9.0 版本,阅读或复刻本文配置时请以同代 pnpm 为准; --filter的[git-ref]增量过滤要求仓库处于可访问指定引用的 git 环境中;- 工作区配置与根
package.json的workspaces字段宜保持同构,避免多包管理器工具对成员集合判断不一致; - 本文所有配置路径均相对 airi 仓库根目录,可直接打开核对:pnpm-workspace.yaml、package.json、pnpm-lock.yaml、turbo.json。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00