Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9cc0b8e
fix: fix `vi.importActual()` for virtual modules (#9771)
hi-ogawa Mar 3, 2026
71fbf91
refactor: move injectQuery/removeQuery to moduleRunner/utils.ts
hi-ogawa Mar 3, 2026
d4c4de5
refactor: consolidate virtual module mock tests in test/cli
hi-ogawa Mar 3, 2026
919b4c2
chore: lint
hi-ogawa Mar 3, 2026
1aedd3b
fix: add _vitest_original back to fetchModule result id/url
hi-ogawa Mar 3, 2026
b621937
revert: remove _vitest_original inject-back (not provably needed)
hi-ogawa Mar 3, 2026
fc50d52
chore: comment
hi-ogawa Mar 3, 2026
1e21b99
fix: fix importActual for virtual modules in browser mode
hi-ogawa Mar 3, 2026
158c1e6
chore: comment
hi-ogawa Mar 3, 2026
caf3289
fix: don't touch wdio
hi-ogawa Mar 3, 2026
1099aec
test: wdio
hi-ogawa Mar 3, 2026
42539a0
test: fix wdio
hi-ogawa Mar 3, 2026
4a8fb50
Merge branch 'main' into fix/import-actual-virtual-modules
hi-ogawa Mar 3, 2026
bba414c
test: update snapshot
hi-ogawa Mar 3, 2026
864f2c5
chore: cleanup
hi-ogawa Mar 3, 2026
21ab1ac
fix: manual mock shouldn't load/transform original module
hi-ogawa Mar 3, 2026
bc5a94f
fix: auto detect mock self-import and treat as importActual
hi-ogawa Mar 4, 2026
c4e8935
fix: fix `doMock -> doUnmock -> import`
hi-ogawa Mar 4, 2026
0efc6b2
chore: comment
hi-ogawa Mar 4, 2026
1509b55
test: redirect mock doesn't transform original
hi-ogawa Mar 4, 2026
d7b0216
Merge branch 'main' into fix-9622-manual-mock-no-fetch
hi-ogawa Mar 4, 2026
835d14b
test: more
hi-ogawa Mar 4, 2026
b31de20
test: more
hi-ogawa Mar 4, 2026
50d59c3
chore: cleanup
hi-ogawa Mar 4, 2026
0dc1876
chore: remove debug comments and stale commented-out code
hi-ogawa Mar 4, 2026
b9bdb25
refactor: extract replaceRoot helper in mocking tests
hi-ogawa Mar 4, 2026
9a9523e
merge: resolve conflicts after #9772 squash-merge into main
hi-ogawa Mar 5, 2026
90e0779
chore: cleanup
hi-ogawa Mar 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/browser/src/node/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ export default (parentServer: ParentBrowserProject, base = '/'): Plugin[] => {
}
next()
})
// strip _vitest_original query added by importActual so that
// the plugin pipeline sees the original import id (e.g. virtual modules's load hook).
server.middlewares.use((req, _res, next) => {
if (
req.url?.includes('_vitest_original')
&& parentServer.project.config.browser.provider?.name === 'playwright'
) {
req.url = req.url
.replace(/[?&]_vitest_original(?=[&#]|$)/, '')
.replace(/[?&]ext\b[^&#]*/, '')
.replace(/\?$/, '')
}
next()
})
server.middlewares.use(createOrchestratorMiddleware(parentServer))
server.middlewares.use(createTesterMiddleware(parentServer))

Expand Down
5 changes: 5 additions & 0 deletions packages/vitest/src/runtime/moduleRunner/bareModuleMocker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ export class BareModuleMocker implements TestModuleMocker {
return registry.getById(fixLeadingSlashes(id))
}

public getDependencyMockByUrl(url: string): MockedModule | undefined {
const registry = this.getMockerRegistry()
return registry.get(url)
}

public findMockRedirect(mockPath: string, external: string | null): string | null {
return findMockRedirect(this.root, mockPath, external)
}
Expand Down
3 changes: 2 additions & 1 deletion packages/vitest/src/runtime/moduleRunner/moduleMocker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import vm from 'node:vm'
import { AutomockedModule, RedirectedModule } from '@vitest/mocker'
import { distDir } from '../../paths'
import { BareModuleMocker } from './bareModuleMocker'
import { injectQuery } from './utils'

const spyModulePath = resolve(distDir, 'spy.js')

Expand Down Expand Up @@ -130,7 +131,7 @@ export class VitestMocker extends BareModuleMocker {
callstack?: string[] | null,
): Promise<T> {
const { url } = await this.resolveId(rawId, importer)
const node = await this.moduleRunner.fetchModule(url, importer)
const node = await this.moduleRunner.fetchModule(injectQuery(url, '_vitest_original'), importer)
const result = await this.moduleRunner.cachedRequest(
node.url,
node,
Expand Down
16 changes: 15 additions & 1 deletion packages/vitest/src/runtime/moduleRunner/moduleRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as viteModuleRunner from 'vite/module-runner'
import { Traces } from '../../utils/traces'
import { VitestMocker } from './moduleMocker'
import { VitestTransport } from './moduleTransport'
import { injectQuery } from './utils'

export type CreateImportMeta = (modulePath: string) => viteModuleRunner.ModuleRunnerImportMeta | Promise<viteModuleRunner.ModuleRunnerImportMeta>
export const createNodeImportMeta: CreateImportMeta = (modulePath: string) => {
Expand Down Expand Up @@ -165,11 +166,24 @@ export class VitestModuleRunner

let mocked: any
if (mod.meta && 'mockedModule' in mod.meta) {
const mockedModule = mod.meta.mockedModule as MockedModule
const mockId = this.mocker.getMockPath(mod.id)
// bypass mock and force "importActual" behavior when:
// - mock was removed by doUnmock (stale mockedModule in meta)
// - self-import: mock factory/file is importing the module it's mocking
const isStale = !this.mocker.getDependencyMock(mod.id)
const isSelfImport = callstack.includes(mockId)
|| callstack.includes(url)
|| ('redirect' in mockedModule && callstack.includes(mockedModule.redirect))
if (isStale || isSelfImport) {
const node = await this.fetchModule(injectQuery(url, '_vitest_original'))
return this._cachedRequest(node.url, node, callstack, metadata)
}
Comment on lines +169 to +181

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is added to support existing use cases where "import original module" fallback was happening magically. As the previous PR #9772 consolidated "import original module" to be only possible through _vitest_original query, this adjustment became necessary.

mocked = await this.mocker.requestWithMockedModule(
url,
mod,
callstack,
mod.meta.mockedModule as MockedModule,
mockedModule,
)
}
else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { getCachedVitestImport } from './cachedResolver'
import { unwrapId, VitestModuleEvaluator } from './moduleEvaluator'
import { VitestMocker } from './moduleMocker'
import { VitestModuleRunner } from './moduleRunner'
import { removeQuery } from './utils'

const { readFileSync } = fs

Expand Down Expand Up @@ -95,6 +96,13 @@ export function startVitestModuleRunner(options: ContextModuleRunnerOptions): Vi
return vitest
}

// strip _vitest_original query added by importActual so that
// the plugin pipeline sees the original import id (e.g. virtual modules's load hook)
const isImportActual = id.includes('_vitest_original')
if (isImportActual) {
id = removeQuery(id, '_vitest_original')
}

const rawId = unwrapId(id)
resolvingModules.add(rawId)

Expand All @@ -103,15 +111,17 @@ export function startVitestModuleRunner(options: ContextModuleRunnerOptions): Vi
await moduleRunner.mocker.resolveMocks()
}

const resolvedMock = moduleRunner.mocker.getDependencyMock(rawId)
if (resolvedMock?.type === 'manual' || resolvedMock?.type === 'redirect') {
return {
code: '',
file: null,
id: resolvedMock.id,
url: resolvedMock.url,
invalidate: false,
mockedModule: resolvedMock,
if (!isImportActual) {
const resolvedMock = moduleRunner.mocker.getDependencyMockByUrl(id)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getDependencyMock(rawId) doesn't work for actual file since getDependencyMock expects full module graph id but rawid/id here is a normalized path from project root. Also here id over rawId is preferred since id is an "url" passed to fetchModule, e.g.

  • rawId = \0virtual:module vs id = /@id/__x00__virtual:module
  • rawId = /src/foo.js vs id = /src/foo.js

if (resolvedMock?.type === 'manual' || resolvedMock?.type === 'redirect') {
return {
code: '',
file: null,
id: resolvedMock.id,
url: resolvedMock.url,
invalidate: false,
mockedModule: resolvedMock,
}
}
}

Expand Down
21 changes: 21 additions & 0 deletions packages/vitest/src/runtime/moduleRunner/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// copied from vite/src/shared/utils.ts
const postfixRE = /[?#].*$/

function cleanUrl(url: string): string {
return url.replace(postfixRE, '')
}
function splitFileAndPostfix(path: string): { file: string; postfix: string } {
const file = cleanUrl(path)
return { file, postfix: path.slice(file.length) }
}

export function injectQuery(url: string, queryToInject: string): string {
const { file, postfix } = splitFileAndPostfix(url)
return `${file}?${queryToInject}${postfix[0] === '?' ? `&${postfix.slice(1)}` : /* hash only */ postfix}`
}

export function removeQuery(url: string, queryToRemove: string): string {
return url
.replace(new RegExp(`[?&]${queryToRemove}(?=[&#]|$)`), '')
.replace(/\?$/, '')
}
1 change: 0 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading