Skip to content

Commit 105ff7e

Browse files
committed
fixup! module: add clearCache for CJS and ESM
1 parent 35d207e commit 105ff7e

20 files changed

+604
-115
lines changed

doc/api/module.md

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -66,55 +66,73 @@ const require = createRequire(import.meta.url);
6666
const siblingModule = require('./sibling-module');
6767
```
6868
69-
### `module.clearCache(specifier[, options])`
69+
### `module.clearCache(specifier, options)`
7070
7171
<!-- YAML
7272
added: REPLACEME
7373
-->
7474
7575
> Stability: 1.1 - Active development
7676
77-
* `specifier` {string|URL} The module specifier or URL to resolve. The resolved URL/filename
78-
is cleared from the load cache; the specifier (with `parentURL`) is cleared from the
79-
resolve cache.
77+
* `specifier` {string|URL} The module specifier, as it would have been passed to
78+
`import()` or `require()`.
8079
* `options` {Object}
81-
* `parentURL` {string|URL} The parent URL used to resolve non-URL specifiers.
82-
For CommonJS, pass `pathToFileURL(__filename)`. For ES modules, pass `import.meta.url`.
83-
* Returns: {Object} An object with `{ require: boolean, import: boolean }` indicating whether entries
84-
were removed from each cache.
85-
86-
Clears the CommonJS `require` cache and the ESM module cache for a module. This enables
80+
* `parentURL` {string|URL} The parent URL used to resolve the specifier. Parent identity
81+
is part of the resolution cache key. For CommonJS, pass `pathToFileURL(__filename)`.
82+
For ES modules, pass `import.meta.url`.
83+
* `resolver` {string} Specifies how resolution should be performed. Must be either
84+
`'import'` or `'require'`.
85+
* `caches` {string} Specifies which caches to clear. Must be one of:
86+
* `'resolution'` — only clear the resolution cache entry for this specifier.
87+
* `'module'` — clear the cached module everywhere in Node.js (not counting
88+
JS-level references).
89+
* `'all'` — clear both resolution and module caches.
90+
* `importAttributes` {Object} Optional import attributes. Only meaningful when
91+
`resolver` is `'import'`.
92+
93+
Clears module resolution and/or module caches for a module. This enables
8794
reload patterns similar to deleting from `require.cache` in CommonJS, and is useful for HMR.
88-
Resolution failures for one module system do not throw; check the returned flags to see what
89-
was cleared.
90-
This does not clear resolution cache entries for that specifier. Clearing a module does not
91-
clear cached entries for its dependencies, and other specifiers that resolve to the same target
92-
may remain.
93-
When a `file:` URL is resolved, cached module jobs for the same file path are cleared even if
94-
they differ by search or hash.
95-
If the same file is loaded via multiple specifiers (for example `require('./x')` alongside
96-
`import('./x.js?t=1')` and `import('./x.js?t=2')`), resolution cache entries for each specifier
97-
remain. Use consistent specifiers, or call `clearCache()` for each specifier you want to
98-
re-execute.
95+
96+
When `caches` is `'module'` or `'all'`, the specifier is resolved using the chosen `resolver`
97+
and the resolved module is removed from all internal caches (CommonJS `require` cache, ESM
98+
load cache, and ESM translators cache). When a `file:` URL is resolved, cached module jobs for
99+
the same file path are cleared even if they differ by search or hash.
100+
101+
When `caches` is `'resolution'` or `'all'` with `resolver` set to `'import'`, the ESM
102+
resolution cache entry for the given `(specifier, parentURL, importAttributes)` tuple is
103+
cleared. CJS does not maintain a separate resolution cache.
104+
105+
Clearing a module does not clear cached entries for its dependencies, and other specifiers
106+
that resolve to the same target may remain. Use consistent specifiers, or call `clearCache()`
107+
for each specifier you want to re-execute.
99108
100109
```mjs
101110
import { clearCache } from 'node:module';
102111

103112
const url = new URL('./mod.mjs', import.meta.url);
104113
await import(url.href);
105114

106-
clearCache(url);
115+
clearCache(url, {
116+
parentURL: import.meta.url,
117+
resolver: 'import',
118+
caches: 'module',
119+
});
107120
await import(url.href); // re-executes the module
108121
```
109122
110123
```cjs
111124
const { clearCache } = require('node:module');
125+
const { pathToFileURL } = require('node:url');
112126
const path = require('node:path');
113127

114128
const file = path.join(__dirname, 'mod.js');
115129
require(file);
116130

117-
clearCache(file);
131+
clearCache(file, {
132+
parentURL: pathToFileURL(__filename),
133+
resolver: 'require',
134+
caches: 'module',
135+
});
118136
require(file); // re-executes the module
119137
```
120138

lib/internal/modules/clear.js

Lines changed: 64 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const {
3434
const { Module, resolveForCJSWithHooks } = require('internal/modules/cjs/loader');
3535
const { fileURLToPath, isURL, URLParse, pathToFileURL } = require('internal/url');
3636
const { kEmptyObject, isWindows } = require('internal/util');
37-
const { validateObject, validateString } = require('internal/validators');
37+
const { validateObject, validateOneOf, validateString } = require('internal/validators');
3838
const {
3939
codes: {
4040
ERR_INVALID_ARG_VALUE,
@@ -51,14 +51,10 @@ const {
5151

5252
/**
5353
* Normalize the parent URL for cache clearing.
54-
* @param {string|URL|undefined} parentURL
55-
* @returns {{ parentURL: string|undefined, parentPath: string|undefined }}
54+
* @param {string|URL} parentURL
55+
* @returns {{ parentURL: string, parentPath: string|undefined }}
5656
*/
5757
function normalizeClearCacheParent(parentURL) {
58-
if (parentURL === undefined) {
59-
return { __proto__: null, parentURL: undefined, parentPath: undefined };
60-
}
61-
6258
if (isURL(parentURL)) {
6359
let parentPath;
6460
if (parentURL.protocol === 'file:' && parentURL.search === '' && parentURL.hash === '') {
@@ -268,61 +264,85 @@ function isRelative(pathToCheck) {
268264
}
269265

270266
/**
271-
* Clear CommonJS and/or ESM module cache entries.
272-
* @param {string|URL} specifier
273-
* @param {object} [options]
274-
* @param {string|URL} [options.parentURL]
275-
* @returns {{ require: boolean, import: boolean }}
267+
* Clear module resolution and/or module caches.
268+
* @param {string|URL} specifier What would've been passed into import() or require().
269+
* @param {{
270+
* parentURL: string|URL,
271+
* importAttributes?: Record<string, string>,
272+
* resolver: 'import'|'require',
273+
* caches: 'resolution'|'module'|'all',
274+
* }} options
276275
*/
277-
function clearCache(specifier, options = kEmptyObject) {
276+
function clearCache(specifier, options) {
278277
const isSpecifierURL = isURL(specifier);
279278
if (!isSpecifierURL) {
280279
validateString(specifier, 'specifier');
281280
}
282281

283282
validateObject(options, 'options');
284283
const { parentURL, parentPath } = normalizeClearCacheParent(options.parentURL);
285-
const result = { __proto__: null, require: false, import: false };
286284

287-
try {
288-
const deleteCommonjsCachesForFilename = (filename) => {
289-
let deleted = false;
290-
const cachedModule = Module._cache[filename];
291-
if (cachedModule !== undefined) {
292-
delete Module._cache[filename];
293-
deleted = true;
294-
deleteModuleFromParents(cachedModule);
295-
}
296-
return deleted;
297-
};
285+
const { resolver, caches } = options;
286+
validateOneOf(resolver, 'options.resolver', ['import', 'require']);
287+
validateOneOf(caches, 'options.caches', ['resolution', 'module', 'all']);
298288

299-
const filename = resolveClearCacheFilename(specifier, parentPath);
300-
if (filename) {
301-
result.require = deleteCommonjsCachesForFilename(filename);
302-
}
289+
const importAttributes = options.importAttributes ?? kEmptyObject;
290+
if (options.importAttributes !== undefined) {
291+
validateObject(options.importAttributes, 'options.importAttributes');
292+
}
293+
294+
const clearResolution = caches === 'resolution' || caches === 'all';
295+
const clearModule = caches === 'module' || caches === 'all';
303296

304-
if (parentURL !== undefined) {
305-
const url = resolveClearCacheURL(specifier, parentURL);
306-
const resolvedPath = getFilePathFromClearCacheURL(url);
307-
if (resolvedPath && resolvedPath !== filename) {
308-
if (deleteCommonjsCachesForFilename(resolvedPath)) {
309-
result.require = true;
310-
}
297+
// Resolve the specifier when module cache clearing is needed.
298+
// Must be done BEFORE clearing resolution caches since resolution
299+
// may rely on the resolve cache.
300+
let resolvedFilename = null;
301+
let resolvedURL = null;
302+
303+
if (clearModule) {
304+
if (resolver === 'require') {
305+
resolvedFilename = resolveClearCacheFilename(specifier, parentPath);
306+
if (resolvedFilename) {
307+
resolvedURL = pathToFileURL(resolvedFilename).href;
308+
}
309+
} else {
310+
resolvedURL = resolveClearCacheURL(specifier, parentURL);
311+
if (resolvedURL) {
312+
resolvedFilename = getFilePathFromClearCacheURL(resolvedURL);
311313
}
312314
}
315+
}
313316

314-
const url = resolveClearCacheURL(specifier, parentURL);
317+
// Clear resolution cache. Only ESM has a structured resolution cache;
318+
// CJS resolution results are not separately cached.
319+
if (clearResolution && resolver === 'import') {
320+
const specifierStr = isSpecifierURL ? specifier.href : specifier;
315321
const cascadedLoader =
316322
require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
317-
const loadDeleted = deleteLoadCacheEntries(cascadedLoader.loadCache, url);
318-
const { clearCjsCache } = require('internal/modules/esm/translators');
319-
const cjsCacheDeleted = clearCjsCache(url);
320-
result.import = loadDeleted || cjsCacheDeleted;
321-
} catch {
322-
// Best effort: avoid throwing for require cache clearing.
323+
cascadedLoader.deleteResolveCacheEntry(specifierStr, parentURL, importAttributes);
323324
}
324325

325-
return result;
326+
// Clear module caches everywhere in Node.js.
327+
if (clearModule) {
328+
// CJS Module._cache
329+
if (resolvedFilename) {
330+
const cachedModule = Module._cache[resolvedFilename];
331+
if (cachedModule !== undefined) {
332+
delete Module._cache[resolvedFilename];
333+
deleteModuleFromParents(cachedModule);
334+
}
335+
}
336+
337+
// ESM load cache and translators cjsCache
338+
if (resolvedURL) {
339+
const cascadedLoader =
340+
require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
341+
deleteLoadCacheEntries(cascadedLoader.loadCache, resolvedURL);
342+
const { clearCjsCache } = require('internal/modules/esm/translators');
343+
clearCjsCache(resolvedURL);
344+
}
345+
}
326346
}
327347

328348
module.exports = {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Tests that caches: 'all' clears both the resolution cache
2+
// and the module cache. The module IS re-evaluated after clearing.
3+
4+
import '../common/index.mjs';
5+
6+
import assert from 'node:assert';
7+
import { clearCache } from 'node:module';
8+
9+
const specifier = '../fixtures/module-cache/esm-counter.mjs';
10+
11+
const first = await import(specifier);
12+
assert.strictEqual(first.count, 1);
13+
14+
// caches: 'all' — clears both resolution and module caches.
15+
clearCache(specifier, {
16+
parentURL: import.meta.url,
17+
resolver: 'import',
18+
caches: 'all',
19+
});
20+
21+
// Module should be re-evaluated.
22+
const second = await import(specifier);
23+
assert.strictEqual(second.count, 2);
24+
25+
// Clearing again with 'all' should also work.
26+
clearCache(specifier, {
27+
parentURL: import.meta.url,
28+
resolver: 'import',
29+
caches: 'all',
30+
});
31+
32+
const third = await import(specifier);
33+
assert.strictEqual(third.count, 3);
34+
35+
delete globalThis.__module_cache_esm_counter;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Tests that caches: 'resolution' only clears the ESM resolution cache.
2+
// The module itself is NOT re-evaluated because the load cache still holds it.
3+
// Also tests that caches: 'resolution' with resolver: 'require' is harmless
4+
// (CJS has no separate resolution cache).
5+
6+
import '../common/index.mjs';
7+
8+
import assert from 'node:assert';
9+
import { clearCache, createRequire } from 'node:module';
10+
import { fileURLToPath } from 'node:url';
11+
12+
const require = createRequire(import.meta.url);
13+
14+
// --- ESM: resolution-only clearing should NOT re-evaluate ---
15+
16+
const specifier = '../fixtures/module-cache/esm-counter.mjs';
17+
18+
const first = await import(specifier);
19+
assert.strictEqual(first.count, 1);
20+
21+
// Clear only resolution cache.
22+
clearCache(specifier, {
23+
parentURL: import.meta.url,
24+
resolver: 'import',
25+
caches: 'resolution',
26+
});
27+
28+
// Module must NOT be re-evaluated — load cache still holds it.
29+
const second = await import(specifier);
30+
assert.strictEqual(second.count, 1);
31+
assert.strictEqual(first, second);
32+
33+
// Now clear the module cache — module SHOULD be re-evaluated.
34+
clearCache(specifier, {
35+
parentURL: import.meta.url,
36+
resolver: 'import',
37+
caches: 'module',
38+
});
39+
40+
const third = await import(specifier);
41+
assert.strictEqual(third.count, 2);
42+
43+
// --- CJS: resolution-only clearing should be a no-op ---
44+
45+
const cjsFixturePath = fileURLToPath(
46+
new URL('../fixtures/module-cache/cjs-counter.js', import.meta.url),
47+
);
48+
49+
require(cjsFixturePath);
50+
assert.notStrictEqual(require.cache[cjsFixturePath], undefined);
51+
52+
// caches: 'resolution' + resolver: 'require' → no-op for CJS.
53+
clearCache(cjsFixturePath, {
54+
parentURL: import.meta.url,
55+
resolver: 'require',
56+
caches: 'resolution',
57+
});
58+
59+
// Module._cache should be unaffected.
60+
assert.notStrictEqual(require.cache[cjsFixturePath], undefined);
61+
62+
delete globalThis.__module_cache_esm_counter;
63+
delete globalThis.__module_cache_cjs_counter;

0 commit comments

Comments
 (0)