Strapi 共享 Vitest 配置包 vitest-config:unitPreset 原理与 Jest 到 Vitest 的渐进式迁移实践
Strapi 官方仓库正在用 Vitest 逐步接管后端单元测试,而 packages/utils/vitest-config 就是支撑这一迁移的私有共享配置包。本篇以该包的 README 及其预设源码为骨架,完整讲解 unitPreset 每一项参数的含义、各包接入该预设的标准姿势,以及 Strapi 如何通过 .vitest.test.ts 命名约定让 Jest 与 Vitest 两套测试框架在同一 monorepo 中互不干扰地并存运行。
一、包定位:一个明确不对外发布的内部工具包
vitest-config 的定义非常克制。从其 package.json 可以确认以下事实:
- 包名为
vitest-config,"private": true,版本5.52.2(与 monorepo 其余包保持同步的版本策略); - 对外仅暴露一个入口:
./presets/unit,同时映射类型与实现到./src/presets/unit.ts; peerDependencies要求使用方必须自行安装vitest ^4.0.0,配置包本身不锁定具体运行时版本(devDependencies 中通过"vitest": "catalog:"引用仓库统一的 Yarn catalog 版本);engines约束为 Node.js>=20.0.0 <=26.x.x、npm>=6.0.0,与仓库根 package.json 的全局 engines 约束一致。
README 开头用 IMPORTANT 提示框明确声明:这是一个私有包,不打算被 Strapi monorepo 之外的用户使用。因此下文所有用法都应以“在 Strapi 仓库内接入新测试”为适用前提,外部项目参考其思路时应自行落地等价配置。
二、unitPreset 源码逐项解析
预设的完整实现只有 32 行,位于 src/presets/unit.ts,通过 defineConfig 导出为 unitPreset:
import { defineConfig } from 'vitest/config';
export const unitPreset = defineConfig({
test: {
// Require explicit imports from 'vitest' (no globals)
globals: false,
// Node environment for backend unit tests
environment: 'node',
// Only run tests with .vitest.test.ts suffix for incremental migration
include: ['**/*.vitest.test.ts'],
// Exclude common non-test files and directories
exclude: [
'**/node_modules/**',
'**/dist/**',
'**/.cache/**',
'**/*.testdata.{js,ts}',
'**/*.test.utils.{js,ts}',
'**/*.d.ts',
'**/__tests__/resources/**',
'**/tests/resources/**',
],
// Match Jest's default timeout
testTimeout: 5000,
// Watch mode settings
watch: false,
},
});
逐项说明其设计意图:
| 参数 | 取值 | 作用与设计考量 |
|---|---|---|
globals |
false |
禁用 Vitest 全局注入,强制测试文件显式 import { describe, it, expect } from 'vitest'。好处是依赖关系在文件层面可见、易被静态检查发现 |
environment |
'node' |
该预设服务于后端单元测试(server 侧代码),运行在 Node 环境而非 jsdom |
include |
['**/*.vitest.test.ts'] |
这是渐进式迁移的关键:只收集带 .vitest.test.ts 后缀的测试文件,源码注释也写明是 "for incremental migration"。普通 *.test.ts 文件仍归 Jest 管辖,两套框架按文件后缀自动分流 |
exclude |
8 条 glob | 排除 node_modules、dist、.cache、类型声明文件 *.d.ts,以及两类“看起来像测试但并非测试”的文件:*.testdata.{js,ts}(测试数据)与 *.test.utils.{js,ts}(测试工具函数);同时排除 __tests__/resources/ 与 tests/resources/ 资源目录 |
testTimeout |
5000 |
源码注释明确写着 "Match Jest's default timeout"——刻意对齐 Jest 的默认 5 秒超时,避免迁移到 Vitest 后原有测试因超时行为变化而失败 |
watch |
false |
预设层面默认关闭 watch,保证 CI 等场景下的确定行为;需要监听模式时由上层脚本显式开启(见第四节) |
值得注意的是,exclude 列表中的 *.testdata.{js,ts}、*.test.utils.{js,ts}、__tests__/resources 等规则与 Jest 侧的忽略规则(见下文 jest-preset.unit.js)几乎一一对应,可以推断这套预设是在把既有 Jest 的过滤口径平移到 Vitest 上,从而保证同一批“伪测试文件”在两套框架下都不被误收集。
该包的 tsconfig.json 继承自 packages/utils/tsconfig/base.json,仅声明 "types": ["node"] 并包含 src 目录,说明预设本身是纯 TypeScript 源码导出,不做独立构建。
三、标准接入姿势:mergeConfig 合并预设
README 给出的 Usage 示例(也是全仓库 16 个包实际采用的统一模板):
import { defineConfig, mergeConfig } from 'vitest/config';
import { unitPreset } from 'vitest-config/presets/unit';
export default mergeConfig(
unitPreset,
defineConfig({
test: {
root: __dirname,
},
})
);
要点拆解:
- 导入路径是子路径导出
vitest-config/presets/unit,对应package.json的exports字段;包名没有@strapi/前缀,是因为它属于内部工具包(AGENTS.md 将packages/utils/描述为 "Shared tooling: logger, eslint-config, tsconfig, vitest-config")。 mergeConfig(unitPreset, defineConfig(...))的顺序:共享预设在前,包级覆盖在后。后一个配置中出现的同名字段会覆盖预设值。- 包级仅需补充
root: __dirname:把测试根目录锚定到各包自身目录,使每个包的vitest.config.ts成为相互独立的测试工程,互不串扰。
仓库中已经落地该模板的配置文件均与 README 示例逐字一致,例如 packages/core/utils/vitest.config.ts、packages/core/database/vitest.config.ts、packages/providers/upload-local/vitest.config.ts、packages/plugins/sentry/vitest.config.ts 等,全文如下(以 database 包为例):
import { defineConfig, mergeConfig } from 'vitest/config';
import { unitPreset } from 'vitest-config/presets/unit';
export default mergeConfig(
unitPreset,
defineConfig({
test: {
root: __dirname,
},
})
);
当前仓库中接入 vitest-config/presets/unit 的包共 16 个,覆盖核心层、Provider 层与插件层:
- 核心包:packages/core/core/vitest.config.ts、packages/core/database/vitest.config.ts、packages/core/utils/vitest.config.ts、packages/core/permissions/vitest.config.ts、packages/core/data-transfer/vitest.config.ts、packages/core/content-type-builder/vitest.config.ts
- CLI 包:packages/cli/cloud/vitest.config.ts
- Provider 包:packages/providers/email-amazon-ses/vitest.config.ts、packages/providers/email-mailgun/vitest.config.ts、packages/providers/email-nodemailer/vitest.config.ts、packages/providers/email-sendmail/vitest.config.ts、packages/providers/upload-aws-s3/vitest.config.ts、packages/providers/upload-local/vitest.config.ts
- 插件包:packages/plugins/color-picker/vitest.config.ts、packages/plugins/sentry/vitest.config.ts
每个接入包的 package.json 还会声明对 vitest-config 的精确版本依赖(如 "vitest-config": "5.52.2")与 "vitest": "catalog:",并在其 tsconfig.json 的 include 中加入 vitest.config.ts(例如 packages/core/core/tsconfig.json),保证配置文件本身也纳入类型检查。
四、运行入口:根工程级 projects 模式与包级脚本
monorepo 根目录的 vitest.config.ts 只有 7 行:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
projects: ['packages/**/vitest*.config.*'],
},
});
这里使用 Vitest 的 projects(多工程)模式,通过 glob 一次性发现 packages/ 下所有 vitest*.config.* 文件,把 16 个包级配置聚合成一次运行。对应根 package.json 的两条脚本:
"test:unit:vitest": "vitest run",
"test:unit:vitest:watch": "vitest --watch"
- 一次性执行(CI 友好):
yarn test:unit:vitest,即vitest run,此时预设中watch: false的默认值与一次性运行语义一致; - 本地开发监听:
yarn test:unit:vitest:watch,即vitest --watch,用命令行标志显式覆盖预设里的watch: false,这解释了预设为什么要显式写死watch: false——它是run与--watch两种入口的公共基线。
包级别同样提供对称脚本(见 packages/cli/cloud/package.json):test:unit:vitest 与 test:unit:vitest:watch,允许单独对某一个包的 Vitest 用例做开发迭代。
五、渐进式迁移机制:后缀约定让 Jest 与 Vitest 并存
unitPreset 中 include: ['**/*.vitest.test.ts'] 并非孤立的命名偏好,它与 Jest 侧配置构成一对镜像约束,共同实现“按文件逐个迁移、两套框架零冲突”:
- Vitest 侧:
include只认.vitest.test.ts后缀,Jest 存量测试文件(*.test.ts、__tests__/*等)对 Vitest 完全不可见; - Jest 侧:仓库根的 jest-preset.unit.js 在
testPathIgnorePatterns中显式加入了'.vitest.test.ts',源码注释写着 "Prevent Jest from running Vitest test files",防止 Jest 误跑新框架的测试;同时modulePathIgnorePatterns、testMatch等规则维持原有 Jest 口径; - 命名约定:迁移一个测试文件时,只需把文件名从
xxx.test.ts重命名为xxx.vitest.test.ts,并将describe/it/expect改为从vitest显式导入(因为预设强制globals: false),该文件便自动从 Jest 流水线切换到 Vitest 流水线,其余文件不受影响。
从源码结构看,这套约定目前已在 43 个测试文件中落地,分布于 packages/core/database/src/fields/tests/string.vitest.test.ts、packages/core/content-type-builder/server/src/services/schema-builder/tests/content-type-builder.vitest.test.ts、packages/cli/cloud/src/create-project/utils/tests/get-project-name-from-pkg.vitest.test.ts 等位置,均保持“源码旁 __tests__ 目录”的既有组织方式,与 Jest 测试同目录共存。
这种“后缀即路由”的方案对大型 monorepo 的测试框架换代尤其实用:迁移粒度细化到单个文件,任何一步都可以随时停下,而不是一次性改写全仓库。
六、适用前提与使用边界小结
- 版本前提:
vitest ^4.0.0(peer 依赖);Node.js>=20.0.0 <=26.x.x;仓库统一通过 Yarn 4(packageManager: yarn@4.12.0)的 workspace 与 catalog 机制管理依赖。 - 范围边界:该包为私有内部包,仅暴露
./presets/unit一个子路径;README 明确声明不建议在 Strapi monorepo 之外使用。 - 预设语义边界:
unitPreset面向后端 Node 单元测试;仓库中前端单元测试仍走 Jest front 预设体系,二者不要混用。 - 可复制要点:即便不能直接安装该包,仓库外的项目也可以照搬其三个核心手法——共享预设 +
mergeConfig包级覆盖、testTimeout对齐旧框架默认值、以文件后缀约定实现测试框架的渐进式共存。
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 StartedRust0624
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