feat(vitest): recognize __mocks__ specifiers as virtual#265
Merged
BartWaardenburg merged 1 commit intofallow-rs:mainfrom May 3, 2026
Merged
feat(vitest): recognize __mocks__ specifiers as virtual#265BartWaardenburg merged 1 commit intofallow-rs:mainfrom
BartWaardenburg merged 1 commit intofallow-rs:mainfrom
Conversation
Vitest's manual-mock convention places mock factories at
`<package>/__mocks__/<module>.ts` and triggers them via
`vi.mock('<module>')`. Some test setups also import directly from
`@<scope>/__mocks__` paths via package.json `imports` aliases or
workspace virtual paths:
import { mockS3Send } from '@aws-sdk/__mocks__';
Fallow flags these as `unlisted-dep`, and the auto-fix suggests
'install this package' — but `@aws-sdk/__mocks__`,
`@sentry/__mocks__`, `@supabase/__mocks__` etc. do not exist on
npm.
Adds a sibling abstraction to the existing `virtual_module_prefixes`
(prefix-based matching used by Nuxt etc.):
- New `Plugin` trait method `virtual_package_suffixes() -> &[&'static str]`
with default `&[]`, propagated through `AggregatedPluginResult`.
- `VitestPlugin` returns `&["/__mocks__"]`.
- `find_unlisted_dependencies` skips any extracted `package_name`
ending with a registered suffix.
- `run_plugins` merges `virtual_package_suffixes` from per-workspace
plugin runs into the root `AggregatedPluginResult`, mirroring the
existing merge for `virtual_module_prefixes` and
`generated_import_patterns`. Without this, when vitest is only in a
workspace's `package.json` (not the root), the suffix gets registered
locally and dropped before the analyzer reads it.
Tests:
- 5 unit tests in `vitest.rs` and `unused_deps_tests` covering
scoped (`@aws-sdk/__mocks__`), unscoped (`some-pkg/__mocks__`),
and non-mocks specifiers.
- Registry test asserting vitest contributes the suffix.
- 2 integration tests with fixtures: `vitest-mocks-virtual`
(single-package, vitest at root) and `vitest-mocks-workspace`
(monorepo, vitest only in a workspace's package.json) — the second
fixture reproduces the workspace-aggregation case the unit tests
alone cannot catch.
BartWaardenburg
added a commit
that referenced
this pull request
May 3, 2026
Three follow-ups surfaced during pre-ship review of #263 and #265: 1. #263 added five tests for the Next.js dynamic-import re-export fix. Four are regression-strength but `dynamic_import_named_without_matching_export_still_flagged` is coverage-adjacent: the wrapper exports `Bar` and the duplicate detection looks for `Foo` between two unrelated modules, so the wrapper's `matches_export` check is never on the path that decides the test outcome. Removing the Named branch of `matches_export` does not make this test fail. Add a true regression test (`dynamic_import_named_mismatched_with_wrapper_export_still_flagged`) where the wrapper exports `Foo` AND dynamically imports a different name `Bar` from source, so without the matches_export Named branch the wrapper would be registered as a re-export edge and the (source, wrapper) `Foo` duplicate would be silently suppressed. Empirically verified: removing the Named branch makes this test fail and leaves the other four passing. 2. #265 added `Plugin::virtual_package_suffixes()` but only via an explicit `impl Plugin for VitestPlugin` block. The three `define_plugin!` macro variants in `plugins/mod.rs` still listed `virtual_module_prefixes` without a peer entry for the new field, so a future plugin author using the macro would have no path to declare suffixes. Extend all three variants to accept `virtual_package_suffixes:` and add a synthetic smoke test to guard future macro regressions. 3. `.claude/rules/plugins.md` `## Plugin trait extensions` section listed `path_aliases` and `virtual_module_prefixes` but not the new `virtual_package_suffixes`. Add it with a one-line description and a Vitest example.
Collaborator
|
Released in v2.63.0. Thanks @fmguerreiro. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Vitest's manual-mock convention places mock factories at
<package>/__mocks__/<module>.tsand triggers them viavi.mock('<module>'). Some test setups also import directly from@<scope>/__mocks__paths viapackage.json#importsaliases or workspace virtual paths:```ts
import { mockS3Send } from '@aws-sdk/mocks';
```
Fallow flags these as
unlisted-dep, and the auto-fix suggests "install@aws-sdk/__mocks__" — a package that doesn't exist on npm. Same for@sentry/__mocks__,@supabase/__mocks__,@mapbox/__mocks__, etc.Fix
Adds a sibling abstraction to the existing
virtual_module_prefixes(prefix-based matching used by Nuxt, Docusaurus, etc.):virtual_module_prefixes()virtual_package_suffixes()&[]&[]"#""/__mocks__"VitestPlugin::virtual_package_suffixesreturns&["/__mocks__"]. Thefind_unlisted_dependenciesfilter skips anypackage_nameending with a registered suffix.Workspace aggregation
Critical detail: the suffix list must be merged from per-workspace plugin runs into the root
AggregatedPluginResult. When Vitest is in a workspace'spackage.json(not the root) — common in monorepos — the suffix gets registered locally and would be dropped before the analyzer reads it.run_pluginsinlib.rsnow mergesvirtual_package_suffixesalongsidevirtual_module_prefixesandgenerated_import_patternsvia a smallextend_uniquehelper that consolidates the three identical dedup loops.Tests
VitestPlugin::virtual_package_suffixes: scoped/unscoped match plus 6-case adversarial negative (__mocks__-helper,my__mocks__pkg,@scope/__mocks__-utilsetc.).find_unlisted_dependencies:@aws-sdk/__mocks__andsome-pkg/__mocks__fully suppressed.tests/fixtures/vitest-mocks-virtual/: single-package, Vitest at root.tests/fixtures/vitest-mocks-workspace/: monorepo, Vitest only inapps/mrv/package.json. Reproduces the workspace-aggregation case the unit tests alone cannot catch.API surface
Plugin::virtual_package_suffixes(&self) -> &'static [&'static str]with default&[]. Backwards-compatible: existing plugins inherit the default. NewAggregatedPluginResult.virtual_package_suffixes: Vec<String>field flows through the existing aggregation path.Verified on
cargo test --workspace(1909 lib + 299 integration tests passing),cargo clippy --workspace --all-targets -- -D warnings,cargo fmt --all -- --check. Real-world: this fork-pinned binary running in a Turborepo monorepo CI cleared all 5@*/__mocks__false positives.