Next.js 本地 React 同步流程详解:从 build-for-next.sh 到 pnpm sync-react 的集成验证
2026/9/7 8:45:09 网站建设 项目流程

Next.js 本地 React 同步流程详解:从 build-for-next.sh 到 pnpm sync-react 的集成验证

【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js

本篇技术指南基于 Next.js 仓库内置的 Agent 技能文档 .agents/skills/react-sync/SKILL.md 展开,介绍 Next.js 维护者验证"本地 React 改动"的完整工作流:先在 React 仓库构建出 Next.js 消费的 bundle 变体(build/oss-stablebuild/oss-experimental),再通过pnpm sync-react将其同步进本地 Next.js checkout,最后运行聚焦测试完成集成验证。读完后,你可以独立执行该流程,理解sync-react脚本内部的版本改写逻辑、双通道(stable/experimental)机制,以及 Next.js 将 React 打包为 vendored 副本的底层原因。

一、适用场景:什么时候需要这条流程

该技能文档的定位是内部(internal: true)工作流,触发条件在文档 frontmatter 中写得很明确:当你在React 仓库中修改了代码,且这些改动需要在 Next.js 中验证,或者被要求执行buildForNextpnpm sync-react、将本地 React checkout 与 Next.js 保持同步时,使用本流程。

它与常规"升级 npm 上已发布的 React canary 版本"是两条不同的路径。sync-react脚本本身支持多种来源(npm canary、vp:///commit-sha、本地 checkout),而本技能文档聚焦的正是本地 checkout 场景:React 源码尚未发布到 npm,必须先本地构建,再作为file://依赖注入 Next.js。

前提条件(以当前仓库为准):

  • Next.js 仓库要求 Node.js>=20.9.0(见 package.json 的engines字段),包管理器锁定为pnpm@10.33.0(package.json 的packageManager字段,经 Corepack 生效);
  • React 仓库必须已按脚本要求完成构建(这是文档第 24 行注释的硬性要求,见 scripts/sync-react.js)。

二、第一步:构建 Next.js 消费的 bundle 变体

文档给出的第一步操作是:工作目录必须在 React 仓库内,然后以绝对路径调用 Next.js 仓库中的构建脚本:

cd <react-repo> bash <next-repo>/.agents/skills/react-sync/scripts/build-for-next.sh

之所以要求"传绝对路径 + cwd 在 React 仓库",是因为脚本内部调用的是 React 仓库自身的node ./scripts/rollup/build.js,而脚本文件本身存放在 Next.js 仓库中,两处分离。执行完成后,React 仓库根目录下会生成两个关键目录:

  • build/oss-stable:稳定通道(RELEASE_CHANNEL=stable)的构建产物;
  • build/oss-experimental:实验通道(RELEASE_CHANNEL=experimental)的构建产物。

脚本内部做了什么

查看 .agents/skills/react-sync/scripts/build-for-next.sh 可以确认完整细节。脚本以set -euo pipefail严格模式运行,定义了 21 个 bundle 入口:

bundles=( "react/index" "react/jsx" "react-jsx-runtime.react-server" "react-jsx-dev-runtime.react-server" "react/compiler-runtime" "react.react-server" "react-dom/index" "react-dom/client" "react-dom/profiling" "react-dom/server" "react-dom.react-server" "react-dom-server.browser" "react-dom-server.bun" "react-dom-server.edge" "react-dom-server.node" "react-dom-server-legacy.browser" "react-dom-server-legacy.node" "react-is" "scheduler" "react-server-dom-webpack/" "react-server-dom-turbopack/" )

这个清单值得注意两点:其一,它覆盖了 App Router 所需的RSC 双端入口*.react-server变体)与react-server-dom-webpack/react-server-dom-turbopack/两个 Flight 打包层,这与 Next.js 对 React 的 vendoring 需求一一对应;其二,构建产物按 8 种 target type 输出:

type="BUN_DEV,BUN_PROD,NODE_DEV,NODE_PROD,NODE_PROFILING,ESM_DEV,ESM_PROD,NODE_ES2015"

随后脚本分两次调用 React 的 rollup 构建:

RELEASE_CHANNEL=stable node ./scripts/rollup/build.js "${bundles[@]}" --type="$type" mv ./build/node_modules ./build/oss-stable RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js "${bundles[@]}" --type="$type" --unsafe-partial mv ./build/node_modules ./build/oss-experimental

稳定通道正常构建后,把 rollup 默认输出目录build/node_modules重命名为build/oss-stable;实验通道额外带--unsafe-partial参数(用于构建未完成的 partial 变更),重命名为build/oss-experimental

为什么是这两个目录名?这不是随意的约定。在 scripts/sync-react.js 中,当--version传入file://路径时,脚本会精确地把稳定与实验通道指向这两个子目录:

if (version !== null && version.startsWith('file://')) { experimentalNewVersionStr = new URL('build/oss-experimental/', version).href newVersionStr = new URL('build/oss-stable/', version).href }

因此第一步的产物结构必须与第二步的解析逻辑严格对齐,目录名不能改动。

三、第二步:将构建产物同步进 Next.js

文档给出的同步命令带有PATH前缀,必须在 Next.js 仓库根目录执行:

cd <next-repo> PATH="$(dirname "$(command -v corepack)"):$PATH" \ pnpm sync-react --version <react-repo>

sync-react是仓库根 package.json 中注册的 npm script,实际执行node ./scripts/sync-react.js。传入的<react-repo>是本地 React checkout 的绝对路径。

PATH 前缀的作用:一个具体的兼容性陷阱

文档花了一整段解释为什么要加这个PATH前缀,这是本技能文档中最有实战价值的细节。原因是特定 Agent 环境(Codex)存在PATH优先级缺陷:

  • Codex 会向PATH中注入一个自带的pnpm可执行文件,且它排在用户 Corepack shim 之前;
  • 命令行查找发生在 Corepack 读取仓库packageManager字段之前,因此裸调用pnpm可能命中 Codex 自带的版本,而不是仓库锁定的pnpm@10.33.0
  • 命令前缀PATH="$(dirname "$(command -v corepack)"):$PATH"把激活 Corepack shim 的目录置于PATH最前面,使命令查找重新经由 Corepack 解析到仓库锁定的版本。

文档还特别指出:只在外层命令前加corepack pnpm是不够的,因为sync-react脚本内部还会嵌套调用pnpm install(见下文),子进程依然从PATH中解析pnpm。只有修改PATH本身才能同时覆盖外层命令与嵌套调用。

sync-react.js 内部执行了什么

理解脚本源码后,本地同步的完整动作链是:

  1. 版本字符串归一化(scripts/sync-react.js):--version/开头的路径会被pathToFileURL转换为file://URL,并补上结尾斜杠,使其被当作目录处理。
  2. 双通道版本改写:脚本先同步experimental通道(更新react-experimental-builtinreact-dom-experimental-builtinscheduler-experimental-builtinreact-server-dom-webpack-experimentalreact-server-dom-turbopack-experimental等 devDependencies),再同步稳定通道。稳定通道的改写范围更大,除了react-builtinreact-dom-builtinscheduler-builtinreact-is-builtin外,还会更新根package.jsonpnpm.overrides下的reactreact-domschedulerreact-is(scripts/sync-react.js)。当前仓库 package.json 的 overrides 中可以看到这种npm:react@<canary>形态的锁定结果。
    • 本地file://场景下,getSchedulerVersion不做 npm registry 查询,scheduler 直接复用同一本地路径(scripts/sync-react.js);npm 版本场景下则从 registry 拉取react-dommanifest 中的scheduler依赖版本。
  3. Pages Router 的 peer 版本联动:Next.js 同时服务 App Router(使用内置 React)与 Pages Router(使用用户自装的 React),因此脚本会更新三处引用const nextjsReactPeerVersion = "..."的文件——run-tests.js、packages/create-next-app/templates/index.ts 与 test/lib/next-modes/base.ts(当前值均为"19.2.8"),并同步更新packages/next/package.jsonpackages/third-parties/package.jsonpeerDependencies(范围形如^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || <active>,见 scripts/sync-react.js)。
  4. 安装依赖并重建 vendored 文件pnpm install --no-frozen-lockfile(因为版本刚被改写,lockfile 必然变化),随后在packages/next目录内执行pnpm ncc-compiled(即taskr ncc,见 packages/next/package.json),重新编译 vendored React 产物(scripts/sync-react.js)。
    • 若只想改package.json不自动安装,可用pnpm run sync-react --no-install,随后手动执行pnpm installpnpm ncc-compiled(脚本头部注释,scripts/sync-react.js)。

补充说明:脚本还支持--create-pull--commit--actor等选项(scripts/sync-react.js),用于 CI 场景自动创建带签名 commit 的 Pull Request;本地开发验证不需要这些选项。

四、为什么同步后要重建:React 的 vendoring 机制

pnpm ncc-compiled这一步不是形式流程,而是由 Next.js 的架构决定的:App Router 不通过node_modules解析 React,而是在pnpm build阶段把 React 复制进packages/next/src/compiled/。这一过程由 packages/next/taskfile.js 中的copy_vendor_react()任务完成,它分 stable/experimental 两次执行,分别产出compiled/react/compiled/react-experimental/两条通道。

从 taskfile 源码可以看到两个关键动作(packages/next/taskfile.js):

  • 包名改写:把 vendored 包的name追加-builtin/-experimental-builtin后缀,避免 React 内部 Haste module map 与node_modules中的同名包冲突;
  • require 别名重写:源码中require("react")require("react-dom")require("scheduler")被静态替换为require("next/dist/compiled/react...")等指向编译产物的路径。

运行时 webpack 配置再通过makeAppAliases({ experimental })将别名指向对应通道。而 Pages Router 仍然从node_modules正常解析 React。这一机制也正是sync-react同时维护两条 devDependency 通道与两组pnpm.overrides的原因:App Router 吃 vendored 副本(由*-builtin包驱动),Pages Router 的 peer 声明则指向 npm 上的版本范围。

五、第三步:检查同步结果并运行聚焦测试

文档第三步的要求是:

  1. 先检查同步结果,确认两个 checkout 中的无关改动都被保留(本地同步会触及package.json、lockfile、peer 声明文件与dist/compiled产物,diff 面较大);
  2. 按需重建 Next.js(vendored React 变化通常意味着需要重新构建);
  3. 运行与改动行为匹配的聚焦测试命令,而不是全量测试。

结合仓库实际可用的测试入口,常见的聚焦方式包括:

# 单元测试(不涉及浏览器启动,最快) pnpm test-unit # dev/start 模式的 e2e 测试,按 bundler 区分 pnpm test-dev-webpack pnpm test-start-webpack pnpm test-dev-turbo # 指定测试文件(run-jest.sh 透传 jest 参数) pnpm test-dev-webpack -- --testPathPattern app/server-components

另有一个测试侧的版本控制点:测试基础设施中通过NEXT_TEST_REACT_VERSION环境变量可覆盖被测 React 版本(run-tests.js、test/lib/next-modes/base.ts),排查"vendored 版本 vs 用户安装版本"差异时有用。

六、延伸:与 react-vendoring 技能的边界

SKILL.md 末尾将 react-vendoring 列为关联技能,两者分工明确:react-sync负责"把新 React 放进 Next.js",react-vendoring负责"放进去之后,vendored 副本的类型边界与 React Server 层约束"。后者规定了若干同步后改动时容易踩坑的边界,例如:

  • 所有对react-server-dom-webpack/*(Flight server/static API)的导入必须经由entry-base.ts,其他文件需通过ComponentMod参数访问;
  • 新增 Node-only API(如renderToPipeableStream)需要在packages/next/types/$$compiled.internal.d.ts补类型声明;
  • Turbopack 会在运行时把react-server-dom-webpack/*静默重映射为react-server-dom-turbopack/*,调试时堆栈中出现的 turbopack 变体属于正常现象。

七、流程速查

步骤工作目录命令产物
1. 构建 React 变体React checkoutbash <next-repo>/.agents/skills/react-sync/scripts/build-for-next.shbuild/oss-stablebuild/oss-experimental
2. 同步进 Next.jsNext.js checkoutPATH="$(dirname "$(command -v corepack)"):$PATH" pnpm sync-react --version <react-repo>双通道 devDependencies、pnpm.overrides、lockfile、vendored 副本
3. 验证Next.js checkout检查 diff → 按需重建 → 聚焦测试(如pnpm test-dev-webpack集成验证结论

整体来看,这条流程是 Next.js 与 React 两个仓库协同开发的关键基础设施:build-for-next.sh定义了"Next.js 需要哪些 bundle"的消费契约,sync-react把本地 React 构建以file://依赖形式注入双通道并联动 peer 声明,而 vendored 机制保证了 App Router 始终运行在与测试环境一致的 React 副本上。理解这条链路,是排查 RSC、Flight 协议与 React 升级相关问题的必要前提。

【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询