From 81db25b5b798d4a595a4bb01a31f993218ec2ed4 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 11 Nov 2026 02:47:56 -0500 Subject: [PATCH 1/6] perf(cli): give the Vitest resolver a resolveId hook filter --- .../define-config-vitest-resolver.spec.ts | 64 +++++++++++++++-- packages/cli/src/define-config.ts | 72 ++++++++++--------- 2 files changed, 96 insertions(+), 40 deletions(-) diff --git a/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts b/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts index b48553e1b4..9118f60c08 100644 --- a/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts +++ b/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts @@ -1,7 +1,14 @@ import { fileURLToPath } from 'node:url'; import { preview } from '@vitest/browser-preview'; -import { resolveConfig, type Alias, type ConfigEnv, type Plugin, type UserConfig } from 'vite'; +import { + build, + resolveConfig, + type Alias, + type ConfigEnv, + type Plugin, + type UserConfig, +} from 'vite'; import { describe, expect, it } from 'vitest'; import { defineConfig, defineProject, isVitestFamilySpecifier } from '../define-config.ts'; @@ -86,6 +93,49 @@ describe('isVitestFamilySpecifier', () => { }); describe('vitePlusVitestResolverPlugin', () => { + it.each([ + ['react', 'resolved-by-project'], + ['vitest', 'resolved-by-vitest'], + ['vitest/internal/browser', 'resolved-by-vitest'], + ['@vitest/mocker', 'resolved-by-vitest'], + ['vite-plus/test', 'resolved-by-vitest'], + ])('dispatches %s through the native hook filter', async (specifier, expected) => { + const plugin = findPlugin(defineConfig({}).plugins, RESOLVER_PLUGIN_NAME) as Plugin; + const resolver = { + ...plugin, + resolveId: { + ...plugin.resolveId, + // The emitted import identifies which resolver actually ran, including + // when Rolldown's report counts native filter rejects as no-op calls. + handler() { + return { id: 'resolved-by-vitest', external: true }; + }, + }, + }; + const result = await build({ + configFile: false, + logLevel: 'silent', + plugins: [ + resolver, + { + name: 'fixture', + resolveId(id) { + return id === 'virtual:entry' + ? '\0entry' + : { id: 'resolved-by-project', external: true }; + }, + load(id) { + if (id === '\0entry') { + return `export { default } from ${JSON.stringify(specifier)};`; + } + }, + }, + ], + build: { write: false, rolldownOptions: { input: 'virtual:entry' } }, + }); + expect(result).toMatchObject({ output: [{ imports: [expected] }] }); + }); + it.each(['serve', 'build'] as const)( 'leaves application dependencies unchanged during %s with browser tests configured', async (command) => { @@ -213,7 +263,7 @@ describe('vitePlusVitestResolverPlugin', () => { const plugin = findPlugin(plugins, RESOLVER_PLUGIN_NAME); expect(plugin).toBeDefined(); expect(plugin?.enforce).toBe('pre'); - expect(typeof plugin?.resolveId).toBe('function'); + expect(plugin?.resolveId).toMatchObject({ handler: expect.any(Function) }); } }); @@ -225,7 +275,7 @@ describe('vitePlusVitestResolverPlugin', () => { expect(plugin).toBeDefined(); expect(plugin?.enforce).toBe('pre'); - expect(typeof plugin?.resolveId).toBe('function'); + expect(plugin?.resolveId).toMatchObject({ handler: expect.any(Function) }); }); }); @@ -255,8 +305,10 @@ interface ResolveCall { function getResolveId(): ResolveId { const result = defineConfig({}) as { plugins: unknown[] }; const plugin = findPlugin(result.plugins, RESOLVER_PLUGIN_NAME); - expect(plugin).toBeDefined(); - return plugin?.resolveId as ResolveId; + if (!plugin) { + throw new Error(`${RESOLVER_PLUGIN_NAME} is not injected`); + } + return (plugin.resolveId as { handler: ResolveId }).handler; } // The resolver re-resolves through `this.resolve` rooted at vite-plus's own @@ -282,7 +334,7 @@ function resolver(command: 'serve' | 'build', vitestServer: boolean): ResolveId const plugin = findPlugin(defineConfig({}).plugins, RESOLVER_PLUGIN_NAME)!; const hook = (plugin.config as { handler: (config: UserConfig, env: ConfigEnv) => void }).handler; hook(vitestServer ? { environments: { __vitest__: {} } } : {}, { command, mode: 'test' }); - return plugin.resolveId as ResolveId; + return (plugin.resolveId as { handler: ResolveId }).handler; } describe('workspace test API identity', () => { diff --git a/packages/cli/src/define-config.ts b/packages/cli/src/define-config.ts index 8ecf080232..ac7ee702b5 100644 --- a/packages/cli/src/define-config.ts +++ b/packages/cli/src/define-config.ts @@ -360,41 +360,45 @@ function vitePlusVitestResolverPlugin(): PluginOption { }); plugins.splice(0, plugins.length, ...unique); }, - async resolveId(id, importer, options) { - if (isVitestServer && id === 'vite-plus/test') { - // pnpm can install separate vite-plus/Vitest peer instances for a - // workspace's root and children. Externalizing the child wrapper lets - // Node load its uninitialized runner state, bypassing Vite's hooks. - // Resolve the API before externalization through Vitest's own resolver: - // it selects the active runner (or the browser's optimized instance), - // not the Vitest copy next to this config's vite-plus installation. - return this.resolve('vitest', importer, { ...options, skipSelf: true }); - } - if (!isVitestFamilySpecifier(id)) { - return null; - } - // The redirected imports are all clean bare specifiers; a queried id is - // outside the scope of this resolver — let the default resolver handle it. - if (id.includes('?')) { - return null; - } - // Re-resolve from vite-plus's own (pinned) tree via Vite's resolver so the - // runner and every imported Vitest module are a single physical instance. - // `skipSelf` avoids infinite recursion back into this plugin. - const vitestAnchorPath = getVitestAnchor(); - const bundledAnchors = vitestAnchorPath - ? [vitePlusModuleFile, vitestAnchorPath] - : [vitePlusModuleFile]; - for (const anchor of bundledAnchors) { - const resolved = await this.resolve(id, anchor, { ...options, skipSelf: true }); - if (resolved) { - return resolved; + resolveId: { + // Keep the allowlist in the handler; skip unrelated ids before entering JS. + filter: { id: /^(?:vitest(?:\/|$)|@vitest\/|vite-plus\/test$)/ }, + async handler(id, importer, options) { + if (isVitestServer && id === 'vite-plus/test') { + // pnpm can install separate vite-plus/Vitest peer instances for a + // workspace's root and children. Externalizing the child wrapper lets + // Node load its uninitialized runner state, bypassing Vite's hooks. + // Resolve the API before externalization through Vitest's own resolver: + // it selects the active runner (or the browser's optimized instance), + // not the Vitest copy next to this config's vite-plus installation. + return this.resolve('vitest', importer, { ...options, skipSelf: true }); } - } - // Last resort: default project-rooted resolution for any family id the - // bundled tree cannot resolve (e.g. coverage providers not shipped with - // vite-plus). - return this.resolve(id, importer, { ...options, skipSelf: true }); + if (!isVitestFamilySpecifier(id)) { + return null; + } + // The redirected imports are all clean bare specifiers; a queried id is + // outside the scope of this resolver — let the default resolver handle it. + if (id.includes('?')) { + return null; + } + // Re-resolve from vite-plus's own (pinned) tree via Vite's resolver so the + // runner and every imported Vitest module are a single physical instance. + // `skipSelf` avoids infinite recursion back into this plugin. + const vitestAnchorPath = getVitestAnchor(); + const bundledAnchors = vitestAnchorPath + ? [vitePlusModuleFile, vitestAnchorPath] + : [vitePlusModuleFile]; + for (const anchor of bundledAnchors) { + const resolved = await this.resolve(id, anchor, { ...options, skipSelf: true }); + if (resolved) { + return resolved; + } + } + // Last resort: default project-rooted resolution for any family id the + // bundled tree cannot resolve (e.g. coverage providers not shipped with + // vite-plus). + return this.resolve(id, importer, { ...options, skipSelf: true }); + }, }, }; } From d0a3e93c5e9f4b3dda3b66b4845257a0d0eaf436 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 11 Nov 2026 05:32:40 -0500 Subject: [PATCH 2/6] test(cli): pin the resolver filter to its package allowlist --- .../define-config-vitest-resolver.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts b/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts index 9118f60c08..01308786c4 100644 --- a/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts +++ b/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts @@ -12,6 +12,7 @@ import { import { describe, expect, it } from 'vitest'; import { defineConfig, defineProject, isVitestFamilySpecifier } from '../define-config.ts'; +import { VITEST_RESOLVER_PACKAGES } from '../utils/vitest-ecosystem.ts'; const RESOLVER_PLUGIN_NAME = 'vite-plus:vitest-resolver'; @@ -93,6 +94,17 @@ describe('isVitestFamilySpecifier', () => { }); describe('vitePlusVitestResolverPlugin', () => { + it('lets every resolver package through the hook filter', () => { + const plugin = findPlugin(defineConfig({}).plugins, RESOLVER_PLUGIN_NAME) as Plugin; + const { filter } = plugin.resolveId as { filter: { id: RegExp } }; + for (const name of [...VITEST_RESOLVER_PACKAGES, 'vite-plus/test']) { + expect(name).toMatch(filter.id); + } + for (const name of VITEST_RESOLVER_PACKAGES) { + expect(`${name}/subpath`).toMatch(filter.id); + } + }); + it.each([ ['react', 'resolved-by-project'], ['vitest', 'resolved-by-vitest'], @@ -119,6 +131,8 @@ describe('vitePlusVitestResolverPlugin', () => { resolver, { name: 'fixture', + // Ahead of Vite's own resolver, so an installed package can't answer first. + enforce: 'pre', resolveId(id) { return id === 'virtual:entry' ? '\0entry' From fc8661f525a0153564b77dcd43aaae7160825aaa Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 11 Nov 2026 05:41:13 -0500 Subject: [PATCH 3/6] docs(cli): say where the resolver filter applies --- .../cli/src/__tests__/define-config-vitest-resolver.spec.ts | 3 +-- packages/cli/src/define-config.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts b/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts index 01308786c4..63b5341ac1 100644 --- a/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts +++ b/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts @@ -117,8 +117,7 @@ describe('vitePlusVitestResolverPlugin', () => { ...plugin, resolveId: { ...plugin.resolveId, - // The emitted import identifies which resolver actually ran, including - // when Rolldown's report counts native filter rejects as no-op calls. + // The stub's external id shows which resolver answered. handler() { return { id: 'resolved-by-vitest', external: true }; }, diff --git a/packages/cli/src/define-config.ts b/packages/cli/src/define-config.ts index ac7ee702b5..ae22427c4f 100644 --- a/packages/cli/src/define-config.ts +++ b/packages/cli/src/define-config.ts @@ -361,7 +361,7 @@ function vitePlusVitestResolverPlugin(): PluginOption { plugins.splice(0, plugins.length, ...unique); }, resolveId: { - // Keep the allowlist in the handler; skip unrelated ids before entering JS. + // Keep the allowlist in the handler; skip unrelated ids before the handler runs. filter: { id: /^(?:vitest(?:\/|$)|@vitest\/|vite-plus\/test$)/ }, async handler(id, importer, options) { if (isVitestServer && id === 'vite-plus/test') { From 821c83a84a55ef67581528e6a7cbb5c6e715fe2a Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 11 Nov 2026 23:20:54 -0500 Subject: [PATCH 4/6] test(cli): type the plugin finder so the resolver tests type-check --- .../src/__tests__/define-config-vitest-resolver.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts b/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts index 63b5341ac1..0363b0f550 100644 --- a/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts +++ b/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts @@ -16,12 +16,12 @@ import { VITEST_RESOLVER_PACKAGES } from '../utils/vitest-ecosystem.ts'; const RESOLVER_PLUGIN_NAME = 'vite-plus:vitest-resolver'; -function findPlugin(plugins: unknown, name: string): Record | undefined { +function findPlugin(plugins: unknown, name: string): Plugin | undefined { if (!Array.isArray(plugins)) { return undefined; } return plugins.find( - (p): p is Record => + (p): p is Plugin => !!p && typeof p === 'object' && (p as { name?: unknown }).name === name, ); } @@ -95,7 +95,7 @@ describe('isVitestFamilySpecifier', () => { describe('vitePlusVitestResolverPlugin', () => { it('lets every resolver package through the hook filter', () => { - const plugin = findPlugin(defineConfig({}).plugins, RESOLVER_PLUGIN_NAME) as Plugin; + const plugin = findPlugin(defineConfig({}).plugins, RESOLVER_PLUGIN_NAME)!; const { filter } = plugin.resolveId as { filter: { id: RegExp } }; for (const name of [...VITEST_RESOLVER_PACKAGES, 'vite-plus/test']) { expect(name).toMatch(filter.id); @@ -112,7 +112,7 @@ describe('vitePlusVitestResolverPlugin', () => { ['@vitest/mocker', 'resolved-by-vitest'], ['vite-plus/test', 'resolved-by-vitest'], ])('dispatches %s through the native hook filter', async (specifier, expected) => { - const plugin = findPlugin(defineConfig({}).plugins, RESOLVER_PLUGIN_NAME) as Plugin; + const plugin = findPlugin(defineConfig({}).plugins, RESOLVER_PLUGIN_NAME)!; const resolver = { ...plugin, resolveId: { From 0f01e3ee4befb98957d84da6ca9b35a90a609ea6 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 11 Nov 2026 23:21:06 -0500 Subject: [PATCH 5/6] style(cli): format the plugin finder --- .../cli/src/__tests__/define-config-vitest-resolver.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts b/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts index 0363b0f550..706ab7bb34 100644 --- a/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts +++ b/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts @@ -21,8 +21,7 @@ function findPlugin(plugins: unknown, name: string): Plugin | undefined { return undefined; } return plugins.find( - (p): p is Plugin => - !!p && typeof p === 'object' && (p as { name?: unknown }).name === name, + (p): p is Plugin => !!p && typeof p === 'object' && (p as { name?: unknown }).name === name, ); } From cfa7e22dd2f26263d08f159b19274687462755a5 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Thu, 12 Nov 2026 01:21:54 -0500 Subject: [PATCH 6/6] test(cli): narrow the resolver hook and return null from the stub load --- .../src/__tests__/define-config-vitest-resolver.spec.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts b/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts index 706ab7bb34..a8bbb99f25 100644 --- a/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts +++ b/packages/cli/src/__tests__/define-config-vitest-resolver.spec.ts @@ -112,10 +112,14 @@ describe('vitePlusVitestResolverPlugin', () => { ['vite-plus/test', 'resolved-by-vitest'], ])('dispatches %s through the native hook filter', async (specifier, expected) => { const plugin = findPlugin(defineConfig({}).plugins, RESOLVER_PLUGIN_NAME)!; + const hook = plugin.resolveId; + if (typeof hook !== 'object') { + throw new Error('The resolver plugin declares resolveId as an object hook.'); + } const resolver = { ...plugin, resolveId: { - ...plugin.resolveId, + ...hook, // The stub's external id shows which resolver answered. handler() { return { id: 'resolved-by-vitest', external: true }; @@ -140,6 +144,7 @@ describe('vitePlusVitestResolverPlugin', () => { if (id === '\0entry') { return `export { default } from ${JSON.stringify(specifier)};`; } + return null; }, }, ],