自定义工具包导出Vitest时遇内部状态访问错误的排查与解决
报错信息
Error: Vitest failed to access its internal state.
可能原因:
- 未执行"vitest"命令直接导入"vitest"
- 在"globalSetup"中导入"vitest"(可改用"setupFiles")
- 可能是Vitest Bug,请提交至指定仓库
场景与需求
正在开发一个基于Rollup打包的工具包,需要导出eslint、playwright、vitest、@testing-library/react等工具。将该包安装到消费项目执行npm run test时触发上述错误。
检索资料后发现问题可能和入口文件重导出含vitest的模块有关,即使创建非index入口仍报错;如果不导出vitest仅作为dependency引入,消费项目直接导入原vitest包可正常工作,但这不符合通过自定义包统一导出的需求。
核心需求:让消费项目通过自定义包导入使用vitest和@testing-library/react的所有方法,同时避免上述报错。
包代码结构
src eslint: index.ts playwright: index.ts vitest: index.ts (尝试非index名称仍报错) import * as vitest from 'vitest' import * as rtl from '@testing-library/react' export * from './vitestDefaultConfig'; export {vitest, rtl} // 也曾尝试export * as vitest from 'vitest',无效 vitestDefaultConfig.ts: import {defineConfig} from 'vitest/config'; import react from '@vitejs/plugin-react'; export const vitestDefaultConfig = () => { return defineConfig({...}); }
关键配置文件
rollup.config.mjs
import typescript from '@rollup/plugin-typescript'; import { getBabelOutputPlugin } from '@rollup/plugin-babel'; import terser from '@rollup/plugin-terser'; import generatePackageJson from 'rollup-plugin-generate-package-json'; import dotenv from 'dotenv'; import glob from 'glob'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { readFileSync } from 'node:fs'; import copy from 'rollup-plugin-copy'; const ts = JSON.parse(readFileSync('tsconfig.json', { encoding: 'utf8' })); const devMode = process.env.ENVIRONMENT === 'development'; const __dirname = path.resolve(); dotenv.config({ path: path.resolve(__dirname, './.env') }); const common = { input: 'src/index.ts', watch: { include: './src/**', clearScreen: false, }, plugins: [ typescript(), getBabelOutputPlugin({ presets: ['@babel/preset-env'], allowAllFormats: true, }), terser({ compress: { module: true, pure_funcs: ['console.log', 'console.info'], drop_debugger: true, }, }), generatePackageJson({ baseContents: (pkg) => ({ ...pkg, }), }), copy({ targets: [ { src: 'README.md', dest: `${ts.compilerOptions.outDir}/` }, { src: 'scripts', dest: `${ts.compilerOptions.outDir}/` }, ], copyOnce: true, }), ], }; const esmBundlePathLevel = { ...common, input: Object.fromEntries( glob .sync('src/**/*.{ts,tsx}') .filter((file) => !file.includes('typings/')) .map((file) => [ path.relative('src', file.slice(0, file.length - path.extname(file).length)), fileURLToPath(new URL(file, import.meta.url)), ]) ), output: { dir: ts.compilerOptions.outDir, format: 'es', sourcemap: devMode, }, }; const esmBundle = { ...common, output: { file: `${ts.compilerOptions.outDir}/index.mjs`, format: 'es', sourcemap: devMode, }, }; const cjsBundle = { ...common, output: { file: `${ts.compilerOptions.outDir}/index.js`, format: 'cjs', sourcemap: devMode, }, }; export default [esmBundlePathLevel, cjsBundle, esmBundle];
tsconfig.json
{ "compilerOptions": { "paths": { "@eslint/*": ["./src/eslint/*"], "@playwright/*": ["./src/playwright/*"] }, "moduleResolution": "node", "target": "ES2017", "module": "ES2015", "strict": true, "sourceMap": true, "declaration": true, "allowSyntheticDefaultImports": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "esModuleInterop": true, "outDir": "dist", "typeRoots": ["node_modules/@types"], "jsx": "react-jsx" }, "include": ["src", "test"] }
package.json(摘要)
{ "name": "web-config-utility", "version": "0.0.1", "description": "", "main": "dist/index.js", "types": "dist/index.d.ts", "module": "dist/index.mjs", "scripts": { "prepare": "rm -rf .husky/_ && husky install", "postinstall": "node ./scripts/postinstall.js", "prebuild": "rimraf dist", "build": "npx rollup -c", "tsc": "tsc" }, "dependencies": { "@testing-library/react": "^16.0.1", "@vitejs/plugin-react": "4.3.3", "vitest": "2.1.5" }, "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "4.21.2" }, }
原因分析
Vitest的运行依赖其内部维护的状态,这个状态只有在通过vitest命令启动测试时才会初始化。当在自定义包中直接重导出vitest模块时,消费项目在导入包时会提前加载vitest核心代码,此时Vitest还未被正确初始化,就会触发"无法访问内部状态"的错误。
另外,Rollup的打包行为会将vitest的代码打包到工具包中,破坏了Vitest原本的模块加载逻辑——Vitest期望自己作为独立模块被加载,而非被嵌入到其他包的代码里。
解决方法
1. 将Vitest设为peerDependency而非dependency
修改package.json,把vitest和@testing-library/react移到peerDependencies中:
{ "peerDependencies": { "@testing-library/react": "^16.0.1", "vitest": "2.1.5" }, "dependencies": { "@vitejs/plugin-react": "4.3.3" } }
这样消费项目会自行安装这些依赖,工具包只是导出它们的引用,不会提前加载Vitest核心代码。
2. 修改Rollup配置,将Vitest和相关依赖标记为外部依赖
在Rollup的common配置中添加external选项,阻止Rollup将这些依赖打包到工具包中:
external: ['vitest', '@testing-library/react', 'vitest/config'],
工具包导出的只是对这些模块的引用,消费项目会直接加载自己安装的Vitest版本,避免初始化冲突。
3. 调整导出方式,使用代理导出(可选)
修改src/vitest/index.ts,改为动态导入的代理形式(需注意类型定义问题):
// 导出Vitest的代理,让消费项目实际使用时才导入vitest export const vitest = new Proxy({}, { get(_, prop) { return import('vitest').then(mod => mod[prop]); } }); export const rtl = new Proxy({}, { get(_, prop) { return import('@testing-library/react').then(mod => mod[prop]); } }); export * from './vitestDefaultConfig';
4. 单独拆分Vitest相关导出为独立入口
将Vitest相关的导出单独放在一个子入口,让消费项目从your-package/vitest导入,而非根入口,避免项目初始化时就加载Vitest代码。
验证步骤
- 修改上述配置后重新构建工具包
- 在消费项目中重新安装工具包
- 执行
npm run test,确认错误消失 - 验证消费项目可以通过工具包正常导入并使用Vitest和@testing-library/react的方法
内容的提问来源于stack exchange,提问作者Prajwal Kulkarni

