Strapi 版本升级指南:深入解析 @strapi/upgrade 升级工具的命令体系与 Codemod 机制
【免费下载链接】strapi🚀 Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.项目地址: https://gitcode.com/GitHub_Trending/st/strapi
本文以 Strapi 仓库中的packages/utils/upgrade/README.md为核心,系统讲解官方升级 CLI(@strapi/upgrade)的全部命令与选项、json/code两类代码转换(codemod)的原理与编写方式,并结合仓库源码说明 codemod 的目录规范、版本发现机制与升级流程约束,帮助你安全完成 Strapi 主版本迁移、并能为项目贡献自己的 codemod。
1. 升级工具的定位:为什么不要手改 package.json
Strapi Upgrade Tool 是一个专门用于在 Strapi 各版本之间迁移的 CLI 工具,对应仓库中的 packages/utils/upgrade 包,npm 包名为@strapi/upgrade(当前仓库版本为 5.52.2,见 package.json)。根据 README 的说明,它负责三件事:
- 将项目
package.json中的 Strapi 依赖更新到正确的版本; - 运行包管理器安装器完成依赖安装;
- 针对主版本(major)中的破坏性变更(breaking changes),运行官方提供的代码转换脚本(codemods)。
README 明确建议:升级到任何 major、minor、patch 版本时,都应使用该工具,而不是手动修改package.json,因为主版本升级往往伴随需要批量改动的 API 变更,codemod 可以替你完成这些机械性替换。
1.1 命令一览
工具提供以下命令(引自 README):
latest [options] Upgrade to the latest available version of Strapi major [options] Upgrade to the next available major version of Strapi minor [options] Upgrade to the latest minor and patch version of Strapi for the current major patch [options] Upgrade to latest patch version of Strapi for the current major and minor to <version> Upgrade to a specific version of Strapi codemods [options] Run a set of available codemods for the selected target version without updating the Strapi dependencies这四个按发布类型划分的命令(latest/major/minor/patch)并非简单重复——从源码 src/cli/commands/upgrade.ts 可以看到,它们通过addReleaseUpgradeCommand统一注册,只是把不同的releaseType作为target传给同一个upgrade动作函数。而to命令则接收一个具体版本号,并对参数做 semver 合法性校验(isValidSemVer),非法输入会抛出InvalidArgumentError直接拒绝执行。
1.2latest被注册策略挡住时怎么办
README 特别提到一个实战场景:当latest解析到的版本被 registry 策略(例如min-release-age,即"新版本必须发布满若干小时后才可安装")挡住时,应改用to命令显式指定一个已发布的版本:
npx @strapi/upgrade to 5.42.0对于预发布版本,则用--codemods-target指定要运行哪一套 codemod(默认取目标版本的major.minor.patch部分):
npx @strapi/upgrade to 5.0.0-beta.951 --codemods-target 5.0.0从 src/cli/commands/upgrade.ts 的注册代码看,--codemods-target(简写-c)是to命令独有的选项,其argParser会用isLiteralSemVer强制要求"<number>.<number>.<number>"的完整字面量格式,避免预发布号被误当作 codemod 目录名。在任务层(src/tasks/upgrade/upgrade.ts),该值通过upgrader.overrideCodemodsTarget(codemodsTarget)手动覆盖目标,这正是"装 5.0.0-beta.951、却跑 5.0.0 那套 codemod"的实现来源。
2. 通用选项:--dry、--project-path 与确认提示
latest/major/minor/patch/to命令共享同一组选项,定义在 src/cli/options.ts:
| 选项 | 简写 | 作用 | 默认值 |
|---|---|---|---|
--project-path <path> | -p | 指定 Strapi 应用或插件的根路径(不传则使用当前工作目录) | process.cwd() |
--dry | -n | 模拟升级,不实际修改任何文件 | false |
--debug | -d | 输出更多调试日志 | false |
--silent | -s | 不输出任何日志 | false |
--yes | -y | 对所有交互式提示自动回答"yes" | false |
--dry是安全验证升级效果的首选方式:dry: true会一路透传到 upgrader(见 upgrade.ts 的.dry(options.dry ?? false)),让整条流水线走完但跳过写盘。--yes则适合 CI 场景,源码中confirm闭包在yes为真时直接返回true,跳过prompts交互(commands/upgrade.ts)。
codemods子命令额外支持--range <range>(-r),用于按 semver 范围筛选要执行的 codemod,同样带有范围合法性校验。
3. 使用方式:npx、strapi upgrade 与 monorepo 开发
README 给出的标准用法是在 Strapi 项目目录内执行:
npx @strapi/upgrade --help npx @strapi/upgrade to 5.42.0README 还说明:
- 在已安装 Strapi 的项目中,也可以直接使用
strapi upgrade触发同一工具; - 在 Strapi 官方仓库内做 monorepo 开发、针对
examples示例应用联调时,可从示例应用目录直接运行../../packages/utils/upgrade/bin/upgrade(对应 package.json 中的"bin": "./bin/upgrade.js"入口)。
值得注意的是 src/tasks/upgrade/upgrade.ts 中的一处硬性约束:upgrade系列命令只能运行在Strapi 应用项目上,对插件项目会抛出错误并提示改用codemods命令:
The "<target>" upgrade can only be run on a Strapi project; for plugins, please use "codemods".也就是说,latest/major/to这类会改写依赖的命令面向应用;而codemods run则同时服务于应用与插件(见 commands/codemods.ts 的描述:在应用项目上默认只列与当前主版本匹配的 codemod,在插件项目上则列出全部)。
3.1 升级流程在源码中如何走
以to 5.42.0为例,任务层(src/tasks/upgrade/upgrade.ts)的执行顺序是:
- 解析
cwd,构建project对象并校验其是否为 Strapi 应用; - 通过
npmPackageFactory从 NPM registry 拉取@strapi/strapi的全部可用版本(refresh()); - 先调用
prompts.pinVersions把范围式的@strapi/*依赖固定为具体版本,再解析升级目标; - 创建 upgrader 实例,链式设置
dry、确认回调与 logger; - 若显式提供了
codemodsTarget则覆盖 codemod 目标版本; - 运行前置提示(
latest会额外走prompts.latest的确认流程); - 按目标类型挂载"要求(requirement)"后执行
upgrader.upgrade(),失败时抛出报告中的错误。
其中 major 升级会强制两个要求(upgrade.ts):
REQUIRE_AVAILABLE_NEXT_MAJOR:必须存在可用的下一个主版本;REQUIRE_LATEST_FOR_CURRENT_MAJOR:必须先把当前主版本升到最新 patch,再跨主版本。
而通过to <version>给出的具体 semver 目标会有意跳过这些检查。此外所有升级都会挂载一个可选的REQUIRE_GIT要求——源码注释解释其目的是让 git 仓库处于"干净"状态,便于升级失败时回滚。
4. 什么是 Codemod,两类 Transform 有什么区别
README 对 codemod 的定义是:以脚本化方式重构代码。当 Strapi 需要变更用户代码(例如重命名一个包、替换一个导入)时,官方不写"请手动全局替换"的升级手册,而是提供脚本,由工具扫描你的项目并自动完成替换。
工具提供两类 transform:
json:用于更新项目中的.json文件,主要目标是package.json;code:基于 jscodeshift 库的 codemod,用于更新.js与.ts源码。
仓库中真实存在的 codemod 位于 resources/codemods 目录,例如 5.0.0 版本包含 11 个转换脚本(如strapi-public-interface.code.ts、entity-service-document-service.code.ts),5.1.0 包含 1 个(dependency-better-sqlite3.json.ts)。
4.1 codemod 的命名与发现机制
编写 codemod 的第一条规则(引自 README):新建文件
upgrade/resources/codemods/{X.X.X}/{short-description-of-action}.{code|json}.ts其中X.X.X是该 codemod 服务的目标 Strapi 版本——例如 Strapi v5 首个正式版本的所有破坏性变更都放在upgrade/resources/codemods/5.0.0下。文件名中的连字符描述会被转换成展示给用户的空格分隔文本,如sqlite3-to-better-sqlite3显示为 "sqlite3 to better sqlite3"。
这个约定在源码中有严格的实现对应。CodemodRepository 的发现逻辑是:
refreshAvailableVersions:读取 codemod 根目录,只保留目录名是合法 semver的子目录,并按版本升序排列;refreshAvailableFilesForVersion:遍历各版本目录,只接受符合CODEMOD_FILE_REGEXP的文件;parseCodemodKindFromFilename(repository.ts):从文件名倒数第二段(.code.ts/.json.ts的code或json)解析 codemod 类型,并且断言该后缀必须在允许列表内——这就是为什么文件名必须严格遵循描述.{code|json}.ts格式,否则仓库加载阶段就会报错。
5. 编写jsontransform
README 给出的完整示例:针对根目录package.json,把dependencies.@strapi/strapi的版本改写为5.0.0:
import path from 'node:path'; import type { JSONTransform } from '../../..'; const transform: JSONTransform = (file, params) => { // Extract the json api and the cwd so we can target specific files const { cwd, json } = params; // To target only a root level package.json file: const rootPackageJsonPath = path.join(cwd, 'package.json'); if (file.path !== rootPackageJsonPath) { // Return the json object unmodified to pass it to the next transform return file.json; } // Use json() to get useful helpers for performing your transform const j = json(file.json); const strapiDepAddress = 'dependencies.@strapi/strapi'; // if this file contains a value at dependencies.@strapi/strapi if (j.has(strapiDepAddress)) { // we set the value to 5.0.0 j.set(strapiDepAddress, '5.0.0'); } // at the end we must return the modified json object return j.root(); }; export default transform;关键契约:json transform 会被调用于用户项目中的每一个 json 文件,函数必须返回(可能修改过的)json 对象,交给下一个 transform 接力处理;不关心的文件要原样返回。
README 引用的类型定义来自 src/modules/json/types.ts:
export interface JSONTransformAPI { get<T extends Utils.JSONValue>(path: string): T | undefined; get<T extends Utils.JSONValue>(path: string, defaultValue: T): T; has(path: string): boolean; set(path: string, value: Utils.JSONValue): this; remove(path: string): this; merge(other: Utils.JSONObject): this; root(): Utils.JSONObject; }各方法语义(README 原文 + 源码印证):
- get(path, default):读取路径值,不存在时返回默认值;
- set(path, value):按点分路径(如
engines.node、author.name)设置值; - has(path):判断路径是否存在;
- merge(obj):合并两个 json 对象;
- root():返回完整的 json 对象;
- remove(path):删除路径对应的属性(如
dependencies.strapi)。
从源码 src/modules/json/transform-api.ts 可以看到,这些方法全部是对 lodash/fp 的get、has、set、merge、omit的包装,构造函数中先cloneDeep一份输入,root()与get()返回的也都是深克隆——这意味着 transform 内部可以自由链式改写而不污染原始对象,remove实际由omit实现,天然支持路径式删除。
真实仓库中的 dependency-better-sqlite3.json.ts 是一个很好的参考实现:它同样只对根package.json生效,并额外用semver.valid+semver.lt做了"只升级、不降级"的保护——当现有依赖版本已是合法 semver 且低于12.8.0时才写入目标版本。编写 json codemod 时值得借鉴这一防御性写法。
6. 编写codecodemod
code 类 transform 使用 jscodeshift 库(该包在 package.json 中依赖 jscodeshift 17.3.0)修改代码。file与api参数直接来自 jscodeshift 的同名入参。README 的官方示例是把项目里所有的console.log调用改名为console.info:
import type { Transform } from 'jscodeshift'; const transform: Transform = (file, api) => { // Extract the jscodeshift API const { j } = api; // Parse the file content const root = j(file.source); root // Find console.log calls expressions .find(j.CallExpression, { callee: { object: { name: 'console' }, property: { name: 'log' } }, }) // For each call expression .forEach((path) => { const { callee } = path.node; if ( // Make sure the callee is a member expression (object/property) j.MemberExpression.check(callee) && // Make sure the property is an actual identifier (contains a name property) j.Identifier.check(callee.property) ) { // Update the property's identifier name callee.property.name = 'info'; } }); // Return the updated file content return root.toSource(); }; export default transform;写法要点:
- 用
j(file.source)得到 AST 根节点,用.find(节点类型, 过滤条件)定位目标语法; - 修改前用
j.MemberExpression.check(...)/j.Identifier.check(...)做类型守卫,避免误改结构不匹配的节点; - 最后必须
return root.toSource()返回更新后的源码字符串。
一个更具代表性的真实案例是 strapi-public-interface.code.ts:它把旧版import strapi from '@strapi/strapi'; strapi()的用法转换为新的公开接口——ESM 下改写为import { createStrapi } from '@strapi/strapi'并调用createStrapi(),CommonJS 下则改写为strapi.createStrapi()。文件头部用注释块完整记录了 Before/After 对照,这正是官方 codemod 的良好文档习惯。
7. codemods 子命令:不升级依赖只跑转换
codemods命令组(注册于 src/cli/commands/codemods.ts)包含两个子命令:
codemods run [uid]:对当前项目执行一组 codemod。不带uid时,交互式列出项目可用的全部 codemod 供多选(源码使用autocompleteMultiselect提示,默认全选);提供uid时只运行对应的那一个。其默认 target 为major(DEFAULT_TARGET = Version.RELEASE_TYPES.Major),可用-r/--range覆盖为自定义 semver 范围;codemods ls:列出可用 codemod。
两者在执行前都会打印备份警告("Please make sure you've created a backup of your codebase and files before running the codemods")。run的完整选项为--project-path、--dry、--debug、--silent、--range,其中--dry让你在真正动手前预览将发生的代码变更。
查询侧的实现(CodemodRepository.find)支持按 semver 范围(range.test(version))与 uid 列表双重过滤,且只返回至少含 1 个 codemod 的版本分组——这就是ls输出按版本组织、run [uid]能精确定位的底层机制。
8. 数据迁移:升级工具不做什么
README 专门划清了边界:数据迁移(data migrations)不由升级工具负责。
- 对 Strapi v4:不会允许数据迁移,也没有计划支持(极端特殊情况如与数据库结构相关的关键安全问题除外);
- 对 Strapi v5:自动化的数据迁移可以加入本仓库
develop分支的packages/core/database包中。
因此,用@strapi/upgrade完成依赖与代码层面迁移后,仍需自行关注数据库结构相关的兼容性问题;仓库中的 tests/migration 目录(含 CHECKPOINTS.md 与场景框架)可作为 v5 数据迁移机制的测试参考。
9. 实践清单与适用前提
- 升级前备份:CLI 在每次
upgrade与codemods run时都会主动打印备份警告,源码中还有可选的 git 干净状态要求来帮助回滚; - 先用
--dry模拟:加-n参数跑完整流程但不落盘,确认 codemod 影响面后再实跑; latest被min-release-age类策略挡住时,改用to <version>指定具体已发布版本;- 预发布目标要配合
-c, --codemods-target <major.minor.patch>指定 codemod 集合; - 插件项目请使用
codemods命令组而非latest/major/to; - 运行环境:该包声明
node >=20.0.0 <=26.x.x、npm >=6.0.0(见 package.json),在 monorepo 内联调则从示例应用目录执行../../packages/utils/upgrade/bin/upgrade。
综合来看,packages/utils/upgrade这套工具把"版本解析(semver 模块)— 依赖改写(json transform + lodash 封装)— 代码重构(jscodeshift)— 流程约束(requirement 校验 + git 保护)"组织成了一条可 dry-run、可交互确认、可精确回放的升级流水线。理解 README 中的命令与 transform 编写规范,再对照 src/modules 下的codemod-repository、json、runner等模块源码,既能安全完成自身的版本升级,也能按{X.X.X}/{描述}.{code|json}.ts的约定为 Strapi 社区贡献新的 codemod。
【免费下载链接】strapi🚀 Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.项目地址: https://gitcode.com/GitHub_Trending/st/strapi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考