Skip to content

Commit ba8ffaa

Browse files
committed
fixup! module: add clearCache for CJS and ESM
1 parent 0507308 commit ba8ffaa

File tree

14 files changed

+382
-56
lines changed

14 files changed

+382
-56
lines changed

doc/api/module.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,20 +76,22 @@ added: REPLACEME
7676
7777
* `specifier` {string|URL} The module specifier or URL to clear.
7878
* `options` {Object}
79-
* `mode` {string} Which caches to clear. Supported values are `'all'`, `'cjs'`, and `'esm'`.
79+
* `mode` {string} Which caches to clear. Supported values are `'all'`, `'commonjs'`, and `'module'`.
8080
**Default:** `'all'`.
81-
* `parentURL` {string|URL} The parent URL or absolute path used to resolve non-URL specifiers.
82-
For CommonJS, pass `__filename`. For ES modules, pass `import.meta.url`.
83-
* `type` {string} Import attributes `type` used for ESM resolution.
84-
* `importAttributes` {Object} Import attributes for ESM resolution. Cannot be used with `type`.
85-
* Returns: {Object} An object with `{ cjs: boolean, esm: boolean }` indicating whether entries
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+
* `importAttributes` {Object} Import attributes for ESM resolution.
84+
* Returns: {Object} An object with `{ commonjs: boolean, module: boolean }` indicating whether entries
8685
were removed from each cache.
8786
8887
Clears the CommonJS `require` cache and/or the ESM module cache for a module. This enables
8988
reload patterns similar to deleting from `require.cache` in CommonJS, and is useful for HMR.
9089
When `mode` is `'all'`, resolution failures for one module system do not throw; check the
9190
returned flags to see what was cleared.
92-
This also clears internal resolution caches for the resolved module.
91+
This also clears internal resolution caches for the resolved module. Clearing a module does
92+
not clear cached entries for its dependencies.
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.
9395
9496
```mjs
9597
import { clearCache } from 'node:module';

lib/internal/modules/cjs/loader.js

Lines changed: 127 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2033,7 +2033,7 @@ function createRequire(filenameOrURL) {
20332033
}
20342034

20352035
/**
2036-
* Normalize the parent URL/path for cache clearing.
2036+
* Normalize the parent URL for cache clearing.
20372037
* @param {string|URL|undefined} parentURL
20382038
* @returns {{ parentURL: string|undefined, parentPath: string|undefined }}
20392039
*/
@@ -2051,18 +2051,10 @@ function normalizeClearCacheParent(parentURL) {
20512051
}
20522052

20532053
validateString(parentURL, 'options.parentURL');
2054-
if (path.isAbsolute(parentURL)) {
2055-
return {
2056-
__proto__: null,
2057-
parentURL: pathToFileURL(parentURL).href,
2058-
parentPath: parentURL,
2059-
};
2060-
}
2061-
20622054
const url = URLParse(parentURL);
20632055
if (!url) {
20642056
throw new ERR_INVALID_ARG_VALUE('options.parentURL', parentURL,
2065-
'must be an absolute path or URL');
2057+
'must be a URL');
20662058
}
20672059

20682060
const parentPath =
@@ -2122,7 +2114,11 @@ function resolveClearCacheFilename(specifier, parentPath) {
21222114
}
21232115

21242116
const parent = parentPath ? createParentModuleForClearCache(parentPath) : null;
2125-
return Module._resolveFilename(request, parent, false);
2117+
const { filename, format } = resolveForCJSWithHooks(request, parent, false, false);
2118+
if (format === 'builtin') {
2119+
return null;
2120+
}
2121+
return filename;
21262122
}
21272123

21282124
/**
@@ -2156,17 +2152,20 @@ function resolveClearCacheURL(specifier, parentURL, importAttributes) {
21562152
/**
21572153
* Remove path cache entries that resolve to a filename.
21582154
* @param {string} filename
2155+
* @param {Set<string>|null} [existingKeys]
21592156
* @returns {boolean} true if any entries were deleted.
21602157
*/
2161-
function deletePathCacheEntries(filename) {
2158+
function deletePathCacheEntries(filename, existingKeys = null) {
21622159
const cache = Module._pathCache;
21632160
const keys = ObjectKeys(cache);
21642161
let deleted = false;
21652162
for (let i = 0; i < keys.length; i++) {
21662163
const key = keys[i];
21672164
if (cache[key] === filename) {
2165+
if (existingKeys === null || existingKeys.has(key)) {
2166+
deleted = true;
2167+
}
21682168
delete cache[key];
2169-
deleted = true;
21702169
}
21712170
}
21722171
return deleted;
@@ -2175,91 +2174,179 @@ function deletePathCacheEntries(filename) {
21752174
/**
21762175
* Remove relative resolve cache entries that resolve to a filename.
21772176
* @param {string} filename
2177+
* @param {Set<string>|null} [existingKeys]
21782178
* @returns {boolean} true if any entries were deleted.
21792179
*/
2180-
function deleteRelativeResolveCacheEntries(filename) {
2180+
function deleteRelativeResolveCacheEntries(filename, existingKeys = null) {
21812181
const keys = ObjectKeys(relativeResolveCache);
21822182
let deleted = false;
21832183
for (let i = 0; i < keys.length; i++) {
21842184
const key = keys[i];
21852185
if (relativeResolveCache[key] === filename) {
2186+
if (existingKeys === null || existingKeys.has(key)) {
2187+
deleted = true;
2188+
}
21862189
delete relativeResolveCache[key];
2190+
}
2191+
}
2192+
return deleted;
2193+
}
2194+
2195+
/**
2196+
* Remove cached module references from parent children arrays.
2197+
* @param {Module} targetModule
2198+
* @returns {boolean} true if any references were removed.
2199+
*/
2200+
function deleteModuleFromParents(targetModule) {
2201+
const keys = ObjectKeys(Module._cache);
2202+
let deleted = false;
2203+
for (let i = 0; i < keys.length; i++) {
2204+
const cachedModule = Module._cache[keys[i]];
2205+
const children = cachedModule?.children;
2206+
if (!ArrayIsArray(children)) {
2207+
continue;
2208+
}
2209+
const index = ArrayPrototypeIndexOf(children, targetModule);
2210+
if (index !== -1) {
2211+
ArrayPrototypeSplice(children, index, 1);
21872212
deleted = true;
21882213
}
21892214
}
21902215
return deleted;
21912216
}
21922217

2218+
/**
2219+
* Resolve a file path for a file URL, stripping search/hash.
2220+
* @param {string} url
2221+
* @returns {string|null}
2222+
*/
2223+
function getFilePathFromClearCacheURL(url) {
2224+
const parsedURL = URLParse(url);
2225+
if (!parsedURL || parsedURL.protocol !== 'file:') {
2226+
return null;
2227+
}
2228+
2229+
if (parsedURL.search !== '' || parsedURL.hash !== '') {
2230+
parsedURL.search = '';
2231+
parsedURL.hash = '';
2232+
}
2233+
2234+
try {
2235+
return fileURLToPath(parsedURL);
2236+
} catch {
2237+
return null;
2238+
}
2239+
}
2240+
2241+
/**
2242+
* Remove load cache entries for a URL and its file-path variants.
2243+
* @param {import('internal/modules/esm/module_map').LoadCache} loadCache
2244+
* @param {string} url
2245+
* @returns {boolean} true if any entries were deleted.
2246+
*/
2247+
function deleteLoadCacheEntries(loadCache, url) {
2248+
let deleted = loadCache.deleteAll(url);
2249+
const filename = getFilePathFromClearCacheURL(url);
2250+
if (!filename) {
2251+
return deleted;
2252+
}
2253+
2254+
const urls = [];
2255+
for (const entry of loadCache) {
2256+
ArrayPrototypePush(urls, entry[0]);
2257+
}
2258+
2259+
for (let i = 0; i < urls.length; i++) {
2260+
const cachedURL = urls[i];
2261+
if (cachedURL === url) {
2262+
continue;
2263+
}
2264+
const cachedFilename = getFilePathFromClearCacheURL(cachedURL);
2265+
if (cachedFilename === filename) {
2266+
loadCache.deleteAll(cachedURL);
2267+
deleted = true;
2268+
}
2269+
}
2270+
2271+
return deleted;
2272+
}
2273+
21932274
/**
21942275
* Clear CommonJS and/or ESM module cache entries.
21952276
* @param {string|URL} specifier
21962277
* @param {object} [options]
2197-
* @param {'all'|'cjs'|'esm'} [options.mode]
2278+
* @param {'all'|'commonjs'|'module'} [options.mode]
21982279
* @param {string|URL} [options.parentURL]
2199-
* @param {string} [options.type]
22002280
* @param {Record<string, string>} [options.importAttributes]
2201-
* @returns {{ cjs: boolean, esm: boolean }}
2281+
* @returns {{ commonjs: boolean, module: boolean }}
22022282
*/
22032283
function clearCache(specifier, options = kEmptyObject) {
22042284
const isSpecifierURL = isURL(specifier);
22052285
if (!isSpecifierURL) {
22062286
validateString(specifier, 'specifier');
22072287
}
2288+
const specifierKey = isSpecifierURL ? specifier.href : specifier;
22082289

22092290
validateObject(options, 'options');
22102291
const mode = options.mode === undefined ? 'all' : options.mode;
2211-
validateOneOf(mode, 'options.mode', ['all', 'cjs', 'esm']);
2212-
2213-
if (options.importAttributes !== undefined && options.type !== undefined) {
2214-
throw new ERR_INVALID_ARG_VALUE('options.importAttributes', options.importAttributes,
2215-
'cannot be used with options.type');
2216-
}
2292+
validateOneOf(mode, 'options.mode', ['all', 'commonjs', 'module']);
22172293

2218-
let importAttributes = options.importAttributes;
2219-
if (options.type !== undefined) {
2220-
validateString(options.type, 'options.type');
2221-
importAttributes = { __proto__: null, type: options.type };
2222-
} else if (importAttributes !== undefined) {
2294+
const importAttributes = options.importAttributes;
2295+
if (importAttributes !== undefined) {
22232296
validateObject(importAttributes, 'options.importAttributes');
22242297
}
22252298

22262299
const { parentURL, parentPath } = normalizeClearCacheParent(options.parentURL);
2227-
const result = { __proto__: null, cjs: false, esm: false };
2300+
const result = { __proto__: null, commonjs: false, module: false };
22282301

2229-
if (mode !== 'esm') {
2302+
if (mode !== 'module') {
2303+
const pathCacheKeys = new SafeSet(ObjectKeys(Module._pathCache));
2304+
const relativeResolveCacheKeys = new SafeSet(ObjectKeys(relativeResolveCache));
22302305
try {
22312306
const filename = resolveClearCacheFilename(specifier, parentPath);
22322307
if (filename) {
22332308
let deleted = false;
2234-
if (Module._cache[filename] !== undefined) {
2309+
const cachedModule = Module._cache[filename];
2310+
if (cachedModule !== undefined) {
22352311
delete Module._cache[filename];
22362312
deleted = true;
2313+
if (deleteModuleFromParents(cachedModule)) {
2314+
deleted = true;
2315+
}
22372316
}
2238-
if (deletePathCacheEntries(filename)) {
2317+
if (deletePathCacheEntries(filename, pathCacheKeys)) {
22392318
deleted = true;
22402319
}
2241-
if (deleteRelativeResolveCacheEntries(filename)) {
2320+
if (deleteRelativeResolveCacheEntries(filename, relativeResolveCacheKeys)) {
22422321
deleted = true;
22432322
}
2244-
result.cjs = deleted;
2323+
result.commonjs = deleted;
22452324
}
22462325
} catch (err) {
2247-
if (mode === 'cjs') {
2326+
if (mode === 'commonjs') {
22482327
throw err;
22492328
}
22502329
}
22512330
}
22522331

2253-
if (mode !== 'cjs') {
2332+
if (mode !== 'commonjs') {
22542333
try {
22552334
const url = resolveClearCacheURL(specifier, parentURL, importAttributes);
22562335
const cascadedLoader =
22572336
require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
2258-
const loadDeleted = cascadedLoader.loadCache.deleteAll(url);
2259-
const resolveDeleted = cascadedLoader.deleteResolveCache(url);
2260-
result.esm = loadDeleted || resolveDeleted;
2337+
const loadDeleted = deleteLoadCacheEntries(cascadedLoader.loadCache, url);
2338+
let resolveDeleted = cascadedLoader.deleteResolveCacheEntry(
2339+
specifierKey,
2340+
parentURL,
2341+
importAttributes ?? kEmptyObject,
2342+
);
2343+
const resolvedPath = getFilePathFromClearCacheURL(url);
2344+
if (resolvedPath) {
2345+
resolveDeleted = cascadedLoader.deleteResolveCacheByFilename(resolvedPath) || resolveDeleted;
2346+
}
2347+
result.module = loadDeleted || resolveDeleted;
22612348
} catch (err) {
2262-
if (mode === 'esm') {
2349+
if (mode === 'module') {
22632350
throw err;
22642351
}
22652352
}

lib/internal/modules/esm/loader.js

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const {
66
ArrayPrototypeReduce,
77
FunctionPrototypeCall,
88
JSONStringify,
9+
ObjectKeys,
910
ObjectSetPrototypeOf,
1011
Promise,
1112
PromisePrototypeThen,
@@ -30,7 +31,7 @@ const {
3031
ERR_UNKNOWN_MODULE_FORMAT,
3132
} = require('internal/errors').codes;
3233
const { getOptionValue } = require('internal/options');
33-
const { isURL, pathToFileURL } = require('internal/url');
34+
const { isURL, pathToFileURL, fileURLToPath, URLParse } = require('internal/url');
3435
const { kEmptyObject } = require('internal/util');
3536
const {
3637
compileSourceTextModule,
@@ -190,6 +191,61 @@ class ModuleLoader {
190191
return this.#resolveCache.deleteByResolvedURL(url);
191192
}
192193

194+
/**
195+
* Delete cached resolution for a specific request.
196+
* @param {string} specifier
197+
* @param {string|undefined} parentURL
198+
* @param {Record<string, string>} importAttributes
199+
* @returns {boolean} true if any entries were deleted.
200+
*/
201+
deleteResolveCacheEntry(specifier, parentURL, importAttributes) {
202+
return this.#resolveCache.deleteBySpecifier(specifier, parentURL, importAttributes);
203+
}
204+
205+
/**
206+
* Delete cached resolutions that resolve to a file path.
207+
* @param {string} filename
208+
* @returns {boolean} true if any entries were deleted.
209+
*/
210+
deleteResolveCacheByFilename(filename) {
211+
let deleted = false;
212+
for (const entry of this.#resolveCache) {
213+
const parentURL = entry[0];
214+
const entries = entry[1];
215+
const keys = ObjectKeys(entries);
216+
for (let i = 0; i < keys.length; i++) {
217+
const key = keys[i];
218+
const resolvedURL = entries[key]?.url;
219+
if (!resolvedURL) {
220+
continue;
221+
}
222+
const parsedURL = URLParse(resolvedURL);
223+
if (!parsedURL || parsedURL.protocol !== 'file:') {
224+
continue;
225+
}
226+
if (parsedURL.search !== '' || parsedURL.hash !== '') {
227+
parsedURL.search = '';
228+
parsedURL.hash = '';
229+
}
230+
let resolvedFilename;
231+
try {
232+
resolvedFilename = fileURLToPath(parsedURL);
233+
} catch {
234+
continue;
235+
}
236+
if (resolvedFilename === filename) {
237+
delete entries[key];
238+
deleted = true;
239+
}
240+
}
241+
242+
if (ObjectKeys(entries).length === 0) {
243+
this.#resolveCache.delete(parentURL);
244+
}
245+
}
246+
return deleted;
247+
}
248+
193249
/**
194250
* Methods which translate input code or other information into ES modules
195251
*/

0 commit comments

Comments
 (0)