Describe the bug
When running vitest related <file> in a workspace with multiple projects (e.g., a jsdom project and a browser/Playwright project), the getTestDependencies function SSR-transforms Vue SFC files for dependency analysis across ALL projects. This SSR transform poisons @vitejs/plugin-vue's module-level descriptorCache singleton, causing subsequent browser/client transforms to produce incorrect output.
Specifically, for <script setup> components, template-only component imports are omitted from the setup function's __returned__ object, making them undefined in the render function's $setup proxy. This causes [Vue warn]: Invalid vnode type when creating vnode: undefined and components fail to render.
Reproduction
Minimal reproduction: https://github.com/seanogdev/vue-sfc-cache-poisoning-repro
pnpm install
pnpm test # runs: vitest related src/Target.vue --run
The bug only triggers via vitest related (which calls getTestDependencies across all projects). Running vitest run directly passes because it skips the multi-project dependency resolution that causes the cache poisoning.
Environment:
- Vitest workspace with 2 projects:
- Project A: jsdom environment (app unit tests)
- Project B: browser/Playwright environment (Storybook component tests via
@storybook/addon-vitest)
- Both projects share a common Vite config with
@vitejs/plugin-vue
Vue component structure:
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue'; // Used ONLY in template
const someRef = ref(0); // Used in script
</script>
<template>
<ChildComponent />
<span>{{ someRef }}</span>
</template>
Steps:
vitest related Parent.vue → FAILS - ChildComponent is undefined in render
vitest --project my-storybook-project Parent.vue → PASSES
vitest Parent.vue (name filter, both projects) → PASSES
Root Cause Analysis
The issue is in the interaction between vitest related's dependency analysis and @vitejs/plugin-vue's caching:
-
getTestDependencies (in packages/vitest/src/node/specifications.ts) calls project.vite.environments.ssr.transformRequest(filepath) for each project to build module dependency graphs.
-
@vitejs/plugin-vue's descriptorCache is a module-level singleton Map<string, SFCDescriptor> keyed by filename only — no environment/SSR discrimination. When the SSR transform runs for the jsdom project, it creates/mutates a descriptor that's shared with the browser project.
-
When @vue/compiler-sfc's compileScript runs for SSR, it processes the <script setup> block differently. Template-only component imports may be excluded from __returned__ because SSR rendering handles component resolution differently.
-
The subsequent client/browser transform reuses the poisoned descriptor from the shared cache, producing a setup function where __returned__ omits template-only imports:
Failing (vitest related):
const __returned__ = { props, modelValue, someRef }
// ChildComponent is MISSING
Passing (single project):
const __returned__ = { props, modelValue, someRef, ChildComponent }
-
The compiled render function accesses $setup["ChildComponent"] which returns undefined, triggering the "Invalid vnode type" warning.
Evidence
Confirmed by instrumenting the component's render function at runtime:
// Failing run ($setup keys via for..in):
["localVar1", "localVar2", "props", "modelValue", ...]
// ChildComponent and other template-only imports ABSENT
// Passing run ($setup keys):
["localVar1", "localVar2", "props", "modelValue", ..., "ChildComponent"]
// All imports present
The raw import at the module level resolves correctly in both cases (typeof ChildComponent === 'object'), but the $setup proxy used by the compiled render function does not include it.
Expected behavior
vitest related should produce identical test results to running the same tests via vitest --project <name> or vitest <name-filter>.
Possible fixes
- Vitest:
getTestDependencies could avoid using environments.ssr.transformRequest for browser-mode projects, or use an isolated transform pipeline for dependency analysis
@vitejs/plugin-vue: The descriptorCache should be keyed by filename + environment to prevent cross-environment pollution
- Vite Environment API: Plugin state isolation between environments should be enforced
System Info
Vitest: 4.0.18
@vitejs/plugin-vue: 6.0.4
Vue: 3.5.30
Vite: 7.3.1
OS: macOS (Darwin 25.3.0)
Used Package Manager
pnpm
Validations
Describe the bug
When running
vitest related <file>in a workspace with multiple projects (e.g., a jsdom project and a browser/Playwright project), thegetTestDependenciesfunction SSR-transforms Vue SFC files for dependency analysis across ALL projects. This SSR transform poisons@vitejs/plugin-vue's module-leveldescriptorCachesingleton, causing subsequent browser/client transforms to produce incorrect output.Specifically, for
<script setup>components, template-only component imports are omitted from the setup function's__returned__object, making themundefinedin the render function's$setupproxy. This causes[Vue warn]: Invalid vnode type when creating vnode: undefinedand components fail to render.Reproduction
Minimal reproduction: https://github.com/seanogdev/vue-sfc-cache-poisoning-repro
The bug only triggers via
vitest related(which callsgetTestDependenciesacross all projects). Runningvitest rundirectly passes because it skips the multi-project dependency resolution that causes the cache poisoning.Environment:
@storybook/addon-vitest)@vitejs/plugin-vueVue component structure:
Steps:
vitest related Parent.vue→ FAILS -ChildComponentis undefined in rendervitest --project my-storybook-project Parent.vue→ PASSESvitest Parent.vue(name filter, both projects) → PASSESRoot Cause Analysis
The issue is in the interaction between
vitest related's dependency analysis and@vitejs/plugin-vue's caching:getTestDependencies(inpackages/vitest/src/node/specifications.ts) callsproject.vite.environments.ssr.transformRequest(filepath)for each project to build module dependency graphs.@vitejs/plugin-vue'sdescriptorCacheis a module-level singletonMap<string, SFCDescriptor>keyed by filename only — no environment/SSR discrimination. When the SSR transform runs for the jsdom project, it creates/mutates a descriptor that's shared with the browser project.When
@vue/compiler-sfc'scompileScriptruns for SSR, it processes the<script setup>block differently. Template-only component imports may be excluded from__returned__because SSR rendering handles component resolution differently.The subsequent client/browser transform reuses the poisoned descriptor from the shared cache, producing a setup function where
__returned__omits template-only imports:Failing (vitest related):
Passing (single project):
The compiled render function accesses
$setup["ChildComponent"]which returnsundefined, triggering the "Invalid vnode type" warning.Evidence
Confirmed by instrumenting the component's render function at runtime:
The raw import at the module level resolves correctly in both cases (
typeof ChildComponent === 'object'), but the$setupproxy used by the compiled render function does not include it.Expected behavior
vitest relatedshould produce identical test results to running the same tests viavitest --project <name>orvitest <name-filter>.Possible fixes
getTestDependenciescould avoid usingenvironments.ssr.transformRequestfor browser-mode projects, or use an isolated transform pipeline for dependency analysis@vitejs/plugin-vue: ThedescriptorCacheshould be keyed byfilename + environmentto prevent cross-environment pollutionSystem Info
Used Package Manager
pnpm
Validations