发布日期

第 40 讲:给 Next.js 主仓库提 PR:从克隆到合并

从 Fork 到合并:给 Next.js 主仓库贡献 PR 的完整流程与注意事项

整门课的最后一讲。前 39 讲让你"读懂"和"用好"Next.js,这一讲带你正式参与主仓库:fork → 找 issue → 改代码 → 加测试 → 跑 CI → review → merge。并附一份"如何长期跟踪 Next.js 演进"的清单,让你毕业后也能持续深入。

学习目标

  1. 知道 Next.js 仓库的协作流程、PR 模板、CI 规则。
  2. 学会用 pnpm new-test / pnpm test-dev-turbo / pr-status.js 等内部工具。
  3. 完整走一遍 "找 issue → 复现 → 修复 → 测试 → PR → review → merge"。
  4. 知道维护者关注什么、PR 容易被拒的常见原因。
  5. 拥有一份长期成长路线:从 good-first-issue 到能 review 别人的 PR。

一、准备工作

1.1 仓库结构(对应第 2 讲,再回顾一次)

next.js/
├── packages/           ← published npm packages
│   ├── next/          ← 主框架
│   ├── next-codemod/  ← 升级 codemod
│   ├── next-swc/Rust 绑定
│   ├── eslint-plugin-next/
│   └── ...
├── turbopack/Turbopack(Rust)git subtree
├── crates/SWC custom transform Rust
├── test/               ← 全部测试
│   ├── e2e/
│   ├── development/
│   ├── production/
│   └── unit/
├── examples/           ← 示例项目
├── docs/               ← 文档
└── scripts/            ← 维护脚本

1.2 fork & clone

# fork on github first
git clone git@github.com:<your-handle>/next.js.git
cd next.js
git remote add upstream git@github.com:vercel/next.js.git
git fetch upstream
git checkout -b my-fix upstream/canary

canary 是开发主干,所有 PR 都对 canary

1.3 安装依赖

corepack enable
pnpm install

packages/next-swc/native/ 会自动下载预构建的 .node 二进制(你无需装 Rust 工具链——除非你改 Rust 代码)。

1.4 构建

# 改了 packages/next/** 后
pnpm --filter=next build

# 改了 Rust(turbopack / next-swc)
pnpm build-all

第一次完整 build 大约 1-2 分钟(视机器);后续增量 build 几秒。

watch 模式:

pnpm --filter=next dev
# 监听 src 改动自动重编译

二、找 issue

2.1 good-first-issue / help-wanted

打开 https://github.com/vercel/next.js/labels/good%20first%20issue 看 open 列表。

筛选原则:

  • 描述清晰,有复现
  • 不涉及 Vercel 内部基础设施
  • 不依赖一堆背景知识("先理解 RSC 整个架构"那种先放放)
  • 维护者已确认是 bug(不是讨论中的 feature request)

2.2 自己用过程中发现的 bug

最好的来源。你正在用 Next.js 做项目,遇到一个具体异常 → 写最小复现 → 搜 issue → 没有就开新 issue → 自告奋勇修。

2.3 复现验证

任何 PR 第一步都要能在 main 复现 bug:

# 用 test 复现
pnpm new-test -- --args true my-bug-repro e2e

# 这会在 test/e2e/my-bug-repro/ 生成模板
# 编辑 fixture 和测试用例
# 跑测试
NEXT_SKIP_ISOLATE=1 pnpm test-dev-turbo test/e2e/my-bug-repro/

如果用现成 issue,可以直接拷贝 issue 里附的 repro 进 test/e2e/issues/{issue-number}/

三、写修复

3.1 定位代码

整门课讲了几十个源码入口,回顾一下定位思路:

  • bug 在 dev mode?→ src/cli/next-dev.ts + src/server/dev/
  • bug 在 prod mode?→ src/cli/next-start.ts + src/server/next-server.ts
  • bug 在 build?→ src/build/index.ts
  • bug 在 RSC render?→ src/server/app-render/
  • bug 在 client navigation?→ src/client/components/
  • bug 在 manifest 生成?→ src/build/webpack/plugins/

不知道从哪入手时:

# 搜错误消息字符串
rg "exact error message" packages/next/src

# 搜 spans 名找入口
rg "BaseServer\.\w+" packages/next/src

3.2 边写边测

watch 模式 + NEXT_SKIP_ISOLATE=1 是最快循环:

# 终端 A
pnpm --filter=next dev

# 终端 B
NEXT_SKIP_ISOLATE=1 NEXT_TEST_MODE=dev pnpm testheadless test/e2e/my-bug-repro/foo.test.ts

改一次 src/ → watch 自动重编 → 跑测试 → 1-5 秒反馈。

⚠️ 模块解析改动不能用 NEXT_SKIP_ISOLATE=1(第 0 讲 / CLAUDE.md 强调过)。要验证 publish 后能否解析时,去掉这个 env。

3.3 写新测试

测试必须覆盖你修的 bug。模板:

// test/e2e/issues/12345/issue-12345.test.ts
import { nextTestSetup } from "e2e-utils";
import { retry } from "next-test-utils";

describe("issue #12345", () => {
  const { next } = nextTestSetup({
    files: __dirname, // 用同目录下的 fixture
  });

  it("should render correctly when X", async () => {
    const browser = await next.browser("/");
    await retry(async () => {
      const text = await browser.elementByCss("h1").text();
      expect(text).toBe("expected");
    });
  });
});

注意(来自 CLAUDE.md):

  • retry() 而非 setTimeout
  • nextTestSetup({ files: __dirname }) 而非 inline files
  • 不要用 deprecated check()

3.4 写好 commit message

Next.js 不强求 conventional commits,但简明扼要的 imperative 风格是惯例:

Fix incorrect cache key for static params in App Router

When generateStaticParams returns nested objects, the cache key was
constructed without normalization. Add a stable JSON serialization
to ensure consistent keys across builds.

Fixes #12345

不超过 50 字符的 subject,blank line,详细 body,可选 Fixes # 链接 issue。

四、PR 提交流程

4.1 推 fork

git push -u origin my-fix

4.2 打开 PR

GitHub 会弹"open pull request"。注意:

  • target branch 选 canary
  • 标题简明:fix: correct cache key for nested static params
  • 描述区会自动套用模板 .github/pull_request_template.md

模板里要填:

  • What? 简述变更
  • Why? 链接 issue 或解释动机
  • How? 关键设计/取舍
  • 复选框:documentation updates / tests / breaking change
  • 末尾保留隐藏注释(团队 LLM 标记):
<!-- NEXT_JS_LLM_PR -->

⚠️ PR 默认 draft。维护者建议留 draft,准备好后再 ready for review。你不要自己点 "ready for review",让维护者决定(CLAUDE.md 明确说)。

4.3 等 CI

PR 提交后跑大量 CI job:

  • build / lint
  • types
  • prettier
  • eslint
  • 各种 mode 的 test(dev / prod × turbo / webpack)
  • rust check / build 如果你动了 Rust

CI 一般 15-30 分钟跑完。失败时:

# 在本地复现 CI 失败
node scripts/pr-status.js   # 自动识别当前分支对应的 PR
# 生成 scripts/pr-status/ 下的分析文件

按 CLAUDE.md 里 $pr-status-triage skill 的优先级:build > lint > types > tests。先修阻塞的。

4.4 review 流程

维护者(如 @timneutkens / @ijjk / @feedthejim / ...)会 review。常见反馈:

  1. "Please add a test case for X"
  2. "This logic should live in Y file instead"
  3. "Performance: avoid allocating in hot loop"
  4. "Consider edge runtime"
  5. "Breaking change—needs codemod"

针对反馈:

  • 在分支上加 commit(不要 squash 后强推)
  • 留言回复每一条
  • 改完 ping reviewer

4.5 合并

通过 review + CI 全绿 + 维护者批准 → 维护者 squash & merge 进 canary。

下个 canary 发版(几乎每天)你的修复就 ship 了。约 2-4 周后随主版本进 stable。

五、维护者关注什么(写 PR 前对照)

按从大到小:

关注点解释
正确性没引入新 bug;现有测试不挂;新增测试覆盖
性能hot path 无 deopt(第 36 讲);bundle size 不上涨
DX错误信息友好;dev overlay 体验好
Edge 兼容改动是否在 edge runtime 跑得动;是否过早 import node:*
Stable API不破坏现有 public API;experimental flag 标好
代码风格跟周围代码一致;不引入新依赖
测试质量测试稳定不 flaky

六、PR 容易被拒的常见原因

原因改进
没有测试覆盖必须加 e2e 或 unit 测试
测试不稳定 / 用 setTimeout改用 retry()
引入新依赖通常 reject;除非必要并讨论过
大改架构未先讨论先开 RFC 或 issue 讨论
改 internal API 但没改对应 callsite找全所有调用方
breaking change 没提供 codemod大版本里通常要求 codemod
只改了 webpack 没改 turbopack(或反之)两条线都要顾
commit 历史混乱(强推、合并提交)保持 clean linear history

七、内部工具速查

命令用途
pnpm new-test -- --args true my-feature e2e生成新测试模板
pnpm test-dev-turbo <path>dev + turbopack 跑测试
pnpm test-start-webpack <path>prod + webpack 跑测试
pnpm testheadless <path>不重 build,直接跑测试
pnpm --filter=next types类型检查(10s 比 build 60s 快)
pnpm lint / pnpm lint-fixlint
pnpm prettier-fixformat
node scripts/pr-status.js拉取本分支 PR 的 CI 状态
cargo fmtRust 格式化

八、长期成长路线

毕业后想持续深入:

8.1 follow 通道

  • GitHub Watch vercel/next.js → 选 "Custom" → Releases + Discussions
  • 订阅 https://nextjs.org/blog
  • Twitter: @leeerob @timneutkens @feedthejim @sebmarkbage(React core)
  • 每周看 git log --oneline upstream/canary 了解动态

8.2 阅读 RFC / Discussions

vercel/next.js/discussions 是 RFC 区。重大架构变化(如 PPR、cache components)都先在这里讨论。早期介入有助于:

  • 知道下一个 major 会有什么
  • 提建议影响设计
  • 早晚体验,先你团队一步用上

8.3 升 contributor → reviewer

  1. 持续提合理 PR,建立信誉
  2. review 别人的 PR(任何人都能 review,不需要权限)
  3. 在 discussion 里答疑
  4. 半年-1 年后可能拿到 triager / collaborator 权限

8.4 持续阅读源码的节奏建议

每次新版发布:

  1. 看 release notes,挑 2-3 个让你好奇的功能
  2. 找对应 PR,读 diff
  3. 把 diff 涉及的目录读一遍
  4. 写一份笔记

3-5 个版本后你会对 Next.js 内部建立起非常完整的 mental model。

九、配套 fixture

fixtures/lecture-40/ 是一份 "PR 模板演练":

  • MOCK_ISSUE.md:假 issue 描述
  • MOCK_REPRO/:最小复现 Next.js fixture
  • MOCK_PATCH.diff:示意性补丁
  • MOCK_TEST.test.ts:测试模板

不能真的对接 GitHub workflow(需要 vercel/next.js 权限),但作为"流程演练"足够。

启动:

cd learning/nextjs-40-lectures/fixtures/lecture-40
# 阅读所有 MD 文件,按 README 步骤模拟

十、本讲与全课总结

整门 40 讲走完,你应该具备:

能力达成
对 Next.js 整体架构有完整 mental model✅(讲 1-4 / 15-22)
阅读 Next.js 任意源码模块✅(贯穿全部)
排查生产环境常见问题✅(讲 33-38)
优化 Next.js 应用性能✅(讲 35-36)
给 Next.js 写扩展(adapter / cache handler / codemod)✅(讲 39)
给 Next.js 主仓库提 PR✅(本讲)

整门课讲的范围已经覆盖了 Next.js core team 公开的 99% 知识点。剩下 1% 在两个地方:

  1. Vercel 内部基础设施(serverless runtime、edge 路由、CDN)—— 不公开
  2. 正在开发中的新功能(cache components、新 DX、新 RSC API)—— 跟 RFC

恭喜走完 40 讲!现在你不仅是"会用 Next.js 的人",是"懂 Next.js 内部、能扩展 Next.js、能给 Next.js 贡献"的人。

长期建议

整门课的核心思想:抽象只有读了实现才能真懂。Next.js 是个完美样本——它把现代前端最复杂的几件事(RSC、Streaming、Build pipeline、Edge runtime、Caching)都做成生产级代码。任何对你不清楚的地方,去读源码,永远比读文档讲得更准确。

希望未来你不仅自己用得溜,也能成为团队里讲清楚 Next.js 的人,乃至给社区贡献价值。

— 第 40 讲完 —