@@ -20,6 +20,7 @@ const { |
20 | 20 | ReflectConstruct, |
21 | 21 | ReflectGet, |
22 | 22 | SafeMap, |
| 23 | + StringPrototypeIncludes, |
23 | 24 | StringPrototypeSlice, |
24 | 25 | StringPrototypeStartsWith, |
25 | 26 | } = primordials; |
@@ -207,6 +208,7 @@ class MockModuleContext { |
207 | 208 | baseURL, |
208 | 209 | cache, |
209 | 210 | caller, |
| 211 | + cjsPath, |
210 | 212 | format, |
211 | 213 | fullPath, |
212 | 214 | moduleExports, |
@@ -222,12 +224,25 @@ class MockModuleContext { |
222 | 224 |
|
223 | 225 | sharedState.mockMap.set(baseURL, config); |
224 | 226 | sharedState.mockMap.set(fullPath, config); |
| 227 | + // For dual packages (e.g., a package with a "exports" field that exposes |
| 228 | + // both ESM and CJS entry points), the file selected by the ESM resolver |
| 229 | + // (used to compute fullPath) may differ from the one selected by CJS |
| 230 | + // require(). Register the CJS-resolved path so that require() also picks |
| 231 | + // up the mock. See https://github.com/nodejs/node/issues/58231. |
| 232 | + if (cjsPath !== null && cjsPath !== fullPath) { |
| 233 | + sharedState.mockMap.set(cjsPath, config); |
| 234 | + } |
225 | 235 |
|
226 | 236 | this.#sharedState = sharedState; |
227 | 237 | this.#restore = { |
228 | 238 | __proto__: null, |
229 | 239 | baseURL, |
230 | 240 | cached: fullPath in Module._cache, |
| 241 | + cjsPath, |
| 242 | + cjsCached: cjsPath !== null && cjsPath !== fullPath && |
| 243 | + cjsPath in Module._cache, |
| 244 | + cjsValue: cjsPath !== null && cjsPath !== fullPath ? |
| 245 | + Module._cache[cjsPath] : undefined, |
231 | 246 | format, |
232 | 247 | fullPath, |
233 | 248 | value: Module._cache[fullPath], |
@@ -257,6 +272,9 @@ class MockModuleContext { |
257 | 272 | } |
258 | 273 |
|
259 | 274 | delete Module._cache[fullPath]; |
| 275 | + if (cjsPath !== null && cjsPath !== fullPath) { |
| 276 | + delete Module._cache[cjsPath]; |
| 277 | + } |
260 | 278 | sharedState.mockExports.set(baseURL, { |
261 | 279 | __proto__: null, |
262 | 280 | moduleExports, |
@@ -276,6 +294,14 @@ class MockModuleContext { |
276 | 294 | Module._cache[this.#restore.fullPath] = this.#restore.value; |
277 | 295 | } |
278 | 296 |
|
| 297 | + if (this.#restore.cjsPath !== null && |
| 298 | + this.#restore.cjsPath !== this.#restore.fullPath) { |
| 299 | + delete Module._cache[this.#restore.cjsPath]; |
| 300 | + if (this.#restore.cjsCached) { |
| 301 | + Module._cache[this.#restore.cjsPath] = this.#restore.cjsValue; |
| 302 | + } |
| 303 | + } |
| 304 | + |
279 | 305 | const mock = mocks.get(this.#restore.baseURL); |
280 | 306 |
|
281 | 307 | if (mock !== undefined) { |
@@ -285,6 +311,10 @@ class MockModuleContext { |
285 | 311 |
|
286 | 312 | this.#sharedState.mockMap.delete(this.#restore.baseURL); |
287 | 313 | this.#sharedState.mockMap.delete(this.#restore.fullPath); |
| 314 | + if (this.#restore.cjsPath !== null && |
| 315 | + this.#restore.cjsPath !== this.#restore.fullPath) { |
| 316 | + this.#sharedState.mockMap.delete(this.#restore.cjsPath); |
| 317 | + } |
288 | 318 | this.#restore = undefined; |
289 | 319 | } |
290 | 320 | } |
@@ -680,11 +710,19 @@ class MockTracker { |
680 | 710 |
|
681 | 711 | const fullPath = StringPrototypeStartsWith(url, 'file://') ? |
682 | 712 | fileURLToPath(url) : null; |
| 713 | + // For dual packages, the ESM resolver may return a different file than |
| 714 | + // CJS require() would for the same specifier (e.g., when a package's |
| 715 | + // "exports" field points to different files for the "import" and |
| 716 | + // "require" conditions). Compute the CJS-resolved path so that |
| 717 | + // require() of a mocked module also picks up the mock. |
| 718 | + // See https://github.com/nodejs/node/issues/58231. |
| 719 | + const cjsPath = resolveAsCJS(mockSpecifier, caller, fullPath); |
683 | 720 | const ctx = new MockModuleContext({ |
684 | 721 | __proto__: null, |
685 | 722 | baseURL: baseURL.href, |
686 | 723 | cache, |
687 | 724 | caller, |
| 725 | + cjsPath, |
688 | 726 | format, |
689 | 727 | fullPath, |
690 | 728 | moduleExports, |
@@ -987,6 +1025,54 @@ function cjsMockModuleLoad(request, parent, isMain) { |
987 | 1025 | return modExports; |
988 | 1026 | } |
989 | 1027 |
|
| 1028 | +// Resolve `specifier` using CJS resolution rules so that mocks for dual |
| 1029 | +// packages (e.g., a package whose "exports" field points to different files |
| 1030 | +// for the "import" and "require" conditions) also intercept require(). |
| 1031 | +// Returns an absolute file path on success, or null when the specifier cannot |
| 1032 | +// be resolved as CJS (for example, when the package is ESM-only or when it is |
| 1033 | +// a non-file URL such as data: or node:). |
| 1034 | +function resolveAsCJS(specifier, callerURL, esmFullPath) { |
| 1035 | + if (isBuiltin(specifier) || |
| 1036 | + StringPrototypeStartsWith(specifier, 'node:') || |
| 1037 | + StringPrototypeStartsWith(specifier, 'data:')) { |
| 1038 | + return null; |
| 1039 | + } |
| 1040 | + |
| 1041 | + let parentPath; |
| 1042 | + if (StringPrototypeStartsWith(callerURL, 'file://')) { |
| 1043 | + try { |
| 1044 | + parentPath = fileURLToPath(callerURL); |
| 1045 | + } catch { |
| 1046 | + return null; |
| 1047 | + } |
| 1048 | + } else { |
| 1049 | + return null; |
| 1050 | + } |
| 1051 | + |
| 1052 | + try { |
| 1053 | + const tmpModule = new Module(parentPath, null); |
| 1054 | + tmpModule.paths = _nodeModulePaths(parentPath); |
| 1055 | + const resolved = _resolveFilename(specifier, tmpModule, false); |
| 1056 | + if (typeof resolved !== 'string') { |
| 1057 | + return null; |
| 1058 | + } |
| 1059 | + // If the resolution matches what the ESM resolver picked, there is |
| 1060 | + // nothing additional to register. |
| 1061 | + if (resolved === esmFullPath) { |
| 1062 | + return esmFullPath; |
| 1063 | + } |
| 1064 | + // If the resolution returned something that is not a filesystem path |
| 1065 | + // (e.g., a builtin id without a slash or backslash), ignore it. |
| 1066 | + if (!StringPrototypeIncludes(resolved, '/') && |
| 1067 | + !StringPrototypeIncludes(resolved, '\\')) { |
| 1068 | + return null; |
| 1069 | + } |
| 1070 | + return resolved; |
| 1071 | + } catch { |
| 1072 | + return null; |
| 1073 | + } |
| 1074 | +} |
| 1075 | + |
990 | 1076 | function validateStringOrSymbol(value, name) { |
991 | 1077 | if (typeof value !== 'string' && typeof value !== 'symbol') { |
992 | 1078 | throw new ERR_INVALID_ARG_TYPE(name, ['string', 'symbol'], value); |
|