Skip to content

Commit ddde84e

Browse files
committed
fixup! module: add clearCache for CJS and ESM
1 parent 0b472a6 commit ddde84e

File tree

3 files changed

+34
-61
lines changed

3 files changed

+34
-61
lines changed

doc/api/module.md

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -105,18 +105,20 @@ When `caches` is `'resolution'` or `'all'` with `resolver` set to `'import'`, th
105105
resolution cache entry for the given `(specifier, parentURL, importAttributes)` tuple is
106106
cleared. When `resolver` is `'require'`, internal CJS resolution caches (including the
107107
relative resolve cache and path cache) are also cleared for the resolved filename.
108-
When `importAttributes` are provided, they are used to construct the cache key; if a module
108+
When `importAttributes` are provided for `'import'` resolution, they are used to construct the cache key; if a module
109109
was loaded with multiple different import attribute combinations, only the matching entry
110110
is cleared from the resolution cache. The module cache itself (`caches: 'module'`) clears
111111
all attribute variants for the URL.
112112
113-
Clearing a module does not clear cached entries for its dependencies, and other specifiers
114-
that resolve to the same target may remain. Use consistent specifiers, or call `clearCache()`
115-
for each specifier you want to re-execute.
113+
Clearing a module does not clear cached entries for its dependencies. When using
114+
`resolver: 'import'`, resolution cache entries for other specifiers that resolve to the
115+
same target are not cleared — only the exact `(specifier, parentURL, importAttributes)`
116+
entry is removed. The module cache itself is cleared by resolved file path, so all
117+
specifiers pointing to the same file will see a fresh execution on next import.
116118
117119
#### ECMA-262 spec considerations
118120
119-
Re-importing the exact same `(specifier, parentURL)` pair after clearing the module cache
121+
Re-importing the exact same `(specifier, parentURL, importAttribtues)` tuple after clearing the module cache
120122
technically violates the idempotency invariant of the ECMA-262
121123
[`HostLoadImportedModule`][] host hook, which expects that the same module request always
122124
returns the same Module Record for a given referrer. For spec-compliant usage, use
@@ -149,31 +151,28 @@ watch(base, async () => {
149151
```mjs
150152
import { clearCache } from 'node:module';
151153

152-
const url = new URL('./mod.mjs', import.meta.url);
153-
await import(url.href);
154+
await import('./mod.mjs');
154155

155-
clearCache(url, {
156+
clearCache('./mod.mjs', {
156157
parentURL: import.meta.url,
157158
resolver: 'import',
158159
caches: 'module',
159160
});
160-
await import(url.href); // re-executes the module
161+
await import('./mod.mjs'); // re-executes the module
161162
```
162163
163164
```cjs
164165
const { clearCache } = require('node:module');
165-
const { pathToFileURL } = require('node:url');
166-
const path = require('node:path');
167166

168-
const file = path.join(__dirname, 'mod.js');
169-
require(file);
167+
require('./mod.js');
170168

171-
clearCache(file, {
172-
parentURL: pathToFileURL(__filename),
169+
clearCache('./mod.js', {
170+
parentURL: __filename,
173171
resolver: 'require',
174172
caches: 'module',
175173
});
176-
require(file); // re-executes the module
174+
require('./mod.js'); // eslint-disable-line node-core/no-duplicate-requires
175+
// re-executes the module
177176
```
178177
179178
### `module.findPackageJSON(specifier[, base])`

lib/internal/modules/cjs/loader.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ const kIsExecuting = Symbol('kIsExecuting');
111111
const kURL = Symbol('kURL');
112112
const kFormat = Symbol('kFormat');
113113

114+
const relativeResolveCache = { __proto__: null };
115+
114116
// Set first due to cycle with ESM loader functions.
115117
module.exports = {
116118
kModuleSource,
@@ -120,6 +122,7 @@ module.exports = {
120122
initializeCJS,
121123
Module,
122124
clearCJSResolutionCaches,
125+
relativeResolveCache,
123126
findLongestRegisteredExtension,
124127
resolveForCJSWithHooks,
125128
loadSourceForCJSWithHooks: loadSource,
@@ -224,8 +227,6 @@ let { startTimer, endTimer } = debugWithTimer('module_timer', (start, end) => {
224227
const { tracingChannel } = require('diagnostics_channel');
225228
const onRequire = getLazy(() => tracingChannel('module.require'));
226229

227-
const relativeResolveCache = { __proto__: null };
228-
229230
/**
230231
* Clear all entries in the CJS relative resolve cache and _pathCache
231232
* that map to a given filename. This is needed by clearCache() to

lib/internal/modules/clear.js

Lines changed: 16 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ const {
1010
StringPrototypeStartsWith,
1111
} = primordials;
1212

13-
const { Module, resolveForCJSWithHooks, clearCJSResolutionCaches } = require('internal/modules/cjs/loader');
13+
const {
14+
Module,
15+
resolveForCJSWithHooks,
16+
clearCJSResolutionCaches,
17+
relativeResolveCache,
18+
} = require('internal/modules/cjs/loader');
1419
const { fileURLToPath, isURL, URLParse, pathToFileURL } = require('internal/url');
1520
const { emitExperimentalWarning, kEmptyObject, isWindows } = require('internal/util');
1621
const { validateObject, validateOneOf, validateString } = require('internal/validators');
@@ -56,23 +61,6 @@ function normalizeClearCacheParent(parentURL) {
5661
return { __proto__: null, parentURL: url.href, parentPath };
5762
}
5863

59-
/**
60-
* Parse a specifier as a URL when possible.
61-
* @param {string|URL} specifier
62-
* @returns {URL|null}
63-
*/
64-
function getURLFromClearCacheSpecifier(specifier) {
65-
if (isURL(specifier)) {
66-
return specifier;
67-
}
68-
69-
if (typeof specifier !== 'string' || path.isAbsolute(specifier)) {
70-
return null;
71-
}
72-
73-
return URLParse(specifier) ?? null;
74-
}
75-
7664
/**
7765
* Create a synthetic parent module for CJS resolution.
7866
* @param {string} parentPath
@@ -88,10 +76,9 @@ function createParentModuleForClearCache(parentPath) {
8876
/**
8977
* Resolve a cache filename for CommonJS.
9078
* Always goes through resolveForCJSWithHooks so that registered hooks
91-
* are respected. For file: URLs, search/hash are stripped before resolving
92-
* since CJS operates on file paths. For non-file URLs, the specifier is
93-
* passed as-is to let hooks handle it.
94-
* @param {string|URL} specifier
79+
* are respected. CJS operates on file paths and bare specifiers; URL
80+
* objects are not valid require() arguments so they are not supported.
81+
* @param {string} specifier
9582
* @param {string|undefined} parentPath
9683
* @returns {string|null}
9784
*/
@@ -100,26 +87,9 @@ function resolveClearCacheFilename(specifier, parentPath) {
10087
return null;
10188
}
10289

103-
const parsedURL = getURLFromClearCacheSpecifier(specifier);
104-
let request = specifier;
105-
if (parsedURL) {
106-
if (parsedURL.protocol === 'file:') {
107-
// Strip search/hash - CJS operates on file paths.
108-
if (parsedURL.search !== '' || parsedURL.hash !== '') {
109-
parsedURL.search = '';
110-
parsedURL.hash = '';
111-
}
112-
request = fileURLToPath(parsedURL);
113-
} else {
114-
// Non-file URLs (e.g. virtual://) - pass the href as-is
115-
// so that registered hooks can resolve them.
116-
request = parsedURL.href;
117-
}
118-
}
119-
12090
const parent = parentPath ? createParentModuleForClearCache(parentPath) : null;
12191
try {
122-
const { filename, format } = resolveForCJSWithHooks(request, parent, false, false);
92+
const { filename, format } = resolveForCJSWithHooks(specifier, parent, false, false);
12393
if (format === 'builtin') {
12494
return null;
12595
}
@@ -314,10 +284,13 @@ function clearCache(specifier, options) {
314284
}
315285

316286
// CJS has relativeResolveCache and Module._pathCache that map
317-
// specifiers to filenames. Clear entries pointing to the resolved file.
318-
if (resolvedFilename) {
319-
clearCJSResolutionCaches(resolvedFilename);
287+
// specifiers to filenames. Only clear the exact entry for this request.
288+
if (resolver === 'require') {
289+
const request = isSpecifierURL ? specifier.href : specifier;
290+
delete relativeResolveCache[`${parentPath}\x00${request}`];
291+
}
320292

293+
if (resolvedFilename) {
321294
// Clear package.json caches for the resolved module's package so that
322295
// updated exports/imports conditions are picked up on re-resolution.
323296
const { getNearestParentPackageJSON, clearPackageJSONCache } =

0 commit comments

Comments
 (0)