Skip to content

Installing react-doctor splits Vitest into duplicate module instances in pnpm monorepos (vite-plus/test) #699

@hckhanh

Description

@hckhanh

Summary

Adding react-doctor as a workspace devDependency in a pnpm monorepo can cause Vitest to fail at test collection with:

Error: Vitest failed to find the current suite.
...
❯ afterEach tests/*.ts:18

This is not a test-logic bug. The same test files pass when react-doctor is not installed.

Environment

  • react-doctor: 0.3.0
  • Package manager: pnpm 11.2.2 (workspace)
  • Toolchain: Vite+ (vite-plus@0.1.22)
  • Vitest: aliased to @voidzero-dev/vite-plus-test@0.1.22 via workspace catalog
  • Test runner: @cloudflare/vitest-pool-workers@0.16.11
  • Monorepo layout: root + apps/engine, apps/website, packages/*
  • Tests import: import { afterEach, ... } from 'vite-plus/test' (re-exports vitest)

Root cause (pnpm dependency graph)

react-doctor@0.3.0 depends on oxlint (not just oxlint-plugin-react-doctor).

That introduces a second physical install of the same Vitest fork (@voidzero-dev/vite-plus-test@0.1.22) with a different peer-dependency fingerprint:

@voidzero-dev/vite-plus-test@0.1.22 peer#61d8  → engine/website vitest graph
@voidzero-dev/vite-plus-test@0.1.22 peer#77ec  → root packages via react-doctor → oxlint → vite-plus

pnpm why @voidzero-dev/vite-plus-test reports: Found 1 version, 2 instances.

At runtime:

  • Vitest CLI / runner resolves one instance (e.g. via apps/engine/node_modules/vitest)
  • Test files import hooks from vite-plus/test, which re-exports from a different instance (via a vite-plus install tied to the oxlint/react-doctor subgraph)

afterEach() then registers on instance A while the active suite lives on instance B → collection fails before any test runs.

Reproduction

  1. pnpm workspace using Vite+ catalog:

    catalogs:
      viteplus:
        vitest: npm:@voidzero-dev/vite-plus-test@0.1.22
        vite: npm:@voidzero-dev/vite-plus-core@0.1.22
        vite-plus: 0.1.22
    overrides:
      vitest: catalog:viteplus
  2. apps/engine (or similar) with:

    • vitest: catalog:viteplus
    • @cloudflare/vitest-pool-workers
    • test files using import { afterEach } from 'vite-plus/test'
  3. Add to root package.json:

    "devDependencies": {
      "react-doctor": "0.3.0"
    }
  4. pnpm install

  5. Run tests from workspace root:

    vp run engine#test
    # or: vp run -r test
  6. Observe all suites fail at collection with Vitest failed to find the current suite on top-level afterEach.

  7. Remove react-doctor from root deps → pnpm install → tests pass again.

Why pnpm.overrides alone does not fix it

We already had:

overrides:
  vitest: catalog:viteplus

Overrides force the package identity everywhere, but pnpm still creates multiple physical installs when peer contexts differ. Vitest’s hook registry is per module instance, so duplicates still break.

Workarounds we found (consumer-side)

Approach Result
Remove react-doctor from workspace deps; use vp dlx react-doctor@0.3.0 Avoids graph split, but loses import type { ReactDoctorConfig } from 'react-doctor/api' without a separate types story
peerDependencyRules.allowedVersions.vitest: npm:@voidzero-dev/vite-plus-test@0.1.22 Sometimes collapses to 1 instance (fragile; still 2 instances observed after reinstall with react-doctor)
dedupePeerDependents: true Helps but not sufficient alone

Suggested directions for react-doctor

  1. Document that installing react-doctor pulls oxlint and may duplicate Vitest/Vite+ installs in strict pnpm monorepos using catalog aliases.
  2. Consider oxlint as optional peer or not bundling oxlint when consumers already use Vite+/oxlint via workspace tooling.
  3. Consider publishing react-doctor/api types separately (or documenting a types-only import path) so consumers can run the CLI via dlx without installing the full package graph.
  4. Add a monorepo / pnpm troubleshooting note for Vitest + vite-plus/test import pattern.

Happy to provide a minimal reproduction repo if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    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