Skip to content

Commit 2a72a4f

Browse files
committed
fixup! module: add clearCache for CJS and ESM
1 parent d7340c0 commit 2a72a4f

29 files changed

+152
-459
lines changed

doc/api/module.md

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -82,34 +82,31 @@ added: REPLACEME
8282
For ES modules, pass `import.meta.url`.
8383
* `resolver` {string} Specifies how resolution should be performed. Must be either
8484
`'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.
85+
* `importAttributes` {Object} Optional import attributes. Only meaningful when
86+
`resolver` is `'import'`.
9087
91-
Clears module resolution and/or module caches for a module. This enables
88+
Clears the module resolution and module caches for a module. This enables
9289
reload patterns similar to deleting from `require.cache` in CommonJS, and is useful for
9390
hot module reload.
9491
95-
When `caches` is `'module'` or `'all'`, the specifier is resolved using the chosen `resolver`
96-
and the resolved module is removed from all internal caches (CommonJS `require` cache, ESM
97-
load cache, and ESM translators cache). When a `file:` URL is resolved, cached module jobs for
98-
the same file path are cleared even if they differ by search or hash. This means clearing
99-
`'./mod.mjs?v=1'` will also clear `'./mod.mjs?v=2'` and any other query/hash variants that
100-
resolve to the same file.
92+
The specifier is resolved using the chosen `resolver`, then the resolved module is removed
93+
from all internal caches (CommonJS `require` cache, CommonJS resolution caches, ESM resolve
94+
cache, ESM load cache, and ESM translators cache). When `resolver` is `'import'`,
95+
`importAttributes` are part of the ESM resolve-cache key, so only the exact
96+
`(specifier, parentURL, importAttributes)` resolution entry is removed. When a `file:` URL is
97+
resolved, cached module jobs for the same file path are cleared even if they differ by search
98+
or hash. This means clearing `'./mod.mjs?v=1'` will also clear `'./mod.mjs?v=2'` and any
99+
other query/hash variants that resolve to the same file.
101100
102-
When `caches` is `'resolution'` or `'all'` with `resolver` set to `'import'`, all ESM
103-
resolution cache entries for the given `(specifier, parentURL)` pair are cleared regardless
104-
of import attributes. When `resolver` is `'require'`, the specific CJS relative resolve
105-
cache entry for the `(parentDir, specifier)` pair is cleared, together with any cached
106-
`package.json` data for the resolved module's package.
101+
When `resolver` is `'require'`, cached `package.json` data for the resolved module's package
102+
is also cleared so that updated exports/imports conditions are picked up on the next
103+
resolution.
107104
108105
Clearing a module does not clear cached entries for its dependencies. When using
109106
`resolver: 'import'`, resolution cache entries for other specifiers that resolve to the
110-
same target are not cleared — only entries for the exact `(specifier, parentURL)` pair
111-
are removed. The module cache itself is cleared by resolved file path, so all specifiers
112-
pointing to the same file will see a fresh execution on next import.
107+
same target are not cleared — only the exact `(specifier, parentURL, importAttributes)`
108+
entry is removed. The module cache itself is cleared by resolved file path, so all
109+
specifiers pointing to the same file will see a fresh execution on next import.
113110
114111
#### Memory retention and static imports
115112
@@ -161,7 +158,6 @@ watch(base, async () => {
161158
clearCache(new URL(`${base.href}?v=${version}`), {
162159
parentURL: import.meta.url,
163160
resolver: 'import',
164-
caches: 'all',
165161
});
166162
version++;
167163
// Re-import with a new search parameter — this is a distinct module request
@@ -181,7 +177,6 @@ await import('./mod.mjs');
181177
clearCache('./mod.mjs', {
182178
parentURL: import.meta.url,
183179
resolver: 'import',
184-
caches: 'module',
185180
});
186181
await import('./mod.mjs'); // re-executes the module
187182
```
@@ -195,7 +190,6 @@ require('./mod.js');
195190
clearCache('./mod.js', {
196191
parentURL: pathToFileURL(__filename),
197192
resolver: 'require',
198-
caches: 'module',
199193
});
200194
require('./mod.js'); // eslint-disable-line node-core/no-duplicate-requires
201195
// re-executes the module

lib/internal/modules/cjs/loader.js

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,6 @@ module.exports = {
122122
initializeCJS,
123123
Module,
124124
clearCJSResolutionCaches,
125-
deleteCJSRelativeResolveCacheEntry,
126125
findLongestRegisteredExtension,
127126
resolveForCJSWithHooks,
128127
loadSourceForCJSWithHooks: loadSource,
@@ -251,23 +250,6 @@ function clearCJSResolutionCaches(filename) {
251250
}
252251
}
253252

254-
/**
255-
* Delete a single entry from the CJS relative resolve cache.
256-
* The cache key is `${parent.path}\x00${request}` where parent.path is the
257-
* directory containing the parent module (i.e. path.dirname(parentFilename)).
258-
* @param {string} parentDir Directory of the parent module (path.dirname of filename).
259-
* @param {string} request The specifier as originally passed to require().
260-
* @returns {boolean} true if the entry existed and was deleted.
261-
*/
262-
function deleteCJSRelativeResolveCacheEntry(parentDir, request) {
263-
const key = `${parentDir}\x00${request}`;
264-
if (relativeResolveCache[key] !== undefined) {
265-
delete relativeResolveCache[key];
266-
return true;
267-
}
268-
return false;
269-
}
270-
271253
let requireDepth = 0;
272254
let isPreloading = false;
273255
let statCache = null;

lib/internal/modules/clear.js

Lines changed: 46 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,10 @@ const {
1414
Module,
1515
resolveForCJSWithHooks,
1616
clearCJSResolutionCaches,
17-
deleteCJSRelativeResolveCacheEntry,
1817
} = require('internal/modules/cjs/loader');
1918
const { getFilePathFromFileURL } = require('internal/modules/helpers');
2019
const { fileURLToPath, isURL, URLParse, pathToFileURL } = require('internal/url');
21-
const { emitExperimentalWarning, isWindows } = require('internal/util');
20+
const { emitExperimentalWarning, kEmptyObject, isWindows } = require('internal/util');
2221
const { validateObject, validateOneOf, validateString } = require('internal/validators');
2322
const {
2423
codes: {
@@ -204,12 +203,12 @@ function isRelative(pathToCheck) {
204203
}
205204

206205
/**
207-
* Clear module resolution and/or module caches.
206+
* Clear module resolution and module caches.
208207
* @param {string|URL} specifier What would've been passed into import() or require().
209208
* @param {{
210209
* parentURL: string|URL,
210+
* importAttributes?: Record<string, string>,
211211
* resolver: 'import'|'require',
212-
* caches: 'resolution'|'module'|'all',
213212
* }} options
214213
*/
215214
function clearCache(specifier, options) {
@@ -223,89 +222,65 @@ function clearCache(specifier, options) {
223222
validateObject(options, 'options');
224223
const { parentURL, parentPath } = normalizeClearCacheParent(options.parentURL);
225224

226-
const { resolver, caches } = options;
225+
const { resolver } = options;
227226
validateOneOf(resolver, 'options.resolver', ['import', 'require']);
228-
validateOneOf(caches, 'options.caches', ['resolution', 'module', 'all']);
229227

230-
const clearResolution = caches === 'resolution' || caches === 'all';
231-
const clearModule = caches === 'module' || caches === 'all';
228+
const importAttributes = options.importAttributes ?? kEmptyObject;
229+
if (options.importAttributes !== undefined) {
230+
validateObject(options.importAttributes, 'options.importAttributes');
231+
}
232232

233-
// Resolve the specifier when module or resolution cache clearing is needed.
234-
// Must be done BEFORE clearing resolution caches since resolution
235-
// may rely on the resolve cache.
233+
// Resolve before clearing so resolution-cache entries are still available.
236234
let resolvedFilename = null;
237235
let resolvedURL = null;
238236

239-
if (clearModule || clearResolution) {
240-
if (resolver === 'require') {
241-
resolvedFilename = resolveClearCacheFilename(specifier, parentPath);
242-
if (resolvedFilename) {
243-
resolvedURL = pathToFileURL(resolvedFilename).href;
244-
}
245-
} else {
246-
resolvedURL = resolveClearCacheURL(specifier, parentURL);
247-
if (resolvedURL) {
248-
resolvedFilename = getFilePathFromFileURL(resolvedURL);
249-
}
237+
if (resolver === 'require') {
238+
resolvedFilename = resolveClearCacheFilename(specifier, parentPath);
239+
if (resolvedFilename) {
240+
resolvedURL = pathToFileURL(resolvedFilename).href;
250241
}
251-
}
252-
253-
// Clear resolution caches.
254-
if (clearResolution) {
255-
// ESM has a structured resolution cache keyed by (specifier, parentURL,
256-
// importAttributes). Clear all attribute variants for the given
257-
// (specifier, parentURL) pair since attributes don't affect resolution
258-
// per spec and it avoids partial-clear surprises.
259-
if (resolver === 'import') {
260-
const specifierStr = isSpecifierURL ? specifier.href : specifier;
261-
const cascadedLoader =
262-
require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
263-
cascadedLoader.deleteAllResolveCacheEntries(specifierStr, parentURL);
242+
} else {
243+
resolvedURL = resolveClearCacheURL(specifier, parentURL);
244+
if (resolvedURL) {
245+
resolvedFilename = getFilePathFromFileURL(resolvedURL);
264246
}
247+
}
265248

266-
// CJS resolution caches are only relevant when the resolver is 'require'.
267-
if (resolver === 'require' && parentPath) {
268-
// Delete the specific relativeResolveCache entry for this
269-
// (parent-dir, request) pair. More targeted than a full value-scan.
270-
const requestStr = isSpecifierURL ? specifier.href : specifier;
271-
deleteCJSRelativeResolveCacheEntry(path.dirname(parentPath), requestStr);
249+
// ESM resolution cache entries are keyed by
250+
// (specifier, parentURL, importAttributes).
251+
if (resolver === 'import') {
252+
const specifierStr = isSpecifierURL ? specifier.href : specifier;
253+
const cascadedLoader =
254+
require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
255+
cascadedLoader.deleteResolveCacheEntry(specifierStr, parentURL, importAttributes);
256+
}
272257

273-
// Clear package.json caches for the resolved module's package so that
274-
// updated exports/imports conditions are picked up on re-resolution.
275-
if (resolvedFilename) {
276-
const { getNearestParentPackageJSON, clearPackageJSONCache } =
277-
require('internal/modules/package_json_reader');
278-
const pkg = getNearestParentPackageJSON(resolvedFilename);
279-
if (pkg?.path) {
280-
clearPackageJSONCache(pkg.path);
281-
}
282-
}
258+
if (resolver === 'require' && resolvedFilename) {
259+
const { getNearestParentPackageJSON, clearPackageJSONCache } =
260+
require('internal/modules/package_json_reader');
261+
const pkg = getNearestParentPackageJSON(resolvedFilename);
262+
if (pkg?.path) {
263+
clearPackageJSONCache(pkg.path);
283264
}
284265
}
285266

286267
// Clear module caches everywhere in Node.js.
287-
if (clearModule) {
288-
// CJS Module._cache
289-
if (resolvedFilename) {
290-
const cachedModule = Module._cache[resolvedFilename];
291-
if (cachedModule !== undefined) {
292-
delete Module._cache[resolvedFilename];
293-
deleteModuleFromParents(cachedModule);
294-
}
295-
// Also clear CJS resolution caches that point to this filename,
296-
// even if only 'module' was requested, to avoid stale resolution
297-
// results pointing to a purged module.
298-
clearCJSResolutionCaches(resolvedFilename);
268+
if (resolvedFilename) {
269+
const cachedModule = Module._cache[resolvedFilename];
270+
if (cachedModule !== undefined) {
271+
delete Module._cache[resolvedFilename];
272+
deleteModuleFromParents(cachedModule);
299273
}
274+
// Also clear CJS resolution caches that point to this filename.
275+
clearCJSResolutionCaches(resolvedFilename);
276+
}
300277

301-
// ESM load cache and translators cjsCache
302-
if (resolvedURL) {
303-
const cascadedLoader =
304-
require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
305-
deleteLoadCacheEntries(cascadedLoader.loadCache, resolvedURL);
306-
const { clearCjsCache } = require('internal/modules/esm/translators');
307-
clearCjsCache(resolvedURL);
308-
}
278+
if (resolvedURL) {
279+
const cascadedLoader =
280+
require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
281+
deleteLoadCacheEntries(cascadedLoader.loadCache, resolvedURL);
282+
const { clearCjsCache } = require('internal/modules/esm/translators');
283+
clearCjsCache(resolvedURL);
309284
}
310285
}
311286

lib/internal/modules/esm/loader.js

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -180,17 +180,6 @@ class ModuleLoader {
180180
return this.#resolveCache.deleteBySpecifier(specifier, parentURL, importAttributes);
181181
}
182182

183-
/**
184-
* Delete all cached resolution entries for a specifier from a parent URL,
185-
* regardless of import attributes.
186-
* @param {string} specifier
187-
* @param {string|undefined} parentURL
188-
* @returns {boolean} true if at least one entry was deleted.
189-
*/
190-
deleteAllResolveCacheEntries(specifier, parentURL) {
191-
return this.#resolveCache.deleteAllBySpecifier(specifier, parentURL);
192-
}
193-
194183
/**
195184
* Check if a cached resolution exists for a specific request.
196185
* @param {string} specifier

lib/internal/modules/esm/module_map.js

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ const {
88
ObjectKeys,
99
ObjectPrototypeHasOwnProperty,
1010
SafeMap,
11-
StringPrototypeStartsWith,
1211
} = primordials;
1312
const { kImplicitTypeAttribute } = require('internal/modules/esm/assert');
1413
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
@@ -110,34 +109,6 @@ class ResolveCache extends SafeMap {
110109
}
111110
return true;
112111
}
113-
114-
/**
115-
* Delete all cached resolution entries for a specifier in a parent URL,
116-
* regardless of import attributes.
117-
* @param {string} specifier
118-
* @param {string|undefined} parentURL
119-
* @returns {boolean} true if at least one entry was deleted.
120-
*/
121-
deleteAllBySpecifier(specifier, parentURL) {
122-
const entries = super.get(parentURL);
123-
if (entries == null) {
124-
return false;
125-
}
126-
// Keys are serialized as `specifier + '::' + attributes`. Match all
127-
// entries whose prefix up to and including '::' matches this specifier.
128-
const prefix = specifier + '::';
129-
let deleted = false;
130-
for (const key of ObjectKeys(entries)) {
131-
if (key === prefix || StringPrototypeStartsWith(key, prefix)) {
132-
delete entries[key];
133-
deleted = true;
134-
}
135-
}
136-
if (deleted && ObjectKeys(entries).length === 0) {
137-
super.delete(parentURL);
138-
}
139-
return deleted;
140-
}
141112
}
142113

143114
/**

test/es-module/test-module-clear-cache-caches-all.mjs

Lines changed: 0 additions & 35 deletions
This file was deleted.

0 commit comments

Comments
 (0)