Skip to content

Commit 723300c

Browse files
committed
fixup! module: add clearCache for CJS and ESM
1 parent f7c826e commit 723300c

14 files changed

Lines changed: 382 additions & 56 deletions

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
@@ -2042,7 +2042,7 @@ function createRequire(filenameOrURL) {
20422042
}
20432043

20442044
/**
2045-
* Normalize the parent URL/path for cache clearing.
2045+
* Normalize the parent URL for cache clearing.
20462046
* @param {string|URL|undefined} parentURL
20472047
* @returns {{ parentURL: string|undefined, parentPath: string|undefined }}
20482048
*/
@@ -2060,18 +2060,10 @@ function normalizeClearCacheParent(parentURL) {
20602060
}
20612061

20622062
validateString(parentURL, 'options.parentURL');
2063-
if (path.isAbsolute(parentURL)) {
2064-
return {
2065-
__proto__: null,
2066-
parentURL: pathToFileURL(parentURL).href,
2067-
parentPath: parentURL,
2068-
};
2069-
}
2070-
20712063
const url = URLParse(parentURL);
20722064
if (!url) {
20732065
throw new ERR_INVALID_ARG_VALUE('options.parentURL', parentURL,
2074-
'must be an absolute path or URL');
2066+
'must be a URL');
20752067
}
20762068

20772069
const parentPath =
@@ -2131,7 +2123,11 @@ function resolveClearCacheFilename(specifier, parentPath) {
21312123
}
21322124

21332125
const parent = parentPath ? createParentModuleForClearCache(parentPath) : null;
2134-
return Module._resolveFilename(request, parent, false);
2126+
const { filename, format } = resolveForCJSWithHooks(request, parent, false, false);
2127+
if (format === 'builtin') {
2128+
return null;
2129+
}
2130+
return filename;
21352131
}
21362132

21372133
/**
@@ -2165,17 +2161,20 @@ function resolveClearCacheURL(specifier, parentURL, importAttributes) {
21652161
/**
21662162
* Remove path cache entries that resolve to a filename.
21672163
* @param {string} filename
2164+
* @param {Set<string>|null} [existingKeys]
21682165
* @returns {boolean} true if any entries were deleted.
21692166
*/
2170-
function deletePathCacheEntries(filename) {
2167+
function deletePathCacheEntries(filename, existingKeys = null) {
21712168
const cache = Module._pathCache;
21722169
const keys = ObjectKeys(cache);
21732170
let deleted = false;
21742171
for (let i = 0; i < keys.length; i++) {
21752172
const key = keys[i];
21762173
if (cache[key] === filename) {
2174+
if (existingKeys === null || existingKeys.has(key)) {
2175+
deleted = true;
2176+
}
21772177
delete cache[key];
2178-
deleted = true;
21792178
}
21802179
}
21812180
return deleted;
@@ -2184,91 +2183,179 @@ function deletePathCacheEntries(filename) {
21842183
/**
21852184
* Remove relative resolve cache entries that resolve to a filename.
21862185
* @param {string} filename
2186+
* @param {Set<string>|null} [existingKeys]
21872187
* @returns {boolean} true if any entries were deleted.
21882188
*/
2189-
function deleteRelativeResolveCacheEntries(filename) {
2189+
function deleteRelativeResolveCacheEntries(filename, existingKeys = null) {
21902190
const keys = ObjectKeys(relativeResolveCache);
21912191
let deleted = false;
21922192
for (let i = 0; i < keys.length; i++) {
21932193
const key = keys[i];
21942194
if (relativeResolveCache[key] === filename) {
2195+
if (existingKeys === null || existingKeys.has(key)) {
2196+
deleted = true;
2197+
}
21952198
delete relativeResolveCache[key];
2199+
}
2200+
}
2201+
return deleted;
2202+
}
2203+
2204+
/**
2205+
* Remove cached module references from parent children arrays.
2206+
* @param {Module} targetModule
2207+
* @returns {boolean} true if any references were removed.
2208+
*/
2209+
function deleteModuleFromParents(targetModule) {
2210+
const keys = ObjectKeys(Module._cache);
2211+
let deleted = false;
2212+
for (let i = 0; i < keys.length; i++) {
2213+
const cachedModule = Module._cache[keys[i]];
2214+
const children = cachedModule?.children;
2215+
if (!ArrayIsArray(children)) {
2216+
continue;
2217+
}
2218+
const index = ArrayPrototypeIndexOf(children, targetModule);
2219+
if (index !== -1) {
2220+
ArrayPrototypeSplice(children, index, 1);
21962221
deleted = true;
21972222
}
21982223
}
21992224
return deleted;
22002225
}
22012226

2227+
/**
2228+
* Resolve a file path for a file URL, stripping search/hash.
2229+
* @param {string} url
2230+
* @returns {string|null}
2231+
*/
2232+
function getFilePathFromClearCacheURL(url) {
2233+
const parsedURL = URLParse(url);
2234+
if (!parsedURL || parsedURL.protocol !== 'file:') {
2235+
return null;
2236+
}
2237+
2238+
if (parsedURL.search !== '' || parsedURL.hash !== '') {
2239+
parsedURL.search = '';
2240+
parsedURL.hash = '';
2241+
}
2242+
2243+
try {
2244+
return fileURLToPath(parsedURL);
2245+
} catch {
2246+
return null;
2247+
}
2248+
}
2249+
2250+
/**
2251+
* Remove load cache entries for a URL and its file-path variants.
2252+
* @param {import('internal/modules/esm/module_map').LoadCache} loadCache
2253+
* @param {string} url
2254+
* @returns {boolean} true if any entries were deleted.
2255+
*/
2256+
function deleteLoadCacheEntries(loadCache, url) {
2257+
let deleted = loadCache.deleteAll(url);
2258+
const filename = getFilePathFromClearCacheURL(url);
2259+
if (!filename) {
2260+
return deleted;
2261+
}
2262+
2263+
const urls = [];
2264+
for (const entry of loadCache) {
2265+
ArrayPrototypePush(urls, entry[0]);
2266+
}
2267+
2268+
for (let i = 0; i < urls.length; i++) {
2269+
const cachedURL = urls[i];
2270+
if (cachedURL === url) {
2271+
continue;
2272+
}
2273+
const cachedFilename = getFilePathFromClearCacheURL(cachedURL);
2274+
if (cachedFilename === filename) {
2275+
loadCache.deleteAll(cachedURL);
2276+
deleted = true;
2277+
}
2278+
}
2279+
2280+
return deleted;
2281+
}
2282+
22022283
/**
22032284
* Clear CommonJS and/or ESM module cache entries.
22042285
* @param {string|URL} specifier
22052286
* @param {object} [options]
2206-
* @param {'all'|'cjs'|'esm'} [options.mode]
2287+
* @param {'all'|'commonjs'|'module'} [options.mode]
22072288
* @param {string|URL} [options.parentURL]
2208-
* @param {string} [options.type]
22092289
* @param {Record<string, string>} [options.importAttributes]
2210-
* @returns {{ cjs: boolean, esm: boolean }}
2290+
* @returns {{ commonjs: boolean, module: boolean }}
22112291
*/
22122292
function clearCache(specifier, options = kEmptyObject) {
22132293
const isSpecifierURL = isURL(specifier);
22142294
if (!isSpecifierURL) {
22152295
validateString(specifier, 'specifier');
22162296
}
2297+
const specifierKey = isSpecifierURL ? specifier.href : specifier;
22172298

22182299
validateObject(options, 'options');
22192300
const mode = options.mode === undefined ? 'all' : options.mode;
2220-
validateOneOf(mode, 'options.mode', ['all', 'cjs', 'esm']);
2221-
2222-
if (options.importAttributes !== undefined && options.type !== undefined) {
2223-
throw new ERR_INVALID_ARG_VALUE('options.importAttributes', options.importAttributes,
2224-
'cannot be used with options.type');
2225-
}
2301+
validateOneOf(mode, 'options.mode', ['all', 'commonjs', 'module']);
22262302

2227-
let importAttributes = options.importAttributes;
2228-
if (options.type !== undefined) {
2229-
validateString(options.type, 'options.type');
2230-
importAttributes = { __proto__: null, type: options.type };
2231-
} else if (importAttributes !== undefined) {
2303+
const importAttributes = options.importAttributes;
2304+
if (importAttributes !== undefined) {
22322305
validateObject(importAttributes, 'options.importAttributes');
22332306
}
22342307

22352308
const { parentURL, parentPath } = normalizeClearCacheParent(options.parentURL);
2236-
const result = { __proto__: null, cjs: false, esm: false };
2309+
const result = { __proto__: null, commonjs: false, module: false };
22372310

2238-
if (mode !== 'esm') {
2311+
if (mode !== 'module') {
2312+
const pathCacheKeys = new SafeSet(ObjectKeys(Module._pathCache));
2313+
const relativeResolveCacheKeys = new SafeSet(ObjectKeys(relativeResolveCache));
22392314
try {
22402315
const filename = resolveClearCacheFilename(specifier, parentPath);
22412316
if (filename) {
22422317
let deleted = false;
2243-
if (Module._cache[filename] !== undefined) {
2318+
const cachedModule = Module._cache[filename];
2319+
if (cachedModule !== undefined) {
22442320
delete Module._cache[filename];
22452321
deleted = true;
2322+
if (deleteModuleFromParents(cachedModule)) {
2323+
deleted = true;
2324+
}
22462325
}
2247-
if (deletePathCacheEntries(filename)) {
2326+
if (deletePathCacheEntries(filename, pathCacheKeys)) {
22482327
deleted = true;
22492328
}
2250-
if (deleteRelativeResolveCacheEntries(filename)) {
2329+
if (deleteRelativeResolveCacheEntries(filename, relativeResolveCacheKeys)) {
22512330
deleted = true;
22522331
}
2253-
result.cjs = deleted;
2332+
result.commonjs = deleted;
22542333
}
22552334
} catch (err) {
2256-
if (mode === 'cjs') {
2335+
if (mode === 'commonjs') {
22572336
throw err;
22582337
}
22592338
}
22602339
}
22612340

2262-
if (mode !== 'cjs') {
2341+
if (mode !== 'commonjs') {
22632342
try {
22642343
const url = resolveClearCacheURL(specifier, parentURL, importAttributes);
22652344
const cascadedLoader =
22662345
require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
2267-
const loadDeleted = cascadedLoader.loadCache.deleteAll(url);
2268-
const resolveDeleted = cascadedLoader.deleteResolveCache(url);
2269-
result.esm = loadDeleted || resolveDeleted;
2346+
const loadDeleted = deleteLoadCacheEntries(cascadedLoader.loadCache, url);
2347+
let resolveDeleted = cascadedLoader.deleteResolveCacheEntry(
2348+
specifierKey,
2349+
parentURL,
2350+
importAttributes ?? kEmptyObject,
2351+
);
2352+
const resolvedPath = getFilePathFromClearCacheURL(url);
2353+
if (resolvedPath) {
2354+
resolveDeleted = cascadedLoader.deleteResolveCacheByFilename(resolvedPath) || resolveDeleted;
2355+
}
2356+
result.module = loadDeleted || resolveDeleted;
22702357
} catch (err) {
2271-
if (mode === 'esm') {
2358+
if (mode === 'module') {
22722359
throw err;
22732360
}
22742361
}

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,
@@ -178,6 +179,61 @@ class ModuleLoader {
178179
return this.#resolveCache.deleteByResolvedURL(url);
179180
}
180181

182+
/**
183+
* Delete cached resolution for a specific request.
184+
* @param {string} specifier
185+
* @param {string|undefined} parentURL
186+
* @param {Record<string, string>} importAttributes
187+
* @returns {boolean} true if any entries were deleted.
188+
*/
189+
deleteResolveCacheEntry(specifier, parentURL, importAttributes) {
190+
return this.#resolveCache.deleteBySpecifier(specifier, parentURL, importAttributes);
191+
}
192+
193+
/**
194+
* Delete cached resolutions that resolve to a file path.
195+
* @param {string} filename
196+
* @returns {boolean} true if any entries were deleted.
197+
*/
198+
deleteResolveCacheByFilename(filename) {
199+
let deleted = false;
200+
for (const entry of this.#resolveCache) {
201+
const parentURL = entry[0];
202+
const entries = entry[1];
203+
const keys = ObjectKeys(entries);
204+
for (let i = 0; i < keys.length; i++) {
205+
const key = keys[i];
206+
const resolvedURL = entries[key]?.url;
207+
if (!resolvedURL) {
208+
continue;
209+
}
210+
const parsedURL = URLParse(resolvedURL);
211+
if (!parsedURL || parsedURL.protocol !== 'file:') {
212+
continue;
213+
}
214+
if (parsedURL.search !== '' || parsedURL.hash !== '') {
215+
parsedURL.search = '';
216+
parsedURL.hash = '';
217+
}
218+
let resolvedFilename;
219+
try {
220+
resolvedFilename = fileURLToPath(parsedURL);
221+
} catch {
222+
continue;
223+
}
224+
if (resolvedFilename === filename) {
225+
delete entries[key];
226+
deleted = true;
227+
}
228+
}
229+
230+
if (ObjectKeys(entries).length === 0) {
231+
this.#resolveCache.delete(parentURL);
232+
}
233+
}
234+
return deleted;
235+
}
236+
181237
/**
182238
* @see {AsyncLoaderHooks.isForAsyncLoaderHookWorker}
183239
* Shortcut to this.#asyncLoaderHooks.isForAsyncLoaderHookWorker.

0 commit comments

Comments
 (0)