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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions