Skip to content

常见错误 ​

无法找到模块 './relative-path' ​

如果你收到模块无法找到的错误,这可能意味着几种不同的情况:

  1. 你拼错了路径。确保路径是正确的。

  2. 你可能依赖了 tsconfig.json 中的 baseUrl。Vite 默认不考虑 tsconfig.json,所以如果你依赖这种行为,可能需要自己安装 vite-tsconfig-paths。

ts
import { defineConfig } from 'vitest/config'
import tsconfigPaths from 'vite-tsconfig-paths'

export default defineConfig({
  plugins: [tsconfigPaths()]
})

或者重写你的路径,使其不相对于根目录:

diff
- import helpers from 'src/helpers'
+ import helpers from '../src/helpers'
  1. 确保你没有相对 别名。Vite 将它们视为相对于导入所在的文件,而不是根目录。
ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    alias: {
      '@/': './src/', 
      '@/': new URL('./src/', import.meta.url).pathname, 
    }
  }
})

无法终止 Worker ​

当 NodeJS 的 fetch 与 pool: 'threads' 一起使用时,可能会发生此错误。详见 #3077。

默认的 pool: 'forks' 没有这个问题。如果你显式设置了 pool: 'threads',切换回 'forks' 或使用 'vmForks' 将解决它。

项目工作目录不会改变 ​

在多项目运行中,项目配置文件和测试里的 process.cwd() 默认返回启动 Vitest 时所在的目录。项目的 root 控制 Vitest 查找文件的位置,但不会更改进程的工作目录。Vite 插件可以从解析后的 Vite 配置的 root 属性读取项目根目录。

如果测试需要让 process.cwd() 指向项目目录,请使用 forks 池和项目专属的 setupFiles 文件:

packages/lib1/vitest.config.ts
ts
import { defineProject } from 'vitest/config'

export default defineProject({
  test: {
    pool: 'forks',
    setupFiles: ['./setup.chdir.ts'],
  },
})
packages/lib1/setup.chdir.ts
ts
import { fileURLToPath } from 'node:url'

process.chdir(fileURLToPath(new URL('.', import.meta.url)))

这会在配置加载完成后更改测试 worker 的工作目录。threads 池无法使用 process.chdir()。

自定义 package 条件未解析 ​

如果你在 package.json 的 exports 或 subpath imports 中使用了自定义条件,你可能会发现 Vitest 默认不尊重这些条件。

例如,如果你的 package.json 中有以下内容:

json
{
  "exports": {
    ".": {
      "custom": "./lib/custom.js",
      "import": "./lib/index.js"
    }
  },
  "imports": {
    "#internal": {
      "custom": "./src/internal.js",
      "default": "./lib/internal.js"
    }
  }
}

默认情况下,Vitest 只会使用 import 和 default 条件。要使 Vitest 尊重自定义条件,你需要在 Vitest 配置中配置 ssr.resolve.conditions:

vitest.config.js
ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  ssr: {
    resolve: {
      conditions: ['custom', 'import', 'default'],
    },
  },
})

为什么使用 ssr.resolve.conditions 而不是 resolve.conditions?

Vitest 遵循 Vite 的配置约定:

  • resolve.conditions 适用于 Vite 的 client 环境,对应 Vitest 的浏览器模式、jsdom、happy-dom,或带有 viteEnvironment: 'client' 的自定义环境。
  • ssr.resolve.conditions 适用于 Vite 的 ssr 环境,对应 Vitest 的 node 环境,或带有 viteEnvironment: 'ssr' 的自定义环境。

由于 Vitest 默认为 node 环境(使用 viteEnvironment: 'ssr'),模块解析使用 ssr.resolve.conditions。这适用于 package exports 和 subpath imports。

你可以在 environment 中了解更多关于 Vite 环境和 Vitest 环境的信息。

段错误和原生代码错误 ​

在 pool: 'threads' 中运行 原生 NodeJS 模块 可能会遇到来自原生代码的神秘错误。

  • Segmentation fault (core dumped)
  • thread '<unnamed>' panicked at 'assertion failed
  • Abort trap: 6
  • internal error: entered unreachable code

在这些情况下,原生模块可能不是为多线程安全构建的。作为变通方法,你可以切换到 pool: 'forks',它在多个 node:child_process 中运行测试用例,而不是多个 node:worker_threads。

ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    pool: 'forks',
  },
})
bash
vitest --pool=forks

Worker 线程中的时区不会改变 ​

在 setup 文件或测试中设置 process.env.TZ,或通过 env 设置 TZ,都不会影响 pool: 'threads' 和 pool: 'vmThreads' 中的 Date。Node.js 只有在主线程设置 TZ 时才会应用它。worker 线程虽然能在 process.env 中看到新值,但仍会沿用主进程的时区。

ts
process.env.TZ = 'Asia/Tokyo'
new Date('2026-01-01T00:00:00Z').getHours() // 9 in forks, unchanged in threads

请在 worker 启动前设置时区。可以通过 shell、配置文件或 globalSetup 设置;它们都在主进程中运行,因此适用于所有池。

bash
TZ=Asia/Tokyo vitest
ts
import { defineConfig } from 'vitest/config'

process.env.TZ = 'Asia/Tokyo'

export default defineConfig({})
ts
export default function () {
  process.env.TZ = 'Asia/Tokyo'
}

如果测试需要在运行时使用不同的时区,请使用 pool: 'forks' 或 pool: 'vmForks',这样每个 worker 都是独立进程;也可以为 Intl.DateTimeFormat 传入 timeZone 选项,而不是更改 TZ。

未处理的 Promise 拒绝 ​

当 Promise 被拒绝但在微任务队列刷新之前没有附加 .catch() 处理程序或 await 时,会发生此错误。这种行为来自 JavaScript 本身,并非 Vitest 特有。请在 Node.js 文档 中了解更多。

一个常见的原因是在没有 await 的情况下调用异步函数:

ts
async function fetchUser(id) {
  const res = await fetch(`/api/users/${id}`)
  if (!res.ok) {
    throw new Error(`用户 ${id} 未找到`) 
  }
  return res.json()
}

test('fetches user', async () => {
  fetchUser(123) 
})

因为 fetchUser() 没有被 await,它的拒绝没有处理程序,Vitest 报告:

Unhandled Rejection: Error: 用户 123 未找到

修复 ​

await 该 promise 以便 Vitest 可以捕获错误:

ts
test('fetches user', async () => {
  await fetchUser(123) 
})

如果你期望调用抛出错误,使用 expect().rejects:

ts
test('rejects for missing user', async () => {
  await expect(fetchUser(123)).rejects.toThrow('User 123 not found')
})

包在 Vitest 中加载失败,但在你的应用中可以正常工作 ​

有些包在应用构建中可以正常工作,但在 Vitest 中会失败,因为它们只有在打包器重写或解析之后才是有效的。当 Vitest 将依赖外部化时,Node.js 会直接加载它,因此会应用 Node 的 ESM 和 package 规则。有关精确规则,请参阅 Node.js 文档中关于 ECMAScript 模块 和 packages 的说明。

常见示例包括以下包:

  • 在 .js 文件中提供 ESM 语法,但没有 "type": "module"
  • 在 ESM 文件中使用不带扩展名的相对导入
  • exports、imports、main 或 module 条目不正确
  • 以只有在打包后才可工作的方式混用 CommonJS 和 ESM 入口点
  • 导入 CSS 或其他非 JavaScript 文件,而这些文件本应由打包器处理

你可能会看到如下错误:

  • Cannot find module './relative-path' imported from ...
  • Unexpected token 'export'
  • Cannot use import statement outside a module
  • Module ... seems to be an ES Module but shipped in a CommonJS package.
  • Unknown file extension ".css"

在可能的情况下,修复该包,使 Node.js 能够直接加载它:为 ESM .js 文件添加 "type": "module",使用 .mjs,在 ESM 导入中包含显式文件扩展名,并确保 exports 指向 Node.js 可以加载的文件。

如果你无法修复包本身,可以将其内联,这样 Vite 就会处理它,而不是将其作为外部依赖传递给 Node.js。将导致无效包的整条依赖链都内联。如果你的源码导入了 wrapper-package,而 wrapper-package 又导入了 broken-package,则两个包都需要内联:

vitest.config.js
ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    server: {
      deps: {
        inline: ['wrapper-package', 'broken-package'],
      },
    },
  },
})

你也可以使用 Vite 的 ssr.resolve.noExternal 达到同样的目的。Vitest 会将 ssr.resolve.noExternal 合并到 server.deps.inline 中,因此当该依赖还需要在 SSR 构建中由 Vite 打包时,这很有用:

vitest.config.js
ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  ssr: {
    resolve: {
      noExternal: ['wrapper-package', 'broken-package'],
    },
  },
})

CommonJS 源码尚未得到完整支持 ​

Vitest 以 ESM 为先。默认情况下,源码会在 Vite 的模块运行器中运行。该运行器为兼容性提供 require、module 和 exports 等 CommonJS 变量,但不会完整复现 Node.js 的 CommonJS 语义。

require() 调用始终直接使用 Node.js,并会离开模块运行器。因此:

  • Vite 插件、别名、转换和模块模拟不会应用于通过 require 加载的文件
  • 不支持通过 require 加载 TypeScript 或其他 Node.js 无法执行的文件
  • 对同一文件同时使用 import 和 require 可能会执行两次,导致单例状态、对象引用或 instanceof 检查出错

如果项目使用 CommonJS 且不需要 Vite 转换,请将 experimental.viteModuleRunner 设为 false,让原生运行时加载整个模块图:

vitest.config.ts
ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    experimental: {
      viteModuleRunner: false,
    },
  },
})

如果应用使用 ESM 源码,但从同一 monorepo 导入 CommonJS 包,则可以改用 server.deps.external 将整个 CommonJS 包外部化。这样它的入口和内部 require() 调用会留在同一个原生模块缓存中。例如:

vitest.config.ts
ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    server: {
      deps: {
        external: [/\/packages\/legacy-cjs\//],
      },
    },
  },
})