You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义工具包导出Vitest时遇内部状态访问错误的排查与解决

问题:自定义工具包导出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代码。

验证步骤

  1. 修改上述配置后重新构建工具包
  2. 在消费项目中重新安装工具包
  3. 执行npm run test,确认错误消失
  4. 验证消费项目可以通过工具包正常导入并使用Vitest和@testing-library/react的方法

内容的提问来源于stack exchange,提问作者Prajwal Kulkarni

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 05:29:53