Expo 开源仓库贡献指南:从环境搭建、SDK 包编辑到测试与文档的完整贡献流程
本文基于 Expo 仓库根目录的 CONTRIBUTING.md 编写,面向希望向 Expo SDK 提交代码的外部贡献者与团队成员。文章完整覆盖仓库贡献的各个环节:开发环境搭建(direnv、Ruby、JDK、ccache 等)、基于 apps/bare-expo 沙盒项目的 SDK 包编辑工作流、单元测试与 E2E 测试的编写与运行、文档更新规则,以及提交前的检查清单,并结合仓库源码补充了各命令背后的实际实现。
贡献范围与开发工作流的核心选型
Expo 仓库目前接受针对 packages/、docs/、templates/、guides/、apps/ 目录以及 markdown 文件的 PR。整个仓库是一个 pnpm workspace 单仓(monorepo),根目录 package.json 通过 workspaces.packages 声明了 apps/*、packages/*、packages/@expo/* 等工作区成员,并用 Turborepo(根目录 turbo.json)统一编排 build、typecheck、lint、test 等任务,且配置了共享远程缓存(turbo.json 中的 remoteCache 段),因此 git pull 或切换分支后通常不需要从头重新编译每个包。
关键选型:SDK 开发请使用 apps/bare-expo,而不是 Expo Go(apps/expo-go)。 原因有两点:
- Expo Go 应用本身较难搭建,且依赖 API token;
- apps/bare-expo 项目链接了
packages/目录下的绝大部分 Expo SDK 依赖,能够直接运行 apps/test-suite 与 apps/native-component-list 两个测试/演示应用,方便浏览 SDK 组件与 API、为任意 SDK 包编写并运行 iOS / Android 的 E2E 测试。单元测试则直接写在 SDK 包内部。代码推送到远端后,CI 会运行该项目并在 Android/iOS 上执行测试,结果回显到你的 PR 上。
二者的关系是:bare-expo 是一个 bare React Native 应用,为了能够运行 apps/ 目录下的项目,它链接了 packages/ 下的全部 Expo SDK 依赖;它导入 test-suite 应用的根组件并作为自己的根组件使用。test-suite 是一个带有少量自定义代码的 Expo 应用,被改造成了测试运行器(test runner);如果在 apps/test-suite 目录里直接运行 expo start,也可以把该项目加载到 Expo Go 中。
此外,apps/native-component-list 中内置了大量人工冒烟测试(manual smoke tests),非常适合需要真机物理交互的演示或测试场景——当你测试 UI 组件交互、或者某个行为很难自动化但手动交互即可验证时,它是首选工具。
下载与基础环境搭建
注意:本仓库的开发环境不支持 Windows,Windows 用户必须使用 WSL 进行贡献。
基础步骤如下(原文档的完整步骤序列):
- 获取代码。Expo 团队成员直接克隆仓库;外部贡献者先将仓库 fork 到自己的账号再克隆到本地,并添加上游远端:
git remote add upstream git@github.com:expo/expo.git。若希望加速克隆,可用git clone --depth 1 --single-branch --branch main git@github.com:expo/expo.git,跳过大部分分支与历史。 - 安装 direnv。macOS 上执行
brew install direnv,并记得把 shell hook 安装到你的 shell profile 中。direnv 对本仓库尤为重要:根目录 .envrc 会在进入仓库时自动加载环境,其中:PATH_add bin把仓库根下的bin/加入 PATH(et等工具即来自这里);- 导出
EXPO_USE_SOURCE=1,强制所有 Expo 模块从源码编译; - 设置
CCACHE_BASEDIR为当前目录,使 ccache 缓存可跨 git worktree 共享(见下文 Android 加速节); - 校验 Ruby 版本(
use_ruby "3.3" "3.4" "4.0"),不在允许列表内会直接报错退出; - 从
secrets/expotools.env加载 expotools 密钥(如存在),并安装 scripts/git-hooks 下的 Git hooks。
- 安装 Ruby 3.3 或更高版本。macOS 自带的是 ruby 2.6,本仓库不支持,可用
brew install ruby@3.3。 - 安装 Node LTS。
- 部分脚本需要 Bun。大多数任务用不到,可按需安装。
Android 环境配置
如果计划贡献 Android 相关代码,在仓库根目录运行:
pnpm run setup:native
从根 package.json 可见,该脚本实际是 ./scripts/download-dependencies.sh --native && ./scripts/setup-react-android.sh 的组合。查看 scripts/download-dependencies.sh 可知它依次完成:
- 前置检查 node、npm、direnv 是否安装(缺失则报错退出);
git submodule update --init拉取react-native等子模块;- 确保 pnpm 已安装(缺失时通过
npm install -g pnpm补装); - 执行
pnpm install下载全部 Node 依赖(并确保你的电脑满足 React Native 环境要求,如缺失会安装 Android NDK)。
JDK:推荐使用 JDK 17(如 zulu17):
brew tap homebrew/cask-versions
brew install --cask zulu@17
安装后在 ~/.bash_profile(ZSH 用户为 ~/.zshrc)中设置:
export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home
ANDROID_SDK_ROOT 需要被设置,或者在你所操作的 native 项目的 android 文件夹下通过 local.properties 配置。
可选:用 ccache 加速 Android 原生构建
ccache 缓存 C/C++ 编译结果,当源码文件未变化时,原生代码的重建几乎是瞬时的。配置步骤:
-
安装:
brew install ccache -
在
~/.zshrc(或~/.bashrc)中添加:export CMAKE_C_COMPILER_LAUNCHER="ccache" export CMAKE_CXX_COMPILER_LAUNCHER="ccache" -
启用预编译头(precompiled header)支持(
expo-modules-core等模块需要):ccache -o sloppiness=pch_defines,time_macros
仓库的 .envrc 会通过 direnv 自动设置 CCACHE_BASEDIR,因此无需额外配置即可在多个 git worktree 之间共享缓存。
iOS 环境配置
如果你要开发 iOS 项目:
- 确保机器上安装了 Ruby 3.3(macOS 自带的 ruby 2.6 不受支持,Homebrew 用户执行
brew install ruby@3.3); - 安装最新稳定版 Xcode 及 Xcode 命令行工具(command line tools)。
验证原生安装是否成功
- 进入 bare 沙盒项目:
cd apps/bare-expo - 在任意原生平台上运行项目:
- iOS:
pnpm ios - Android:
pnpm android - 若在 Linux 上工作,需把
TERMINAL环境变量设置为你的终端应用(例如export TERMINAL="konsole")。
- iOS:
- 此时你运行的就是通过
bare-expo承载的test-suite应用,可以开始对 SDK 包进行改动。
从 apps/bare-expo/package.json 可以看出这些脚本的真实形态:ios 与 android 均以 NODE_ENV="development" 调用 scripts/start-simulator.sh 或 scripts/start-emulator.sh;而 test:ios / test:android 则切换为 NODE_ENV="test"。以 start-simulator.sh 为例,开发模式下它会先执行 setup-ios-project.sh 再运行 npx expo run:ios;测试模式下则自动检测/安装 Maestro 与 idb-companion,必要时先构建 BareExpo.app,然后执行 E2E 测试流程——这正是下文 E2E 测试章节的运行入口。若上述流程无法正常工作,仓库建议开一个 issue 反馈。
编辑 SDK 包
所有 Expo SDK 包都位于 packages/ 目录,并且自动链接到 apps/ 目录中的项目,因此你可以原地编辑并立即在运行中的应用中看到变化。标准工作流:
- 进入要编辑的包,例如
cd packages/expo-constants - 编辑后编译包的 TypeScript:
pnpm build(若该包没有这个脚本可跳过) - 在该包的
src/目录中修改代码 - 通过
bare-expo在模拟器或真机上验证改动:- 添加或修改一个以目标 API 命名的测试文件,例如
apps/test-suite/tests/Constants.js - 要验证原生(native)层改动,需用
apps/bare-expo工程运行test-suite:pnpm <android | ios> - 如果只改了 JavaScript,也可以直接在
apps/test-suite项目中用expo start运行 - 运行完整测试套件:
pnpm test:<android | ios>
- 添加或修改一个以目标 API 命名的测试文件,例如
- 原生代码既可以在
packages/目录下的对应包内直接编辑,也可以打开bare-expo的原生工程:cd apps/bare-expo- Android Studio:
pnpm edit:android - Xcode:
pnpm edit:ios - 任何原生改动之后必须重新构建(rebuild) native 工程
- (可选)包的文档部分由源码生成,运行
et generate-docs-api-data -p <package-name>重新生成文档(et是仓库内 tools 目录提供的 expotools CLI,对应实现见 tools/src/commands/GenerateDocsAPIData.ts)。
以 packages/expo-constants/package.json 为例,可以看到 build 脚本实际是 expo-build src,depscheck 是 expo-module depscheck,lint 使用 oxlint——这些统一行为来自下文提到的 expo-module-scripts 包。
通用包脚本(Common package scripts)
packages/ 下几乎每个包都暴露同一组 npm 脚本,由 Turborepo 在 monorepo 层面统一编排。编译产物 build/ 不会提交到 Git(在 .gitignore 中);Turborepo 按需构建并本地 + 远程缓存结果,避免 git pull / git checkout 后被迫重建所有包。这与 turbo.json 中的任务定义一一对应:build 任务声明了 build/** 等输出产物并依赖上游包的 ^build,lint / format / test 则关闭缓存(cache: false)。
| 脚本 | 作用 |
|---|---|
build |
编译 src/ → build/ |
typecheck |
用 tsc 对包做类型检查 |
test |
运行该包的 Jest 单元测试 |
lint |
对该包做 lint,可传 --fix 自动修复 |
format |
格式化该包,可传 --check 只检查不修改 |
depscheck |
校验包声明的依赖与其实际 import 是否一致 |
两种运行方式:
- 从仓库根目录
pnpm <script>(如pnpm build、pnpm test、pnpm lint、pnpm format、pnpm typecheck)。这会触发turbo <task>,在整个工作区范围内按依赖图和缓存运行脚本; - 从单个包目录
pnpm run <script>(如cd packages/expo-constants && pnpm run test),只运行该包的脚本。
对于“我的改动是否通过了构建、类型检查、lint 和测试”的一次性验证,跨包使用 et check-packages <...packages>(实现见 tools/src/commands/CheckPackages.ts),它运行与 CI 相同的 Turborepo 任务图。
如何找到可做的任务
如果你暂时没有目标,最好的入手点是带有 "Issue accepted" 标签的 open issues。另外注意:仓库一般不接受仅升级原生依赖版本的 PR——这类升级由 Expo 团队在每个 SDK 版本发布流程中统一处理,因为引入新版本需要了解相当多的 Expo Go 上下文。
代码风格
所有模块应遵循以下风格指南:
- Expo Module Infrastructure
- Expo JS Style Guide(大部分规则同样适用于 TypeScript)
- Expo Swift Style Guide
- Updating Changelogs
进阶提示(Extra Credit)
- React Native dev tools 目前在仓库的 RN fork 中处于禁用状态(对应 issue #5602)。可以克隆一份独立于本仓库的 React Native,把其
react-native/React/DevSupport目录内容复制到react-native-lab/react-native/React/DevSupport(bare-expo的 package.json 中也提供了sync:tools脚本做类似同步)。这样只能启用 shake 手势,CMD+R 暂时仍不可用。 - 仓库使用的是
react-native的 fork,位于 react-native-lab/react-native(通过 git submodule 拉取)。你可以在这里做修改或 cherry-pick;该 fork 与package.json中的react-native版本只保持最小必要的偏离。 - 仓库使用一套统一的基础 Bash 脚本与配置 expo-module-scripts,保证 TypeScript、Babel、Jest 等工具链在所有包中行为一致。
测试你的改动
PR 的 Test Plan 部分需要写清楚你如何测试了你的改动。
让改动被合并的最好方式就是为它构建良好的测试。仓库有三类测试:单元测试、自动化 E2E 测试、演示(demo)。补上你发现的缺失测试是快速熟悉项目的好方式。
单元测试
- 在对应包的
src/__tests__目录中为功能创建测试(若目录不存在就创建,文件扩展名使用*-test.ts或*-test.tsx)。 - 所有新增的桥接(bridged)原生函数必须加入 jest-expo 包以确保被 mock。仓库为此提供了专门的工具和指南:Generating Jest Mocks。
- 用
pnpm test运行测试,并确保覆盖 iOS、Android 和 web 各平台。若某功能不支持某平台,可将测试放入带平台扩展名的文件中排除,例如.test.ios.ts、.test.native.ts、.test.web.ts等。 - 也可以按住 X 选择要单独测试的平台,逐个平台运行。
E2E 测试
- 把测试写在
apps/test-suite/tests目录中:- 这些测试运行在 Android/iOS 客户端上,基于一个功能并不完整的 Jasmine 版本,因此快照测试等特殊功能不可用;
- 新建的测试文件务必在 apps/test-suite/TestModules.ts 中注册,应用才能运行它(测试辅助函数位于 apps/test-suite/TestUtils.js);
- 若新测试文件应能从
bare-expo自动化测试中自动运行,将其加入 apps/bare-expo/e2e/TestSuite-test.native.js。从源码可以看到,该文件导出一个TESTS名称数组(如Constants、Crypto、SQLite等),Maestro 测试流程即由这份列表生成,添加或移除条目即可,同时该测试必须在TestModules.ts中注册。
- 在
bare-expo目录本地运行:pnpm test:android或pnpm test:ios。- 务必本地先测:native CI 测试可能脆弱、耗时长,失败时排查也很麻烦。
- 尽量让功能在尽可能多的平台上运行。
更新文档
Expo 文档基于 Next.js 构建,位于 docs 目录,更多细节见 docs/README.md。要点(TL;DR):
- 运行 docs 的 pnpm 命令要求特定版本的 Node,该版本定义在 docs/package.json 的
packageManager/engines字段中(当前仓库要求 Node 22.13.1 及以上,且 docs 包本身声明了更高的 Node 版本要求,以该文件为准)。 - 操作步骤:
- 进入 docs 目录并运行
pnpm install; - 用
pnpm dev启动项目(确保没有其他服务占用 3002 端口——dev脚本即为next dev -p 3002,并先执行generate-static-resources); - 进入要编辑的文档:
cd docs/pages/; - 如果你更新的是某个旧版本,确保对应的 API 文档改动被拷贝到
docs/pages/versions/unversioned/; - 包的 API 文档由源码生成。重新生成:
et generate-docs-api-data -p <package-name>(面向下一个 SDK 版本),或et generate-docs-api-data -p <package-name> -s <number>(面向指定 SDK 版本)。
- 进入 docs 目录并运行
编写 Commit Message
Commit message 最有用的格式是 [platform][api] Title。例如修复了 expo-video 包在 iOS 上的一个 bug,可以写:
[ios][video] Fixed black screen bug that appears on older devices
提交前检查清单
- 记得在改动的包的
CHANGELOG.md中为任何用户可见的改动添加简明描述;若改动不涉及任何包,则写入 根目录 CHANGELOG.md。这对破坏性变更(breaking changes)尤其重要。
改动了 packages/ 中的内容时
- 对你改动的包运行
et check-packages <...packages>(等价于pnpm build、pnpm typecheck、pnpm test、pnpm lint、pnpm format),参见上文通用包脚本; - 运行
pnpm lint --fix和pnpm format修复代码格式,并确认两个命令都能无错误、无警告地通过; - (可选)包的文档部分由源码生成,运行
et generate-docs-api-data -p <package-name>重新生成文档; - 删除所有
console.log和被注释掉的代码块。
编辑了 docs 目录时
- 针对当前 SDK 版本的文档改动,必须同步到 unversioned 副本。当前文档 SDK 版本定义在 docs/package.json 中,版本化工作流描述在 docs/README.md。示例:
- 你修复了
docs/pages/versions/vXX.0.0/sdk/app-auth.md中的拼写错误; - 则需确保把该改动同步到
docs/pages/versions/unversioned/sdk/app-auth.md。
- 你修复了
- 无需本地运行 docs 测试。只需确保你加入的链接没有断链、格式正确,并且改动符合 Expo Documentation Writing Style Guide。
提速技巧(Extra Credit)
CI 测试在你未改动某些目录时会提前结束。如果你想更快拿到结果,应该把 docs 目录的改动单独放在一个 PR 中,其余改动放在另一个 PR 中。
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