首页
/ Expo 单仓库公共 API 兼容性审查:导出映射、类型级破坏与 Changelog 版本策略

Expo 单仓库公共 API 兼容性审查:导出映射、类型级破坏与 Changelog 版本策略

2026-09-05 17:34:44作者:庞队千Virginia

本文以 Expo 官方代码审查智能体 public-api.md 为核心,系统讲解 Expo 单仓库中「公共 API 破坏性变更」的识别标准:从 exports 映射的解析陷阱、在仓库内能编译但会在消费方项目里失败的类型级破坏,到 peerDependencies 收窄与 sideEffects 修剪,并结合发布工具 tools/src/publish-packages/helpers.tstools/src/Changelogs.ts 的源码,说明 Changelog 标题如何机械地决定 npm 包的 MAJOR/MINOR/PATCH 版本。读完后你能掌握一套可落地的公共 API 兼容性审查清单,以及 Expo 发布链路的底层原理。

为什么 Expo 需要专门的公共 API 审查

Expo 是一个包含约 140 个独立版本化包的单仓库:packages/ 下的每个包都以自己的版本号单独发布到 npm。任何一个导出符号的变更,本质上是变更了成千上万个应用依赖的契约。而 apps/ 下的 14 个应用与 tools/ 均标记为 "private": true,从不发布,因此 API 兼容性规则不适用于它们——审查范围严格限定在会发布到 npm 的包上。

该审查规则定义了整份指南最重要的约束:本仓库自己的构建和测试检测不出你所要发现的绝大多数问题。类型级破坏在单仓库内部往往源码兼容,只在消费方的项目里失败。因此「CI 是绿的」永远不能作为变更安全的结论。

这份规则位于 AI 代码审查系统 .expo-agents/code-review/ 中:config.jsonc 约定「agents/ 目录下的每个 markdown 文件即一位审查员」,coordinator.md 负责汇总去重并做最终裁决。public-api 审查员只判断标题与措辞,不重复机械检查已经覆盖的事项——这一分工在后面会反复出现。

有两个仓库事实塑造了审查工作,值得单独强调:

  1. getMinReleaseType 只在 Changelog 的 Unpublished 区块下存在精确标题 🛠 Breaking changes 的条目时才选择 MAJOR 升版。把破坏性变更写进 🐛 Bug fixes 小节,就会以 minor 或 patch 版本悄悄发布。标题是承重结构,不是装饰。
  2. Metro 自 0.82 起默认启用 package.jsonexports 解析,而 Expo 仓库运行在较新的 Metro + React Native 之上(审查指南标注为 Metro 0.84.4 + React Native 0.86,具体版本以当前分支实际依赖为准)。这意味着编辑 exports 字段现在影响的是应用打包,而不只是 Node 的模块解析。

Changelog 标题驱动版本:源码级证据

先看版本决策的代码。helpers.ts 中的 getMinReleaseType 逻辑非常直白:

export function getMinReleaseType(changelogChanges: any): ReleaseType {
  const unpublishedChanges = changelogChanges?.versions[Changelogs.UNPUBLISHED_VERSION_NAME];
  const hasBreakingChanges = unpublishedChanges?.[Changelogs.ChangeType.BREAKING_CHANGES]?.length;
  const hasNewFeatures = unpublishedChanges?.[Changelogs.ChangeType.NEW_FEATURES]?.length;

  // For breaking changes and new features we follow semver.
  if (hasBreakingChanges) {
    return ReleaseType.MAJOR;
  }
  if (hasNewFeatures) {
    return ReleaseType.MINOR;
  }
  return ReleaseType.PATCH;
}

它只读取 Unpublished 版本区块,并按精确字符串匹配小节标题。这些标题定义在 Changelogs.tsChangeType 枚举中:

export enum ChangeType {
  LIBRARY_UPGRADES = '📚 3rd party library updates',
  BREAKING_CHANGES = '🛠 Breaking changes',   // 唯一能触发 MAJOR 的标题
  NEW_FEATURES     = '🎉 New features',       // 触发 MINOR
  BUG_FIXES        = '🐛 Bug fixes',
  NOTICES          = '⚠️ Notices',
  OTHERS           = '💡 Others',
}

调用点位于 loadRequestedParcels.tsminReleaseType: getMinReleaseType(changelogChanges)。也就是说,整个发布工具的升版建议完全由 Changelog 文本中的三级标题决定。

真实的 CHANGELOG 可以印证这套结构。以 packages/expo/CHANGELOG.md 为例,## Unpublished 之下依次是 ### 🛠 Breaking changes### 🎉 New features### 🐛 Bug fixes### 💡 Others;每条变更带 PR 号与作者链接,例如「Raise minimum Node.js version to ^22.13.0 (#47202 by @kitten)」。

还有一个容易忽视的传播机制:helpers.tsresolveReleaseTypeAndVersion 会调用 recursivelyAccumulateReleaseTypes 沿依赖树递归累积所有依赖包的 minReleaseType,再用 highestReleaseTypeReducer 取最高值。换句话说,依赖方被判定为 MAJOR,会把升版压力传递到当前包;此外从 SDK 分支发布时(分支名含 SDK 版本),即使累积出更高的类型也建议按 patch 处理(见 helpers.ts)。这两点决定了审查时不能只看单个包,还要看它在依赖图中的位置。

应标记的破坏性变更

一、破坏性变更写在错误的小节下

触发条件:diff 删除、重命名或改型了某个导出符号,把同步 API 改为异步,或改变了默认值/返回形状——但对应的 Unpublished 条目却挂在 🐛 Bug fixes⚠️ Notices💡 Others 之下。

审查时要读的是条目所在的三级标题,而不是「是否存在条目」。条目存在本身不够——存在性已由机械检查覆盖,只有标题驱动版本升版。这是该审查员职责边界的一个典型体现:机械工具管「有没有」,人工/模型判断管「挂在哪、说得对不对」。

二、exports 映射的解析陷阱

exports 字段的四类问题:

  1. 删除已有 key、把 key 重定向到别的文件、或用更窄的字面 key 替换一个宽泛的模式 key,且没有为旧 specifier 保留别名。审查时优先检查包末尾的通配符。以 packages/expo/package.json 为例,exports 的最后一项是:

    "./*": {
      "types": "./*.d.ts",
      "default": "./*.js"
    }
    

    它相对包根目录(而非 build/)解析。因此删除一个具名子路径后,请求通常会落进这个通配符,最终指向一个不存在的文件——错误在消费方解析时才爆发。

  2. 新增或修改的子路径在已发布形态下无法解析,具体形态包括:

    • 没有 default 条件;
    • default 被放在同一对象的其他条件之前(条件顺序有意义,default 之后的内容全部是死代码);
    • expo-source 条件指向 src/ 但没有 build/ 下对应的构建目标,或反之;
    • 对于带 files 白名单的包,目标路径的顶层段缺失于 files 数组。

    packages/expo/package.jsonfiles 白名单就显式列出了 buildinternalsrctypesvirtual 等段,任何子路径目标若不落在这些段内,打包后就会缺失。

  3. 条件顺序示例。同一文件中 .types 条件是这样组织的(package.json):

    ".": {
      "types": {
        "expo-source": "./src/Expo.ts",
        "default": "./build/Expo.d.ts"
      },
      "expo-source": "./src/Expo.ts",
      "default": "./build/Expo.js"
    }
    

    注意 expo-source自定义解析条件,用于单仓库内部的类型解析(详见「不应标记」一节),default 永远排在其后——顺序颠倒会让 default 之后的条件全部失效。

三、在本仓库仍能编译的类型级破坏

这是该审查规则中技术密度最高的部分,对应四类形态:

1. 符号从值位置退化为纯类型。 export { X } 被改成 export type { X };导出的 enum 被替换为类型别名或 as const 对象加联合类型;class 被替换为 interface。enum 提供值命名空间,而联合类型不提供,所以 EncodingType.UTF8 这种取值写法会直接编译失败。此外,任何对已有导出 enum 成员初始值的修改也应标记。

2. 消费方实现(而非调用)的类型上的参数加宽/返回收窄。 事件监听器与回调签名、hook 的选项回调、config-plugin 的函数类型、消费方需要实现或继承的接口上的方法——参数位置是逆变的:按窄参数写的处理器不再满足加宽后的签名。还要区分写法:方法语法成员(func(x: A): void)比属性语法成员(func: (x: A) => void)风险更高(属性语法在结构类型下允许放宽,方法语法则要求严格匹配),审查结论应说明你看到的是哪一种。

3. 三种「源码兼容但破坏消费方」的形状。 它们在本仓库里不报错,却在消费方项目里失败:

  • 返回类型获得新成员:T 变成 T | undefinedT | null
  • 导出结果类型上的属性变为可选;
  • 导出选项类型上的属性变为必填。

4. 泛型类型参数变更。 已有类型参数默认值改变、新增无默认值的类型参数、或类型参数约束收紧。修改默认值会静默改变裸引用的含义——不报错,类型却变了。

5. 子路径迁移。 导出从一个入口的 barrel 移除、改加到另一个子路径下——除非原入口保留该标识符并挂 @deprecated JSDoc 标签写明新 specifier。消息中指明新路径的抛错桩(throwing stub)算作迁移路径;静默搬家则不算。

四、包元数据变更

  • peerDependencies 收窄:已有 peer 范围的下界提高、|| 范围中的某个分支被删除、或新增 peer 条目却没有 peerDependenciesMeta.<name>.optional: true——除非同一 PR 还包含一条写明新必需版本的 🛠 Breaking changes 条目。npm 7+ 会自动安装 peer 并在依赖树无法解析时报错,因此收窄范围会直接变成安装失败。反向的(放宽范围)永不触发此规则。以 packages/expo/package.json 为参照:peerDependencies@expo/dom-webviewreact-domreact-native-web 等条目在 peerDependenciesMeta 中对应 { "optional": true },正是这套机制的标准用法。

  • sideEffects 修剪sideEffects 被设为 false,或数组中被删掉某个 glob,而被删 glob 匹配的文件仍在 import 期做工作(全局安装器、polyfill、原型补丁)。另外要标记新的 import 期副作用模块——裸 import './Something.fx'、全局安装器、polyfill、原型补丁——出现在该数组任何 glob 都匹配不到的路径上。当数组携带成对的 src/build glob 时,两个必须同时具备。packages/expo/package.json 展示了成对写法:

    "sideEffects": [
      "*.fx.tsx", "*.fx.web.tsx", "*.fx.js", "*.fx.web.js",
      "./src/winter/*.ts",   "./build/winter/*.js",
      "./src/async-require/*.ts", "./build/async-require/*.js"
    ]
    

不应标记的情形(误报防线)

审查规则的一半价值在于抑制误报。以下五类明确不标记

  1. 纯增量表面。 选项类型上新增可选属性、新导出符号、新 exports 子路径、peer 范围放宽、把已有 peer 标记为可选——都不使现有消费方代码失效。同时永远不要要求作者修改包的 version 字段,版本由发布工具负责。
  2. 包自己实现、消费方只调用的函数上的参数加宽/返回收窄。 接受更宽的输入、返回更具体的值,对调用方都是向后兼容的。这是整个领域最可能的误报来源,报告前必须先确认该签名的角色:只有当同一签名同时是消费方实现、作为回调传递或继承的接口的一部分时,才升级处理。
  3. 刻意非公开的入口。 ./internal/* 子路径、src/internal/ 下的文件、unstable- 前缀的子路径、或已带写明替代方案的 @deprecated 标记的符号——它们不携带稳定性承诺,不要为其要求破坏性变更条目或 major 升版。packages/expo/package.json 中的 "./internal/*" 通配子路径就是这类入口的实例。
  4. expo-source 条件指向 src/exports 条目。 这是单仓库类型解析用的自定义条件,不是源码泄漏。不要要求把 src 加进 files,也不要把 files 数组中的 "!src" 当错误看待。
  5. API 变更后重新生成 docs/public/static/data/** 文档工作流会自动完成,永远不要要求作者手动运行生成器。此外,「Changelog 条目缺失」或「条目缺 PR/作者链接」都已被机械检查强制——审查员只判断标题与措辞。

结论输出的证据标准

规则的最后一条是全篇的收束:必须指明哪段消费方代码会被破坏、如何被破坏。 如果你说不出具体哪个 import 或调用会停止工作,这个发现就不成熟。宁可为零发现,也不要给一条投机性的结论。这一标准与 coordinator.md 的汇总规则一脉相承——没有可追踪的失败路径的发现会被直接丢弃,与它声称的类别无关。

小结

审查维度 判定要点 源码/配置证据
Changelog 标题 破坏性条目必须挂在 Unpublished 下的 🛠 Breaking changes 三级标题下 helpers.tsChangelogs.ts
exports 映射 删除/重定向/收窄 key 需保留别名;条件顺序、default 存在性、files 白名单一致 packages/expo/package.json
类型级破坏 值位置退化、逆变回调、三种源码兼容形状、泛型默认值 以 diff 中的类型声明为准
包元数据 peer 收窄需 breaking 条目或 optional 标记;sideEffects glob 成对 packages/expo/package.json
误报抑制 增量表面、纯被调函数、内部/unstable 入口、expo-source 条件、机械检查项 public-api.md

对单仓库独立版本化发布的项目而言,这套规则的通用启示是:把「契约变更」与「机械可查项」分开,让工具守住存在性与格式,让审查者(人或模型)聚焦在标题语义、解析顺序与类型角色这三个 CI 盲区上,并用「说不出哪段消费方代码会坏就不算发现」作为输出的证据门槛。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384