@@ -192,6 +192,7 @@ class MockModuleContext {
192192 namedExports,
193193 sharedState,
194194 specifier,
195+ virtual,
195196 } ) {
196197 const config = {
197198 __proto__ : null ,
@@ -200,19 +201,26 @@ class MockModuleContext {
200201 hasDefaultExport,
201202 namedExports,
202203 caller,
204+ virtual,
203205 } ;
204206
205207 sharedState . mockMap . set ( baseURL , config ) ;
206- sharedState . mockMap . set ( fullPath , config ) ;
208+ if ( fullPath ) {
209+ sharedState . mockMap . set ( fullPath , config ) ;
210+ } else if ( virtual ) {
211+ // Virtual mock — store under raw specifier for CJS resolution fallback.
212+ sharedState . mockMap . set ( specifier , config ) ;
213+ }
207214
208215 this . #sharedState = sharedState ;
209216 this . #restore = {
210217 __proto__ : null ,
211218 baseURL,
212- cached : fullPath in Module . _cache ,
219+ cached : fullPath ? fullPath in Module . _cache : false ,
213220 format,
214221 fullPath,
215- value : Module . _cache [ fullPath ] ,
222+ specifier : virtual ? specifier : undefined ,
223+ value : fullPath ? Module . _cache [ fullPath ] : undefined ,
216224 } ;
217225
218226 const mock = mocks . get ( baseURL ) ;
@@ -226,7 +234,7 @@ class MockModuleContext {
226234 const localVersion = mock ?. localVersion ?? 0 ;
227235
228236 debug ( 'new mock version %d for "%s"' , localVersion , baseURL ) ;
229- mocks . set ( baseURL , {
237+ const mockEntry = {
230238 __proto__ : null ,
231239 url : baseURL ,
232240 cache,
@@ -235,10 +243,17 @@ class MockModuleContext {
235243 format,
236244 localVersion,
237245 active : true ,
238- } ) ;
246+ virtual,
247+ } ;
248+ mocks . set ( baseURL , mockEntry ) ;
249+ if ( virtual ) {
250+ mocks . set ( specifier , mockEntry ) ;
251+ }
239252 }
240253
241- delete Module . _cache [ fullPath ] ;
254+ if ( fullPath ) {
255+ delete Module . _cache [ fullPath ] ;
256+ }
242257 sharedState . mockExports . set ( baseURL , {
243258 __proto__ : null ,
244259 defaultExport,
@@ -251,12 +266,19 @@ class MockModuleContext {
251266 return ;
252267 }
253268
254- // Delete the mock CJS cache entry. If the module was previously in the
255- // cache then restore the old value.
256- delete Module . _cache [ this . #restore. fullPath ] ;
269+ if ( this . #restore. fullPath ) {
270+ // Delete the mock CJS cache entry. If the module was previously in the
271+ // cache then restore the old value.
272+ delete Module . _cache [ this . #restore. fullPath ] ;
273+
274+ if ( this . #restore. cached ) {
275+ Module . _cache [ this . #restore. fullPath ] = this . #restore. value ;
276+ }
257277
258- if ( this . #restore. cached ) {
259- Module . _cache [ this . #restore. fullPath ] = this . #restore. value ;
278+ this . #sharedState. mockMap . delete ( this . #restore. fullPath ) ;
279+ } else if ( this . #restore. specifier !== undefined ) {
280+ // Virtual mock — clean up specifier key.
281+ this . #sharedState. mockMap . delete ( this . #restore. specifier ) ;
260282 }
261283
262284 const mock = mocks . get ( this . #restore. baseURL ) ;
@@ -267,7 +289,9 @@ class MockModuleContext {
267289 }
268290
269291 this . #sharedState. mockMap . delete ( this . #restore. baseURL ) ;
270- this . #sharedState. mockMap . delete ( this . #restore. fullPath ) ;
292+ if ( this . #restore. specifier !== undefined ) {
293+ mocks . delete ( this . #restore. specifier ) ;
294+ }
271295 this . #restore = undefined ;
272296 }
273297}
@@ -630,10 +654,12 @@ class MockTracker {
630654 cache = false ,
631655 namedExports = kEmptyObject ,
632656 defaultExport,
657+ virtual = false ,
633658 } = options ;
634659 const hasDefaultExport = 'defaultExport' in options ;
635660
636661 validateBoolean ( cache , 'options.cache' ) ;
662+ validateBoolean ( virtual , 'options.virtual' ) ;
637663 validateObject ( namedExports , 'options.namedExports' ) ;
638664
639665 const sharedState = setupSharedModuleState ( ) ;
@@ -646,6 +672,33 @@ class MockTracker {
646672 // If the caller is already a file URL, use it as is. Otherwise, convert it.
647673 const hasFileProtocol = StringPrototypeStartsWith ( filename , 'file://' ) ;
648674 const caller = hasFileProtocol ? filename : pathToFileURL ( filename ) . href ;
675+ if ( virtual ) {
676+ const url = `mock:///${ encodeURIComponent ( mockSpecifier ) } ` ;
677+ const format = 'module' ;
678+ const baseURL = URLParse ( url ) ;
679+ const ctx = new MockModuleContext ( {
680+ __proto__ : null ,
681+ baseURL : baseURL . href ,
682+ cache,
683+ caller,
684+ defaultExport,
685+ format,
686+ fullPath : null ,
687+ hasDefaultExport,
688+ namedExports,
689+ sharedState,
690+ specifier : mockSpecifier ,
691+ virtual,
692+ } ) ;
693+
694+ ArrayPrototypePush ( this . #mocks, {
695+ __proto__ : null ,
696+ ctx,
697+ restore : restoreModule ,
698+ } ) ;
699+ return ctx ;
700+ }
701+
649702 const request = { __proto__ : null , specifier : mockSpecifier , attributes : kEmptyObject } ;
650703 const { format, url } = sharedState . moduleLoader . resolveSync ( caller , request ) ;
651704 debug ( 'module mock, url = "%s", format = "%s", caller = "%s"' , url , format , caller ) ;
@@ -680,6 +733,7 @@ class MockTracker {
680733 namedExports,
681734 sharedState,
682735 specifier : mockSpecifier ,
736+ virtual,
683737 } ) ;
684738
685739 ArrayPrototypePush ( this . #mocks, {
@@ -841,7 +895,10 @@ function setupSharedModuleState() {
841895function cjsMockModuleLoad ( request , parent , isMain ) {
842896 let resolved ;
843897
844- if ( isBuiltin ( request ) ) {
898+ // Virtual mock — skip resolution, the module doesn't exist on disk.
899+ if ( this . mockMap . get ( request ) ?. virtual ) {
900+ resolved = request ;
901+ } else if ( isBuiltin ( request ) ) {
845902 resolved = ensureNodeScheme ( request ) ;
846903 } else {
847904 resolved = _resolveFilename ( request , parent , isMain ) ;
0 commit comments