Skip to content

vitest related with multiple projects corrupts Vue SFC compilation via shared plugin cache #9855

@seanogdev

Description

@seanogdev

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:

  1. vitest related Parent.vueFAILS - ChildComponent is undefined in render
  2. vitest --project my-storybook-project Parent.vuePASSES
  3. 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:

  1. getTestDependencies (in packages/vitest/src/node/specifications.ts) calls project.vite.environments.ssr.transformRequest(filepath) for each project to build module dependency graphs.

  2. @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.

  3. 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.

  4. 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 }
  5. 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

  1. Vitest: getTestDependencies could avoid using environments.ssr.transformRequest for browser-mode projects, or use an isolated transform pipeline for dependency analysis
  2. @vitejs/plugin-vue: The descriptorCache should be keyed by filename + environment to prevent cross-environment pollution
  3. 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

  • Check that you are using the latest version of Vitest
  • Read the docs
  • Check that there isn't already an issue that reports the same bug

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions