Next.js 14 到 15 自动化升级全流程:以 @next/codemod upgrade 的 React 19 迁移夹具为切入点
2026/9/8 23:55:59 网站建设 项目流程

Next.js 14 到 15 自动化升级全流程:以 @next/codemod upgrade 的 React 19 迁移夹具为切入点

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

@next/codemod upgrade是 Next.js 官方为「从一个大版本平滑升级到下一个大版本」设计的自动化 CLI:它先探测项目当前安装的 Next.js 与 React 版本,再依次完成依赖版本解析、交互式确认、语义化 codemod 批量改写以及安装执行。本文以 packages/next-codemod/bin/testfixtures/next-14-installed/README.md(测试夹具的期望输出快照)为核心骨架,结合upgrade子命令的真实实现与相关 transform 源码,完整还原「Next.js 14 + React 18 项目升级到 Next.js 15 + React 19」时工具会做什么、为什么这么做,以及每步改动在 package.json 中留下的痕迹。读完后你将能预判升级工具的全部行为,并在自己的项目上安全地复现同样的迁移。

1. 这篇文档是什么:一份「升级行为」的黄金快照

next-14-installed是 packages/next-codemod 测试夹具目录之一,它描述了一个起始版本明确、升级行为可预期的典型场景。目录里只有三个文件:

  • package.json:夹具的「迁移前」状态,即一个依赖next@14.3.0-canary.44react@18.2.0react-dom@18.2.0@types/react@^18.2.0@types/react-dom@^18.2.0的最小项目;
  • pnpm-workspace.yaml:空文件,用于模拟 pnpm 项目中的 workspace 声明;
  • README.md:这份关联文档本身,它以「行为清单 + package.json diff」的形式记录了升级工具在这一夹具上的全部预期动作与最终输出,属于验证upgrade流程的黄金快照。

因此,这份 README 的价值不在其篇幅,而在于它精确刻画了官方对「Next.js 14 老项目升 15」的完整自动化策略。下面逐一对照 upgrade 主流程实现 展开。

2. 升级起始状态:一个典型的 Next.js 14 + React 18 项目

夹具的迁移前package.json如下:

{ "name": "next-14-installed", "scripts": { "dev": "next dev" }, "dependencies": { "next": "14.3.0-canary.44", "react": "18.2.0", "react-dom": "18.2.0", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0" } }

它同时代表了几层含义:

  1. 版本探测依据:升级入口runUpgrade首先通过require.resolve('next/package.json')读取当前安装的 Next.js 版本(见 upgrade.ts 中getInstalledNextVersion),若在 monorepo 根目录找不到会抛出BadInput,提示应到具体 app 目录执行。
  2. React 18 是「历史包袱」:夹具中 React 仍是 18.2.0,这是升级工具需要重点决策的分支点。
  3. 没有 app/pages 目录:夹具项目既不含app也不含pages目录,因此在后面提到的「是否停留在 React 18」询问中不满足"纯 App Router"条件,会走交互式确认分支。

值得注意:该版本组合next@14.3.0-canary.44是刻意选择的临界点——从14.3.0-canary.45起 Next.js 的 peer 依赖要求 React 版本为19.0.0-beta.0(源码注释中给出了 PR/Release 引用),意味着任何低于此版本的 Next.js 14 项目在升级后都将面对 React 大版本跳变。

3. README 记录的五大升级动作逐一拆解

夹具 README 开头的 5 行即升级工具对该场景输出的全部交互与建议

  1. Prompts for React 19 upgrade with a recommendation to do so(提示升级 React 19 并给出推荐)
  2. Suggests adding--turbopacktonext devscript(建议给 dev 脚本追加--turbopack
  3. Suggestsapp-dir-runtime-config-experimental-edgetransform
  4. Suggestsnext-async-request-apitransform
  5. Suggestsnext-request-geo-iptransform

下面逐条对应到runUpgrade的执行顺序。

3.1 React 19 升级询问:哪些项目会问、哪些项目直接升

对应实现位于 upgrade.ts 的 React 分支判断逻辑,触发条件同时满足:

  • 目标 Next.js 版本>= 14.3.0-canary.45compareVersions(targetNextVersion, '14.3.0-canary.45') >= 0);
  • 当前安装的 React 版本以18开头;
  • 不是纯 App Router 项目(即没有只存在app目录而完全不存在pages目录)。

若项目是纯 App Router,工具直接视为必须使用 React 19,不弹窗;若项目同时使用 pages 和 app(混合模式),询问文案会额外提示 "we recommend upgrading React to use a consistent version throughout your app",即推荐统一到 React 19。夹具项目两者目录都没有,故满足弹窗条件,交互结果默认"不留在 React 18"(initial: false)。

shouldStayOnReact18 = true时,React 被固定在18.3.1(React 18 的最后一个 minor 版本);否则通过loadHighestNPMVersionMatchingreact@<目标next的peerDependencies.react>为查询条件去 npm registry 解析最高匹配稳定版,例如夹具中最终落到了19.0.0

3.2 建议给next dev追加--turbopack

仅当目标版本落在>= 15.0.0-canary && < 16.0.0-canary区间时,suggestTurbopack才会触发。它的三段式启发逻辑(源码注释写得很清楚)是:

  1. dev 脚本已含--turbopack→ 什么都不做;
  2. dev 脚本含next dev→ 询问是否启用 Turbopack(非交互模式默认启用),然后替换为next dev --turbopack
  3. dev 脚本不含next dev→ 提示用户手动把启动参数加进自定义命令。

夹具项目 dev 脚本就是最普通的"dev": "next dev",因此匹配第 2 条路径,得到"建议追加--turbopack"这一行为。

一个容易被忽略的细节是 flag 的命名分界:从v15.0.1-canary.3(PR #71657)起 Turbopack 标志从--turbo改为--turbopack。因此若项目从旧版本升上来且 dev 脚本里还残留--turbo,工具会自动把它替换成--turbopack——夹具 README 输出使用--turbopack,正是因为目标版本15.0.4-canary.43已越过该分界点。

3.3 三个被"推荐"的 codemod 是怎么被选出来的

升级工具不会一股脑运行所有 codemod,而是依据统一的 codemod 注册表(TRANSFORMER_INQUIRER_CHOICES)做区间过滤:只推荐「版本介于当前 Next.js 版本与目标版本之间」的 transform。

每个注册项都带一个version字段,代表该 codemod 从哪个 Next.js 版本起生效。注册表注释特别强调:新增 codemod 时务必填写目标 canary 版本而非稳定版,这样从 canary 升 canary 时也能被正确命中。以夹具为例,当前14.3.0-canary.44,目标15.0.4-canary.43,注册表中恰好落在该区间内的三项是:

codemod value注册版本作用
next-request-geo-ip15.0.0-canary.153安装@vercel/functions以替代NextRequest上的geo/ip属性
next-async-request-api15.0.0-canary.171改写 Next.js 异步 Request API 的用法
app-dir-runtime-config-experimental-edge15.0.0-canary.179把 Route Segment Config 的runtime: 'experimental-edge'转为'edge'

而注册表中紧随其后的next-experimental-turbo-to-turbopack15.4.2-canary.21)高于目标版本15.0.4-canary.43,所以不会被推荐——这解释了为何 README 恰好只列了三项。

交互模式下会以多选框呈现,默认全部选中;非交互(--yes或非 TTY)模式则按注释所言 "Every prompt will accept its default",全部应用。

3.4 三个 transform 到底改了什么(源码佐证)

app-dir-runtime-config-experimental-edge:实现见 app-dir-runtime-config-experimental-edge.ts。它先用正则/[/\\]app[/\\].*?(page|layout|route)\.[^/\\]+$/限定只处理 App Router 的page/layout/route文件,再定位名为runtime的具名导出,把字符串字面量'experimental-edge'替换为'edge'

runtimeValue.replaceWith(j.stringLiteral('edge'))

测试用例见 app-dir-runtime-config-experimental-edge.test.js。这是 Next.js 15 中experimental-edgeruntime 命名收敛为edge的自动改写。

next-async-request-api:入口见 next-async-request-api.ts,它把具体逻辑转发到lib/async-request-api/,配套测试有 next-async-request-api-dynamic-apis.test.js 与 next-async-request-api-dynamic-props.test.js。对应 Next.js 15 中cookies()headers()draftMode()等请求相关 API 全面异步化的迁移,把旧的同步取值改为await调用。

next-request-geo-ip:实现见 next-request-geo-ip.ts。Next.js 15 将NextRequest上内置的geo/ip请求信息迁移到独立包@vercel/functions,该 transform 负责把以下形态全部改写为函数调用:

  • req.geo/req.ipgeolocation(req)/ipAddress(req)
  • 解构const { geo, ip } = req→ 拆成独立声明const geo = geolocation(req)
  • NextRequest["geo"]/NextRequest["ip"]类型访问 → 换成@vercel/functions的类型(geolocation/ipAddress命名空间下)并自动补 import。

测试见 next-request-geo-ip.test.js。

4. package.json 的最终 diff:从 14 canary 到 15 canary + React 19

夹具 README 用一段完整 diff 展示了升级后package.json的变化,这也是该场景的核心产物,原样继承如下:

diff --git a/packages/next-codemod/bin/__testfixtures__/next-14-installed/package.json b/packages/next-codemod/bin/__testfixtures__/next-14-installed/package.json index 5ec4c37f0b..131f5b9f4a 100644 --- a/packages/next-codemod/bin/__testfixtures__/next-14-installed/package.json +++ b/packages/next-codemod/bin/__testfixtures__/next-14-installed/package.json @@ -4,10 +4,16 @@ "dev": "next dev" }, "dependencies": { - "next": "14.3.0-canary.44", - "react": "18.2.0", - "react-dom": "18.2.0", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0" + "next": "15.0.4-canary.43", + "react": "19.0.0", + "react-dom": "19.0.0", + "@types/react": "19.0.0", + "@types/react-dom": "19.0.0" + }, + "pnpm": { + "overrides": { + "@types/react": "19.0.0", + "@types/react-dom": "19.0.0" + } } }

这段 diff 可以从 upgrade.ts 的实现中逐一验证:

  • 版本映射表versionMappingnextreactreact-domrequired: true,只要项目里声明过就会写入;react-isoptionalNextjsPackageseslint-config-next@next/mdx@next/env@next/third-parties等共 15 个)为可选依赖,仅当项目中已存在时才跟随升级到同一版本。工具遍历后调用addPackageDependency写入并统一以JSON.stringify(..., null, 2)+ 换行符落盘。
  • 精确版本而非范围:代码注释解释了原因——直接把peerDependencies里类似^18.2.0 || ^19.0.0 || 20.0.0-canary这种"丑陋"的区间写进 manifest 会污染依赖声明,因此统一先解析出最高匹配的精确版本。
  • @types/react/@types/react-dom落到精确的19.0.0:对于稳定版 React 分支,工具以@types/react@<react 的 peerDependencies>查询 npm 并取最高版本;代码注释提到https://github.com/microsoft/DefinitelyTyped-tools/issues/433的隐患,因此即便只有 alias 需求也会把类型包加入 overrides 兜底。

4.1 overrides 字段按包管理器分流的底层实现

diff 最后新增的pnpm.overrides不是随便写的:writeOverridesField会根据探测到的包管理器选择字段落点(upgrade.ts):

包管理器写入位置
npmpackage.json#overrides
pnpm(v10 及以下)已有resolutions则并入,否则写入package.json#pnpm.overrides
pnpm(v11 及以上或版本不可探测)改写入pnpm-workspace.yaml#overrides(见writePnpmWorkspaceOverrides,延迟require('js-yaml')同步读写)
yarnpackage.json#resolutions
bun已有resolutions则并入,否则写入package.json#overrides

夹具 README 展示的是 pnpm 且落在 v11 之前的行为(写入package.json#pnpm.overrides);仓库里同目录的兄弟夹具 pnpm-v11-overrides 则专门验证了 pnpm v11+ 场景——此时pnpm.overrides会被 pnpm v11 静默忽略,必须写到pnpm-workspace.yaml#overrides。源码注释给出了依据:pnpm v11 起 canonical 位置迁移到pnpm-workspace.yaml#overrides,并对无法探测的版本默认按 v11 布局处理。

5. 升级的完整链路与配套的 React 19 生态工具

依赖写入并runInstallation完成后,工具会依次执行三件事(upgrade.ts):

  1. 对第 3.3 节选出的 codemod 逐个调用runTransform(codemod, cwd, { force: true, verbose, nonInteractive })应用改写;
  2. 若选择了 React 19 升级,再调用codemod@latest react/19/migration-recipe --no-interactive --allow-dirty(React 官方 React 19 迁移 recipe;--allow-dirty是必需的,因为前面已修改了 package.json 与 lockfile,recipe 拒绝在脏工作区运行);
  3. 调用types-react-codemod@latest --yes preset-19 .处理 React 19 的 TypeScript 类型变更(这两个外部命令根据包管理器选择npx --yes/pnpm --silent dlx/yarn --quiet dlx/bunx)。

收尾阶段还会尽力刷新项目根AGENTS.md/CLAUDE.md中由工具托管的 agent-rules 块使其与新版本匹配(刷新逻辑见 agents-md.ts),该动作是 best-effort,失败不会中断升级;随后warnDependenciesOutOfRange会扫描node_modules下直接依赖的peerDependencies,把与升级后版本不兼容的依赖以✕ unmet peer树形结构打印出来。

6. 在自己的项目上复现这套升级

要复现夹具描述的流程,只需在目标项目根目录执行:

npx @next/codemod upgrade

关键选项与行为(均来自 upgrade.ts 的runUpgrade签名):

  • revision 参数:可传具体版本号、dist-tag(latest/canary/rc)或语义关键字;语义关键字在resolveSemanticRevision中解析——patch~主.次.0(当前 minor 内最新补丁)、minor^主.0.0(当前 major 内最新 minor)、majorlatest,默认值为minor。非法输入会抛出BadInput并列出可用版本。
  • --yes/ 非 TTY:进入非交互模式,所有提示取默认值(React 18 默认不保留、推荐 codemod 全部应用)。
  • --verbose:打印"Resolved upgrade target"与"Target version"等中间信息,便于排查。
  • 命令须在包含package.json的 Next.js 应用目录下运行;monorepo 场景需切到具体 app 目录,否则版本探测会失败。

工具最后会输出一段结束语:要求人工复核本地改动,并按提示阅读 Next.js 15 迁移指南完成迁移的剩余部分(endMessagemajor(targetNextVersion) === 15分支)——这提醒我们,codemod 只能完成机械改写,路由、数据请求方式等语义层面的最终确认仍需人工 review

7. 小结:从一份夹具快照读懂官方升级策略

next-14-installed夹具看似只是几行行为描述与一段 diff,但它完整浓缩了 Next.js 官方对"老项目跨大版本升级"的工程化策略:以注册版本做 codemod 区间过滤、以 peerDependencies 做依赖精确解析、以包管理器差异做 overrides 落点分流、再叠加 React 官方迁移工具链。对照 upgrade.ts 与 lib/utils.ts 阅读,你不仅能预判工具在任意版本组合下的行为,也能在遇到"为什么升级后多出 pnpm.overrides""为什么某个 codemod 没被推荐"这类问题时,直接从源码中找到答案。

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

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

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

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

立即咨询