@@ -96,6 +96,73 @@ const createMockedModule = (originalModule, isSpy) => {
9696 ) ;
9797} ;
9898
99+ // #1454: publish a mock's settled exports to the worker-realm native-mock
100+ // registry so a Node `registerHooks` load hook can serve them to a module loaded
101+ // NATIVELY by Node — a true-external `A` (a node_modules package), or a local
102+ // module reached via a non-literal `import(variable)` (loaded outside the
103+ // bundle) — that internally imports this mocked module. Routed over the existing
104+ // RSTEST_API channel (same as `mockObject`). `request` is `undefined` under an
105+ // older @rspack /core that omits the request literal — skip then.
106+ //
107+ // `produce` is the source of the mock's exports: the already-built mocked module
108+ // for the spy/mock-option path (the SAME instance bundle consumers see, via its
109+ // getters), or the raw factory for a function mock. It is run on a MICROTASK,
110+ // never eagerly at install time: a factory may reference module-level imports
111+ // not yet evaluated when the hoisted `rs.mock` runs (notably under circular
112+ // deps — see mockHoist/barrel), so eager evaluation throws `Cannot read
113+ // properties of undefined`. The microtask runs after the module body settles its
114+ // imports but before the test body's `await import(...)`, so the registry is
115+ // populated in time. Errors are swallowed: a factory that only runs inside the
116+ // bundle context simply won't back a native mock, leaving the webpack path
117+ // untouched.
118+ //
119+ // LIMITATION (function factory only): the factory is re-run here, so the
120+ // native-realm mock is a SEPARATE instance from the bundle's. A spy created
121+ // inside the factory (`rs.fn()`) is therefore a different object on each side, so
122+ // asserting — through the bundle handle — calls made via the natively-loaded
123+ // module will not see them. Sync object-returning factories (the #1454 repro)
124+ // and the spy/mock-option path are unaffected. Sharing the bundle instance was
125+ // tried (`() => __webpack_require__(id)`) but it eagerly evaluates the mock
126+ // module on the microtask, corrupting state for an async-factory error and
127+ // running factory side effects early; deferring that eval is a future change.
128+ const publishNativeMock = ( request , produce ) => {
129+ if ( request === undefined ) {
130+ return ;
131+ }
132+ const api = globalThis . RSTEST_API ?. rstest ;
133+ if ( ! api || typeof api . __setNativeMock !== 'function' ) {
134+ return ;
135+ }
136+ queueMicrotask ( ( ) => {
137+ try {
138+ const res = produce ( ) ;
139+ if ( isPromise ( res ) ) {
140+ res . then ( ( mod ) => api . __setNativeMock ( request , mod ) ) . catch ( ( ) => { } ) ;
141+ } else {
142+ api . __setNativeMock ( request , res ) ;
143+ }
144+ } catch {
145+ // factory is not standalone-runnable — skip native mocking for it
146+ }
147+ } ) ;
148+ } ;
149+
150+ // Deferred on a microtask like `publishNativeMock` so install→uninstall order is
151+ // preserved: a synchronous unpublish would run BEFORE the deferred publish
152+ // microtask, which would then re-add a stale entry. Queuing both keeps the last
153+ // operation winning (`rs.mock` then `rs.unmock` ⇒ removed).
154+ const unpublishNativeMock = ( request ) => {
155+ if ( request === undefined ) {
156+ return ;
157+ }
158+ queueMicrotask ( ( ) => {
159+ const api = globalThis . RSTEST_API ?. rstest ;
160+ if ( api && typeof api . __unsetNativeMock === 'function' ) {
161+ api . __unsetNativeMock ( request ) ;
162+ }
163+ } ) ;
164+ } ;
165+
99166//#region rs.unmock
100167__webpack_require__ . rstest_unmock = ( id , request ) => {
101168 restoreOriginalFactory ( id ) ;
@@ -105,6 +172,7 @@ __webpack_require__.rstest_unmock = (id, request) => {
105172 if ( request !== undefined ) {
106173 delete __webpack_require__ . rstest_mocked_ids_by_request [ request ] ;
107174 }
175+ unpublishNativeMock ( request ) ;
108176} ;
109177//#endregion
110178
@@ -239,6 +307,7 @@ const getMockImplementation = (mockType = 'mock') => {
239307 } ;
240308
241309 installFactory ( finalModFactory ) ;
310+ publishNativeMock ( request , ( ) => mockedModule ) ;
242311 return ;
243312 }
244313
@@ -282,6 +351,7 @@ const getMockImplementation = (mockType = 'mock') => {
282351 } ;
283352
284353 installFactory ( finalModFactory ) ;
354+ publishNativeMock ( request , modFactory ) ;
285355 }
286356 } ;
287357} ;
@@ -318,6 +388,22 @@ __webpack_require__.rstest_dynamic_require = (id, request) => {
318388} ;
319389//#endregion
320390
391+ //#region non-literal dynamic import() mock resolution
392+ /**
393+ * Expose the mocked instance for a clean `request` to the worker's native import
394+ * hook (loadEsModule/loadModule `defineRstestDynamicImport`), which handles a
395+ * non-literal `import(variable)` outside the webpack runtime. `const s='node:os';
396+ * import(s)` then resolves `s` to the mocked module here instead of natively
397+ * loading the real one. Published on the shared `globalThis` because the worker
398+ * hook has no `__webpack_require__`; returns `undefined` when the request isn't
399+ * mocked, so the hook performs its normal native import.
400+ */
401+ globalThis . __rstest_resolve_mocked_dynamic_request__ = ( request ) => {
402+ const mockedId = __webpack_require__ . rstest_mocked_ids_by_request [ request ] ;
403+ return mockedId !== undefined ? __webpack_require__ ( mockedId ) : undefined ;
404+ } ;
405+ //#endregion
406+
321407//#region rs.reset_modules
322408__webpack_require__ . rstest_reset_modules = ( ) => {
323409 const mockedIds = Object . keys ( __webpack_require__ . rstest_original_modules ) ;
0 commit comments