尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

pnpm `versioning.epics`:用 Epic 机制约束 Monorepo 子包主版本区间的完整指南

发布时间:2026/9/19 12:05:07

资讯中心
01
ARTICLE

pnpm `versioning.epics`:用 Epic 机制约束 Monorepo 子包主版本区间的完整指南

pnpm `versioning.epics`:用 Epic 机制约束 Monorepo 子包主版本区间的完整指南
pnpmversioning.epics用 Epic 机制约束 Monorepo 子包主版本区间的完整指南【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm本文基于 pnpm 仓库 .changeset/epics-monorepo-versioning.md 的变更说明展开结合pnpm/releasing.versioning的 TypeScript 实现pnpm11/releasing/versioning/src/assembleReleasePlan.ts、Rust 原生实现pnpm/crates/versioning以及配套测试pnpm11/releasing/versioning/test/assembleReleasePlan.ts系统讲解versioning.epics的配置方式、区间band算法、selector 匹配规则与全部约束校验。读完本文你将掌握如何在pnpm-workspace.yaml中声明一个 epic、理解成员版本为何被锁定在M×100 … M×10099区间、lead 升主版本时成员如何自动 re-base 到区间下限以及哪些配置组合会被拒绝。一、背景为什么 Monorepo 需要 Epic 版本约束在大型 Monorepo 中pnpm 自身的仓库就是一个典型例子一个工具包如 pnpm CLI与它衍生的一批子包pnpm/*存在强耦合关系。如果每个包各自独立决定主版本很容易出现工具包还是11.x某个配套子包已经冲到1200.x这类撕裂——消费方不知道该跟随谁。传统的同步手段只有两种极端完全解耦各包自由升版依赖方通过 range 兜底代价是版本面混乱、发布节奏失控固定组versioning.fixed一组包永远共享同一个版本号代价是弹性极差——任何一个小 patch 都要拖着一整组包一起发版。versioning.epics提供了第三条路把一组包绑在 lead 包的主版本上但保留组内每个包的独立版本演进空间。它不是让成员与 lead 版本号相同而是让成员的主版本被约束在一个由 lead 主版本推导出的**区间band**内。这样成员可以自由发布 patch、minor甚至带内主版本升级但永远不会跑出区间而 lead 一旦跨入新主版本所有成员在同一份发布计划中自动回到新区间的地板floor。二、配置结构versioning.epics写入pnpm-workspace.yamlEpic 配置声明在pnpm-workspace.yaml的versioning键下pnpm/types中的VersioningSettings定义于 pnpm11/core/types/src/versioning.ts是一个数组每个条目包含两个字段字段类型必填说明leadstring是定义区间的主包可用包名或./前缀的工作区目录引用如pnpm或./packages/pnpmpackagesstring[]是且不能为空数组匹配成员包的 selector 数组支持名称 glob、./前缀目录 glob、!前缀否定一个典型配置# pnpm-workspace.yaml versioning: epics: - lead: pnpm packages: - ./pnpm11/** - !./pnpm11/private/**该配置声明以pnpm为 leadpnpm11目录下除private子目录外的所有包为成员packages数组的示例直接取自类型注释 pnpm11/core/types/src/versioning.ts。配置校验规则pnpm-workspace.yaml解析时会调用 pnpm11/workspace/workspace-manifest-reader/src/versioning.ts 中的assertValidWorkspaceManifestVersioning做静态校验任何一条不满足都会抛InvalidWorkspaceManifestErrorversioning.epics必须是数组否则报错Expected versioning.epics to be an array每个 entry 必须是对象且lead必须是非空字符串Expected every versioning.epics entry to have a non-empty lead package referencepackages必须是非空数组且每个元素都是非空字符串Expected every versioning.epics entry for lead to have a non-empty packages array of selector strings。三、区间算法成员主版本为什么是M×100 … M×10099这是整个机制的核心。区间由 lead 的主版本号M推导区间下限 low M × 100 区间上限 high M × 100 99也就是说lead 在11.x时成员的主版本被允许落在1100到1199之间lead 升到12.x区间整体平移为1200 … 1299。实现位于 pnpm11/releasing/versioning/src/assembleReleasePlan.ts 的epicBandfunction epicBand (epic, participants, newVersions): EpicBand { const floor epicRebaseFloor(epic, participants, newVersions) const major floor ! null ? floor / 100 : epicLeadBandMajor(participants.get(epic.leadDir)!.currentVersion) const low major * 100 const high low 99 return { major, low, high, contains: (memberMajor) memberMajor low memberMajor high } }Rust 版实现于 pnpm/crates/versioning/src/error.rs同样以band_major * 100与band_major * 100 99计算区间。为什么是 ×100因为这个间隔足够宽允许成员在 lead 的整个生命周期内进行几十上百次独立小版本迭代而不触碰天花板同时又严格绑定 lead 的主版本避免成员版本漂移到与 lead 毫无关联的数字。四、成员在区间内独立演进4.1 成员自由进行 patch 和 minor 升级只要不突破区间成员的版本完全自主。测试 pnpm11/releasing/versioning/test/assembleReleasePlan.ts 验证了这一点test(epic members move independently inside the band while the lead major holds, () { const plan assembleReleasePlan({ workspaceDir: /ws, projects: [makeProject(pnpm, 11.2.0), makeProject(lib, 1101.4.2)], intents: [makeIntent(one, { pnpm: patch, lib: minor })], ledger: NO_LEDGER, versioning: { epics: [{ lead: pnpm, packages: [lib] }] }, }) // lead: 11.2.0 - 11.2.1 // 成员 lib: 1101.4.2 - 1101.5.0位于 1100-1199 区间内 })lead 升 patch 的同时成员独立升 minor两者互不牵制。4.2 带内 major intent主版本也可以升只要不越界成员声明major意图时同样只会在区间内前进一步而不是退位到 1.x。测试 assembleReleasePlan.tstest(a major intent bumps a member to the next major inside the band, () { // lead: pnpm11.0.0成员 lib1101.4.2意图 lib: major expect(plan.releases[0].newVersion).toBe(1102.0.0) })成员1101.x声明major后升到1102.0.0仍在1100-1199区间内——这就是 changeset 中所说的 amajorintent that stays in-band。4.3 越界被硬拒绝区间耗尽必须等 lead 升主版本如果成员的一次 bump 会越过区间上限发布计划直接抛错而不是悄悄放行。例如 lead 仍在11、成员想从1199.x升到1200.0.01200属于 lead12的区间会被拒绝。该守卫实现在enforceEpicBandsassembleReleasePlan.ts错误码为VERSIONING_EPIC_OUT_OF_BAND错误信息分两种场景成员主版本高于上限The band is exhausted - the lead must advance to a new major to open the next band.区间已耗尽必须等 lead 升主版本打开新区间成员主版本低于下限Re-base the member into the band, or remove it from the epic.把成员 re-base 进区间或从 epic 中移除。对应测试见 assembleReleasePlan.tsRust 版错误定义于 pnpm/crates/versioning/src/error.rs。五、lead 升主版本全成员同一计划内 re-base 到区间地板这是 epic 最具价值的联动行为。当发布计划把 lead 推向新的稳定主版本时每个被触发的成员会放弃自己的演进轨迹统一重新基底re-base到新区间的地板floor.0.0且发生在同一个发布计划内——不存在lead 先升、成员下一轮再跟的窗口期。核心逻辑分两段epicRebaseFloorassembleReleasePlan.ts判断是否触发 re-base只有当 lead 的新版本是稳定版本非 prerelease且新主版本大于旧主版本时才返回newMajor * 100applyEpicBandVersionsassembleReleasePlan.ts把所有被计划触及的成员版本强制覆盖为{floor}.0.0成员若在 release lane 上则 re-base 为{floor}.0.0-{laneTag}.N。测试 assembleReleasePlan.tstest(when the lead reaches a new stable major every member re-bases to the band floor, () { // lead: pnpm11.9.9成员 lib1101.4.2、ui1105.0.0意图 pnpm: major // pnpm - 12.0.0 // lib - 1200.0.0cause 为 epic // ui - 1200.0.0 })注意lib.causes是[epic]——成员这次发版既不是自己的 intent也不是依赖传播而是 epic 联动发布计划会如实记录原因。ReleaseCause类型定义于 assembleReleasePlan.tsintent | dependencies | fixed | epic。Rust 版对应实现位于 pnpm/crates/versioning/src/plan/propagation.rs行为测试在 pnpm/crates/versioning/src/plan/tests/behavior.rs。5.1 与 release lane 的交互re-base 等待稳定发布当 lead 自身处于 prerelease lane 上时如lanes: { pnpm: alpha }lead 发的是12.0.0-alpha.0这样的预发布版本re-base 会被推迟到 lead 的稳定发布lead 在 alpha lane 上发12.0.0-alpha.0时成员仍在旧区间内演进1101.2.0 - 1101.2.1见测试 assembleReleasePlan.ts当该 prerelease lead 毕业graduation为稳定12.0.0时成员才 re-base 到1200.0.0见测试 assembleReleasePlan.ts反过来成员在 lane 上时re-base 的目标是地板的一个 prerelease1200.0.0-alpha.0见测试 assembleReleasePlan.ts。这套行为由epicLeadBandMajor支撑assembleReleasePlan.tslead 处于M.0.0-tag.N这种即将毕业形态时按M-1计算当前区间避免过早打开新区间。六、成员匹配复用 pnpm 的 package selector 语义packages数组的每个元素都是一个 selector匹配语义与 pnpm 其他配置如pnpm/config.matcher保持一致实现在compileEpicSelector与matchesEpicSelectorsassembleReleasePlan.ts。支持三种形态selector 形态匹配对象示例名称 glob包名*通配任意字符序列pnpm/*、lib./前缀目录 glob工作区相对目录./pkgs/**!前缀否定排除匹配!./pkgs/b三个关键语义lead 永不成为自身成员selector 即使匹配到 lead 本身也会被跳过assembleReleasePlan.ts 中if (participant.dir leadDir) continue顺序相关order-dependent每个 selector 依次执行最后一个匹配的 selector 决定最终结论——后写的包含可以重新纳入先前被!排除的包后写的!也可以排除先前被包含的包。测试 assembleReleasePlan.ts 验证了[!./pkgs/b, ./pkgs/**]中./pkgs/b最终仍是成员通配符语义*匹配任意字符序列其余字符按字面量处理由wildcardMatch编译为正则assembleReleasePlan.ts。目录 glob 与否定组合的完整示例测试见 assembleReleasePlan.tslead: ./pnpmpackages: [./pkgs/**, !./pkgs/b]的配置下pkgs/a是成员而pkgs/b被排除。七、配置约束哪些组合会被直接拒绝validateEpicsassembleReleasePlan.tsRust 版见 pnpm/crates/versioning/src/plan/configuration.rs在计划组装阶段做交叉校验共四类硬错误错误码触发条件错误语义VERSIONING_EPIC_UNKNOWN_LEADlead不是可发布的 workspace 项目必须是有 semver 版本号的命名包配置指向不存在的 leadVERSIONING_EPIC_OVERLAP同一个包被两个不同的 epic 匹配一个包最多属于一个 epicVERSIONING_EPIC_FIXED_GROUP_CONFLICT某个versioning.fixed固定组同时包含 epic 成员与非成员固定组必须整体位于 epic 内或整体位于 epic 外VERSIONING_EPIC_OUT_OF_BAND发布计划将成员带到区间之外见 4.3 节其中固定组不能跨越 epic 边界的逻辑很关键固定组所有成员共享同一个版本号若一半在 epic 内、一半在 epic 外re-base 会让两组版本互相撕裂因此配置阶段直接拒绝。此外resolveConfigRefassembleReleasePlan.ts还会对名称引用歧义一个名字匹配多个 workspace 项目抛VERSIONING_AMBIGUOUS_PACKAGE提示改用目录引用。八、静态校验checkVersioningInvariants提前发现漂移除了发布计划执行时的强制校验版本管理引擎还提供静态检查checkVersioningInvariantsassembleReleasePlan.ts对已提交的版本做不变式验证——每个 epic 成员的当前主版本必须位于 lead 的区间内每个固定组成员必须同版本。任何漂移都会以VERSIONING_EPIC_OUT_OF_BAND/VERSIONING_FIXED_GROUP_MISMATCH的形式一次性全部报告而不是等到某次发布恰好触及才暴露。测试见 assembleReleasePlan.ts。九、源码全景从变更声明到双实现落地该特性在仓库中的完整落点变更声明.changeset/epics-monorepo-versioning.mdpnpm/types、pnpm/releasing.versioning、pnpm/workspace.workspace-manifest-reader、pnpm、pacquet均为 minor 变更类型定义pnpm11/core/types/src/versioning.tsVersioningEpic接口配置校验pnpm11/workspace/workspace-manifest-reader/src/versioning.ts计划组装TypeScriptpnpm11/releasing/versioning/src/assembleReleasePlan.tsresolveEpics/epicBand/epicRebaseFloor/applyEpicBandVersions/enforceEpicBands原生实现Rustpnpm/crates/versioningplan/configuration.rs负责解析与校验plan/propagation.rs负责 re-base 传播error.rs定义错误码行为测试pnpm11/releasing/versioning/test/assembleReleasePlan.ts、pnpm/crates/versioning/src/plan/tests/behavior.rs。十、总结versioning.epics是 pnpm 原生 workspace 版本管理pnpm change/pnpm version -r体系中连接强同步与自由演进的中间态机制一个区间成员主版本永远被约束在lead主版本 × 100到×100 99之间lead 生命周期内成员拥有近百个主版本号的自由演进空间两种移动成员在区间内独立 bumppatch/minor/带内 major越界被VERSIONING_EPIC_OUT_OF_BAND硬性拒绝一次联动lead 进入新稳定主版本时所有被触发的成员在同一发布计划内 re-base 到新区间地板且如实记录epic作为发布原因lane 上的 lead 则把联动推迟到稳定毕业一套约束selector 顺序语义、epic 互斥、固定组不跨边界、lead 必须是可发布项目保证配置永远无歧义可归因。对于 pnpm 这类一个核心 CLI 大量pnpm/*生态包的巨型 Monorepoepic 让子包既能跟随核心节奏、又不被核心的每次发版绑架是介于fixed强锁与完全放任之间的理想版本治理形态。【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。