首页
/ Expo Config Plugins 代码评审指南:prebuild 幂等性、merge tag 作用域与原生声明一致性检查

Expo Config Plugins 代码评审指南:prebuild 幂等性、merge tag 作用域与原生声明一致性检查

2026-09-06 15:21:47作者:尤峻淳Whitney

本文基于 Expo 仓库中 config-plugins/prebuild 领域的代码评审规范(.expo-agents/code-review/agents/config-plugins.md)展开,结合仓库内真实的插件实现、mergeContents 底层代码与声明文件,系统讲解如何评审会改写他人原生工程(Info.plistAndroidManifest.xml、Podfile、.pbxproj)的 config plugin 代码。读完后,你将能够:判断一个 mod 是否存在二次 prebuild 重复写入的幂等性缺陷、识别 merge tag 作用域与 anchor 正则的隐患,并核对 expo-module.config.json.podspecspm.config.json 这类必须同步变更的成对声明。

为什么 config plugins 是高风险评审域

config plugin 代码运行在用户项目中:它执行 prebuild 时改写的是 app 开发者本机的原生工程文件。仓库文档开宗明义地指出,这里的失误不会让 Expo 仓库自身的 CI 失败,而是会静默地损坏用户机器上的 Info.plistAndroidManifest.xml、Podfile 或 .pbxproj——而且常常只在第二次 prebuild 时才暴露。

因此该领域最重要的性质是幂等性(idempotency):连续运行两次 prebuild,必须收敛到同一个文件结果。评审任何插件改动时,都要围绕"第二遍运行会发生什么"来思考。

评审范围覆盖以下目录与文件(详见 config-plugins.md):

  • packages/@expo/config-pluginspackages/@expo/prebuild-configpackages/@expo/configpackages/expo-modules-autolinking
  • 各模块包内的 plugin/ 目录
  • 成对原生声明:expo-module.config.json*.podspecspm.config.json、package build.gradle

幂等性的实现基础:@generated 块与 mergeContents

在讲"该 flag 什么"之前,先理解仓库提供的收敛机制,它是判断幂等性缺陷的依据。mergeContents 定义在 generateCode.ts,其工作流程为:

  1. createGeneratedHeaderComment 生成形如 // @generated begin <tag> - expo prebuild (DO NOT MODIFY) sync-<sha1哈希> 的头部注释,哈希取自待写入内容;
  2. 若目标文件已包含该 header,则直接返回原内容didMerge: false),天然幂等;
  3. 否则先调用 removeGeneratedContents 移除旧的 @generated begin <tag>@generated end <tag> 区段,再在 anchor 匹配行处插入新区块。

其中两个细节与评审直接相关:

  • addLines 在 anchor 正则匹配不到时直接抛出 ERR_NO_MATCH 错误(见 generateCode.tslineIndex < 0 分支)。这意味着任何手写 anchor 正则的调用方,必须自己保证"anchor 不存在"时有兜底路径(try/catch、includes 预检或 WarningAggregator 降级),否则一次模板变更就会让用户的 prebuild 崩溃。
  • removeGeneratedContents 的删除区间由 start/end 双正则定位,因此 tag 字符串本身就是区块的寻址键——两个包共用同一个 tag,区块就会互相覆盖。

该 Flag 的评审点

1. 与类型化 base mod 冲突的改动

  • 手写编辑已有类型化 base mod 负责的文件:新增或扩展的 withDangerousMod 如果在手工编辑 AndroidManifest.xmlstrings.xmlcolors.xml、夜间 colors、styles.xmlgradle.propertiesInfo.plist.entitlementsExpo.plistPodfile.properties.json,应要求改用对应的 typed mod(withAndroidManifestwithInfoPlist 等)。withDangerousMod 的定位见 withDangerousMod.ts:它只用于"不解析数据、全部逻辑在 action 内完成"的场景,且所有 dangerous mod 先于其他 mod 执行。
  • 向原生文件追加/插入文本而不走 merge 机制:对 Podfile、app 或 project build.gradlesettings.gradleAppDelegateMainActivityMainApplication 的追加,必须经过 mergeContents(产生 @generated begin <tag> 块),或在内容已存在时短路返回。裸拼接是最经典的二次 prebuild 重复写入 bug
  • withXcodeProject 内无前置查找的添加操作addBuildPhaseaddPbxGroupaddToPbxBuildFileSectionaddFramework 调用之前,没有先查找对应 phase/group/file 是否已存在——第二次 prebuild 必然产生重复节点。

2. Merge tag 与 anchor 的纪律

  • tag 必须归属包内作用域。两个包共用同一个 tag 会互相覆盖对方的 @generated 区块。仓库中有现成的正反例:
    • 反例:withFontsAndroid.ts 中使用的 xml-fonts-init 就是一个未加包前缀的历史 tag(该文件第 388 行 tag: 'xml-fonts-init');
    • 正例:withExpoLocalization.ts 使用 expo-localization-supported-locales(第 56 行),以及 expo-build-properties 这类以包名为前缀的 tag。
    • 评审时同理:新代码里出现裸的 fontslocales 等通用词 tag,就是同类错误。
  • 禁止重命名已有 tag 字符串。旧 tag 生成的 @generated 区块在已经 prebuild 过的项目里永远不会被替换,内容随之重复。
  • anchor 正则必须有"匹配失败"路径:新增或修改 anchor 正则、或对原生模板文本做 .replace() 时,检查调用处是否具备 try/catch、includes 预检或 WarningAggregator 降级三者之一。
  • Gradle mod 的语言保护:在 withAppBuildGradlewithProjectBuildGradlewithSettingsGradle 中,凡是对 config.modResults.contents 的写入,必须用 config.modResults.language === 'groovy' 保护;同时 flag 只能匹配一种赋值形式(如只匹配 = 不匹配 def x =)的 gradle 正则。

3. 写入原生文件前必须校验的值

任何来自插件 prop 或 app config 的值,在被插值进原生文件之前——Android 资源名/文件名、XML 属性、gradle 字符串字面量、plist 键值、Podfile 行——都应先断言其类型与格式。文档明确分工:注入(security)角度由安全评审负责,工程损坏(corrupted project)角度由本评审负责,两者都值得上报。

4. 必须同步变更的成对声明

  • 新增 Module 但 expo-module.config.json 未变:在 ios/apple/ 下新增 Swift Module 子类、在 android/src/ 下新增 Kotlin/Java 模块,或新增 AppDelegate subscriber、React delegate handler,而包的 expo-module.config.json 未更新。apple.modules 需要 Swift 类名、android.modules 需要 Kotlin 类名——缺失时 autolinking 不会注册该模块,API 在运行时"静默缺失"(autolinking 实现见 expo-modules-autolinking)。
  • spm.config.json 中出现新或改名的 podName:检查包根目录、ios/apple/ 下是否存在对应的 <podName>.podspec
  • podspec 与 spm 配置漂移:在同时拥有 spm.config.json 的包中,修改 podspec 的 source_filesexclude_filess.dependencys.platforms,或新增/改名/移动 iOS 源码目录,而未同步更新对应 target 的 pathpatternexcludedependenciesplatforms——两套构建系统并行运行,漂移是静默的。
  • spm.config.json 新键必须双声明:任何新键必须同时出现在 spm.config.schema.jsonSPMConfig.types.ts 两处。

不应 Flag 的情形(避免误报)

规范同样明确了六类"看起来像问题、实则是预期行为"的情况:

  1. 以创建/复制/移动/删除文件为职责的 withDangerousMod 是合法的。写 res/xmlres/font 文件,复制字体与 splash 资源,生成 widget 或 extension 源码——这正是 dangerous mod 的用途,不要要求它们改成 typed base mod。
  2. WarningAggregator.addWarningAndroid / addWarningIOS / addWarningForPlatform 后原样返回 config:当 mod 遇到无法支持的项目形态(非 groovy 的 build.gradle、非 Swift 的 AppDelegate、PBXGroup 中已存在的文件)时,"警告 + 跳过"就是契约,不是吞掉错误。WarningAggregatorconfig-plugins 入口 导出,是插件侧的标准降级工具。
  3. mod 执行顺序主张:不要断言某个具名 mod 应早于/晚于另一个具名 mod 执行,也不要要求重排 withPlugins 条目来给两种不同 mod 排序。只有当两个操作共享同一个 mod 名,或修复方案是把工作移入 withFinalizedMod 时,顺序问题才值得提出。
  4. packages/*/android/build.gradleversion/versionName 落后于 package.json:版本由发布工具链管理,feature PR 里手动 bump 属于噪音而非修复。
  5. 生成物本身.pbxprojPodfile.lock、生成模板文件已被 diff 过滤,不要审查它们。
  6. 插件缺少测试:当仓库本来就没有对应项目形态的 fixture 时,测试缺失不是可上报项。

缺陷证明方法:走一遍第二次 prebuild

规范对非幂等性报告的证据标准非常严格:prebuild bug 靠推演第二次运行来证明。上报前必须在理由中描述"第二次 prebuild 会改变什么";evidence 字段只保留导致问题的那一行 mod 调用或写入语句,绝不粘贴生成后的原生文件片段。原则是:宁可零发现,也不要低价值发现(Prefer zero findings over a low-value one)

小结

评审 Expo config plugins 的核心心智模型可以浓缩为三条:

  1. 幂等收敛:一切写入必须经 @generated 块(mergeContents)或存在性短路,二次 prebuild 不得产生增量变化(generateCode.ts);
  2. 寻址隔离:tag 带包前缀、anchor 有失败路径、gradle 写入有语言保护;
  3. 声明成对expo-module.config.json、podspec 与 spm.config.json(schema + types)必须同 PR 同步,否则 autolinking 与双构建系统的漂移都是静默的。

这套标准同样适用于在 Expo 生态中编写第三方 config plugin 的开发者:把"第二次 prebuild 会发生什么"作为每次提交前的自检问题,是最廉价也最有效的质量门槛。

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