@@ -16,6 +16,7 @@ const {
1616 SafeMap,
1717 StringPrototypeSlice,
1818 StringPrototypeStartsWith,
19+ encodeURIComponent,
1920} = primordials ;
2021const {
2122 codes : {
@@ -192,6 +193,7 @@ class MockModuleContext {
192193 namedExports,
193194 sharedState,
194195 specifier,
196+ virtual,
195197 } ) {
196198 const config = {
197199 __proto__ : null ,
@@ -200,19 +202,26 @@ class MockModuleContext {
200202 hasDefaultExport,
201203 namedExports,
202204 caller,
205+ virtual,
203206 } ;
204207
205208 sharedState . mockMap . set ( baseURL , config ) ;
206- sharedState . mockMap . set ( fullPath , config ) ;
209+ if ( fullPath ) {
210+ sharedState . mockMap . set ( fullPath , config ) ;
211+ } else if ( virtual ) {
212+ // Virtual mock - store under raw specifier for CJS resolution fallback.
213+ sharedState . mockMap . set ( specifier , config ) ;
214+ }
207215
208216 this . #sharedState = sharedState ;
209217 this . #restore = {
210218 __proto__ : null ,
211219 baseURL,
212- cached : fullPath in Module . _cache ,
220+ cached : fullPath ? fullPath in Module . _cache : false ,
213221 format,
214222 fullPath,
215- value : Module . _cache [ fullPath ] ,
223+ specifier : virtual ? specifier : undefined ,
224+ value : fullPath ? Module . _cache [ fullPath ] : undefined ,
216225 } ;
217226
218227 const mock = mocks . get ( baseURL ) ;
@@ -226,7 +235,7 @@ class MockModuleContext {
226235 const localVersion = mock ?. localVersion ?? 0 ;
227236
228237 debug ( 'new mock version %d for "%s"' , localVersion , baseURL ) ;
229- mocks . set ( baseURL , {
238+ const mockEntry = {
230239 __proto__ : null ,
231240 url : baseURL ,
232241 cache,
@@ -235,10 +244,17 @@ class MockModuleContext {
235244 format,
236245 localVersion,
237246 active : true ,
238- } ) ;
247+ virtual,
248+ } ;
249+ mocks . set ( baseURL , mockEntry ) ;
250+ if ( virtual ) {
251+ mocks . set ( specifier , mockEntry ) ;
252+ }
239253 }
240254
241- delete Module . _cache [ fullPath ] ;
255+ if ( fullPath ) {
256+ delete Module . _cache [ fullPath ] ;
257+ }
242258 sharedState . mockExports . set ( baseURL , {
243259 __proto__ : null ,
244260 defaultExport,
@@ -251,12 +267,19 @@ class MockModuleContext {
251267 return ;
252268 }
253269
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 ] ;
270+ if ( this . #restore. fullPath ) {
271+ // Delete the mock CJS cache entry. If the module was previously in the
272+ // cache then restore the old value.
273+ delete Module . _cache [ this . #restore. fullPath ] ;
274+
275+ if ( this . #restore. cached ) {
276+ Module . _cache [ this . #restore. fullPath ] = this . #restore. value ;
277+ }
257278
258- if ( this . #restore. cached ) {
259- Module . _cache [ this . #restore. fullPath ] = this . #restore. value ;
279+ this . #sharedState. mockMap . delete ( this . #restore. fullPath ) ;
280+ } else if ( this . #restore. specifier !== undefined ) {
281+ // Virtual mock - clean up specifier key.
282+ this . #sharedState. mockMap . delete ( this . #restore. specifier ) ;
260283 }
261284
262285 const mock = mocks . get ( this . #restore. baseURL ) ;
@@ -267,7 +290,9 @@ class MockModuleContext {
267290 }
268291
269292 this . #sharedState. mockMap . delete ( this . #restore. baseURL ) ;
270- this . #sharedState. mockMap . delete ( this . #restore. fullPath ) ;
293+ if ( this . #restore. specifier !== undefined ) {
294+ mocks . delete ( this . #restore. specifier ) ;
295+ }
271296 this . #restore = undefined ;
272297 }
273298}
@@ -630,10 +655,12 @@ class MockTracker {
630655 cache = false ,
631656 namedExports = kEmptyObject ,
632657 defaultExport,
658+ virtual = false ,
633659 } = options ;
634660 const hasDefaultExport = 'defaultExport' in options ;
635661
636662 validateBoolean ( cache , 'options.cache' ) ;
663+ validateBoolean ( virtual , 'options.virtual' ) ;
637664 validateObject ( namedExports , 'options.namedExports' ) ;
638665
639666 const sharedState = setupSharedModuleState ( ) ;
@@ -646,6 +673,33 @@ class MockTracker {
646673 // If the caller is already a file URL, use it as is. Otherwise, convert it.
647674 const hasFileProtocol = StringPrototypeStartsWith ( filename , 'file://' ) ;
648675 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+ } ) ;
694+
695+ ArrayPrototypePush ( this . #mocks, {
696+ __proto__ : null ,
697+ ctx,
698+ restore : restoreModule ,
699+ } ) ;
700+ return ctx ;
701+ }
702+
649703 const request = { __proto__ : null , specifier : mockSpecifier , attributes : kEmptyObject } ;
650704 const { format, url } = sharedState . moduleLoader . resolveSync ( caller , request ) ;
651705 debug ( 'module mock, url = "%s", format = "%s", caller = "%s"' , url , format , caller ) ;
@@ -680,6 +734,7 @@ class MockTracker {
680734 namedExports,
681735 sharedState,
682736 specifier : mockSpecifier ,
737+ virtual,
683738 } ) ;
684739
685740 ArrayPrototypePush ( this . #mocks, {
@@ -841,7 +896,10 @@ function setupSharedModuleState() {
841896function cjsMockModuleLoad ( request , parent , isMain ) {
842897 let resolved ;
843898
844- if ( isBuiltin ( request ) ) {
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 ) ) {
845903 resolved = ensureNodeScheme ( request ) ;
846904 } else {
847905 resolved = _resolveFilename ( request , parent , isMain ) ;
0 commit comments