@@ -209,7 +209,8 @@ class MockModuleContext {
209209 if ( fullPath ) {
210210 sharedState . mockMap . set ( fullPath , config ) ;
211211 } else if ( virtual ) {
212- // Virtual mock - store under raw specifier for CJS resolution fallback.
212+ // Virtual mock for a non-existent module - store under raw specifier
213+ // so CJS resolution fallback can find it.
213214 sharedState . mockMap . set ( specifier , config ) ;
214215 }
215216
@@ -278,7 +279,7 @@ class MockModuleContext {
278279
279280 this . #sharedState. mockMap . delete ( this . #restore. fullPath ) ;
280281 } else if ( this . #restore. specifier !== undefined ) {
281- // Virtual mock - clean up specifier key.
282+ // Virtual mock for non-existent module - clean up specifier key.
282283 this . #sharedState. mockMap . delete ( this . #restore. specifier ) ;
283284 }
284285
@@ -673,35 +674,27 @@ class MockTracker {
673674 // If the caller is already a file URL, use it as is. Otherwise, convert it.
674675 const hasFileProtocol = StringPrototypeStartsWith ( filename , 'file://' ) ;
675676 const caller = hasFileProtocol ? filename : pathToFileURL ( filename ) . href ;
676- if ( virtual ) {
677- const url = `mock:///${ encodeURIComponent ( mockSpecifier ) } ` ;
678- const format = 'module' ;
679- const baseURL = URLParse ( url ) ;
680- const ctx = new MockModuleContext ( {
681- __proto__ : null ,
682- baseURL : baseURL . href ,
683- cache,
684- caller,
685- defaultExport,
686- format,
687- fullPath : null ,
688- hasDefaultExport,
689- namedExports,
690- sharedState,
691- specifier : mockSpecifier ,
692- virtual,
693- } ) ;
677+ const request = { __proto__ : null , specifier : mockSpecifier , attributes : kEmptyObject } ;
694678
695- ArrayPrototypePush ( this . #mocks, {
696- __proto__ : null ,
697- ctx,
698- restore : restoreModule ,
699- } ) ;
700- return ctx ;
679+ // Try to resolve the specifier. For virtual mocks, if the module exists
680+ // on disk, use its canonical URL so that all resolution paths to that
681+ // module are properly intercepted. Only fall back to a synthetic URL
682+ // if the module truly doesn't exist.
683+ let format , url ;
684+ if ( virtual ) {
685+ try {
686+ ( { url } = sharedState . moduleLoader . resolveSync ( caller , request ) ) ;
687+ } catch {
688+ // Module doesn't exist - use a synthetic URL.
689+ url = `mock:///${ encodeURIComponent ( mockSpecifier ) } ` ;
690+ }
691+ // Virtual mocks always use 'module' format since the generated source
692+ // is ESM regardless of the original module's format.
693+ format = 'module' ;
694+ } else {
695+ ( { format, url } = sharedState . moduleLoader . resolveSync ( caller , request ) ) ;
701696 }
702697
703- const request = { __proto__ : null , specifier : mockSpecifier , attributes : kEmptyObject } ;
704- const { format, url } = sharedState . moduleLoader . resolveSync ( caller , request ) ;
705698 debug ( 'module mock, url = "%s", format = "%s", caller = "%s"' , url , format , caller ) ;
706699 if ( format ) { // Format is not yet known for ambiguous files when detection is enabled.
707700 validateOneOf ( format , 'format' , kSupportedFormats ) ;
@@ -896,13 +889,20 @@ function setupSharedModuleState() {
896889function cjsMockModuleLoad ( request , parent , isMain ) {
897890 let resolved ;
898891
899- // Virtual mock - skip resolution, the module doesn't exist on disk.
900- if ( this . mockMap . get ( request ) ?. virtual ) {
901- resolved = request ;
902- } else if ( isBuiltin ( request ) ) {
892+ if ( isBuiltin ( request ) ) {
903893 resolved = ensureNodeScheme ( request ) ;
904894 } else {
905- resolved = _resolveFilename ( request , parent , isMain ) ;
895+ try {
896+ resolved = _resolveFilename ( request , parent , isMain ) ;
897+ } catch ( resolveError ) {
898+ // Resolution failed - check if there's a virtual mock for this specifier.
899+ const virtualConfig = this . mockMap . get ( request ) ;
900+ if ( virtualConfig ?. virtual ) {
901+ resolved = request ;
902+ } else {
903+ throw resolveError ;
904+ }
905+ }
906906 }
907907
908908 const config = this . mockMap . get ( resolved ) ;
0 commit comments