Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions doc/api/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,52 @@ const require = createRequire(import.meta.url);
const siblingModule = require('./sibling-module');
```
### `module.clearCache(specifier[, options])`
<!-- YAML
added: REPLACEME
-->
> Stability: 1.1 - Active development
* `specifier` {string|URL} The module specifier or URL to clear.
Copy link
Member

@joyeecheung joyeecheung Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the resolution and loading cache should be separated instead of being mixed together:

  1. For resolution, the specifier makes a bit more sense (together with other parameters, though)
  2. For loading, this does not make sense. Multiple specifiers can map to the same resolved filename/URL when they come from different modules, clearing one without syncing another might be problematic.

Other than that, there is also the question of CommonJS/ESM cache complexity that isn't really surfaced in the current API: CommonJS uses filename as loading cache keys while ESM uses URLs, it gets even hairier when import cjs and require(esm) happens, because you can have multiple file URLs mapping to the same file path with search parameters (more of a problem for import cjs). This may not play well with how hot module reloading solutions import a full URL with changing search parameters in facade modules - if you don't iterate over all the variants with different search parameters, it will still leak. I am not sure what's the best way forward, though, maybe maintainers of these modules would know what would work for them in this case?

* `options` {Object}
* `mode` {string} Which caches to clear. Supported values are `'all'`, `'cjs'`, and `'esm'`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the module namespace, these are generally called commonjs and module in string constants. cjs and esm generally are only used in informal prose, and if people meant to pass e.g. variables from package.json fields around, they have to convert.

**Default:** `'all'`.
* `parentURL` {string|URL} The parent URL or absolute path used to resolve non-URL specifiers.
For CommonJS, pass `__filename`. For ES modules, pass `import.meta.url`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think accepting filename as parentURL doesn't make too much sense. It would be better to just stick to URL, a commonJS can still convert the file path into an URL.

* `type` {string} Import attributes `type` used for ESM resolution.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this necessary if we also accept importAttributes? What happens if we start to accept more than just type?

* `importAttributes` {Object} Import attributes for ESM resolution. Cannot be used with `type`.
* Returns: {Object} An object with `{ cjs: boolean, esm: boolean }` indicating whether entries
were removed from each cache.
Clears the CommonJS `require` cache and/or the ESM module cache for a module. This enables
reload patterns similar to deleting from `require.cache` in CommonJS, and is useful for HMR.
When `mode` is `'all'`, resolution failures for one module system do not throw; check the
returned flags to see what was cleared.
This also clears internal resolution caches for the resolved module.
```mjs
import { clearCache } from 'node:module';

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

clearCache(url);
await import(url.href); // re-executes the module
```
```cjs
const { clearCache } = require('node:module');
const path = require('node:path');

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

clearCache(file);
require(file); // re-executes the module
```
### `module.findPackageJSON(specifier[, base])`
<!-- YAML
Expand Down
245 changes: 243 additions & 2 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ const { BuiltinModule } = require('internal/bootstrap/realm');
const {
maybeCacheSourceMap,
} = require('internal/source_map/source_map_cache');
const { pathToFileURL, fileURLToPath, isURL, URL } = require('internal/url');
const { pathToFileURL, fileURLToPath, isURL, URL, URLParse } = require('internal/url');
const {
pendingDeprecate,
emitExperimentalWarning,
Expand Down Expand Up @@ -200,7 +200,11 @@ const {
},
setArrowMessage,
} = require('internal/errors');
const { validateString } = require('internal/validators');
const {
validateObject,
validateOneOf,
validateString,
} = require('internal/validators');

const {
CHAR_BACKWARD_SLASH,
Expand Down Expand Up @@ -2028,6 +2032,242 @@ function createRequire(filenameOrURL) {
return createRequireFromPath(filepath, fileURL);
}

/**
* Normalize the parent URL/path for cache clearing.
* @param {string|URL|undefined} parentURL
* @returns {{ parentURL: string|undefined, parentPath: string|undefined }}
*/
function normalizeClearCacheParent(parentURL) {
if (parentURL === undefined) {
return { __proto__: null, parentURL: undefined, parentPath: undefined };
}

if (isURL(parentURL)) {
const parentPath =
parentURL.protocol === 'file:' && parentURL.search === '' && parentURL.hash === '' ?
fileURLToPath(parentURL) :
undefined;
return { __proto__: null, parentURL: parentURL.href, parentPath };
}

validateString(parentURL, 'options.parentURL');
if (path.isAbsolute(parentURL)) {
return {
__proto__: null,
parentURL: pathToFileURL(parentURL).href,
parentPath: parentURL,
};
}

const url = URLParse(parentURL);
if (!url) {
throw new ERR_INVALID_ARG_VALUE('options.parentURL', parentURL,
'must be an absolute path or URL');
}

const parentPath =
url.protocol === 'file:' && url.search === '' && url.hash === '' ?
fileURLToPath(url) :
undefined;
return { __proto__: null, parentURL: url.href, parentPath };
}

/**
* Parse a specifier as a URL when possible.
* @param {string|URL} specifier
* @returns {URL|null}
*/
function getURLFromClearCacheSpecifier(specifier) {
if (isURL(specifier)) {
return specifier;
}

if (typeof specifier !== 'string' || path.isAbsolute(specifier)) {
return null;
}

return URLParse(specifier) ?? null;
}

/**
* Create a synthetic parent module for CJS resolution.
* @param {string} parentPath
* @returns {Module}
*/
function createParentModuleForClearCache(parentPath) {
const parent = new Module(parentPath);
parent.filename = parentPath;
parent.paths = Module._nodeModulePaths(path.dirname(parentPath));
return parent;
}

/**
* Resolve a cache filename for CommonJS.
* @param {string|URL} specifier
* @param {string|undefined} parentPath
* @returns {string|null}
*/
function resolveClearCacheFilename(specifier, parentPath) {
if (!parentPath && typeof specifier === 'string' && isRelative(specifier)) {
return null;
}

const parsedURL = getURLFromClearCacheSpecifier(specifier);
let request = specifier;
if (parsedURL) {
if (parsedURL.protocol !== 'file:' || parsedURL.search !== '' || parsedURL.hash !== '') {
return null;
}
request = fileURLToPath(parsedURL);
}

const parent = parentPath ? createParentModuleForClearCache(parentPath) : null;
return Module._resolveFilename(request, parent, false);
Copy link
Member

@joyeecheung joyeecheung Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this would work with module.register or module.registerHooks? This can be problematic if, for example, the application is using a hook to avoid reading from the file system directly but instead from an archive (I think that's what yarn pnp does, for example), and the user of clear cache then try to wipe the cache of a module sourced from the disk, then the cache clearing would error during resolution and not work.

}

/**
* Resolve a cache URL for ESM.
* @param {string|URL} specifier
* @param {string|undefined} parentURL
* @param {Record<string, string>|undefined} importAttributes
* @returns {string}
*/
function resolveClearCacheURL(specifier, parentURL, importAttributes) {
const parsedURL = getURLFromClearCacheSpecifier(specifier);
if (parsedURL) {
return parsedURL.href;
}

if (path.isAbsolute(specifier)) {
return pathToFileURL(specifier).href;
}

if (parentURL === undefined) {
throw new ERR_INVALID_ARG_VALUE('options.parentURL', parentURL,
'must be provided for non-URL ESM specifiers');
}

const cascadedLoader =
require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
const request = { specifier, attributes: importAttributes, __proto__: null };
return cascadedLoader.resolveSync(parentURL, request).url;
}

/**
* Remove path cache entries that resolve to a filename.
* @param {string} filename
* @returns {boolean} true if any entries were deleted.
*/
function deletePathCacheEntries(filename) {
const cache = Module._pathCache;
const keys = ObjectKeys(cache);
let deleted = false;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (cache[key] === filename) {
delete cache[key];
deleted = true;
}
}
return deleted;
}

/**
* Remove relative resolve cache entries that resolve to a filename.
* @param {string} filename
* @returns {boolean} true if any entries were deleted.
*/
function deleteRelativeResolveCacheEntries(filename) {
const keys = ObjectKeys(relativeResolveCache);
let deleted = false;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (relativeResolveCache[key] === filename) {
delete relativeResolveCache[key];
deleted = true;
}
}
return deleted;
}

/**
* Clear CommonJS and/or ESM module cache entries.
* @param {string|URL} specifier
* @param {object} [options]
* @param {'all'|'cjs'|'esm'} [options.mode]
* @param {string|URL} [options.parentURL]
* @param {string} [options.type]
* @param {Record<string, string>} [options.importAttributes]
* @returns {{ cjs: boolean, esm: boolean }}
*/
function clearCache(specifier, options = kEmptyObject) {
const isSpecifierURL = isURL(specifier);
if (!isSpecifierURL) {
validateString(specifier, 'specifier');
}

validateObject(options, 'options');
const mode = options.mode === undefined ? 'all' : options.mode;
validateOneOf(mode, 'options.mode', ['all', 'cjs', 'esm']);

if (options.importAttributes !== undefined && options.type !== undefined) {
throw new ERR_INVALID_ARG_VALUE('options.importAttributes', options.importAttributes,
'cannot be used with options.type');
}

let importAttributes = options.importAttributes;
if (options.type !== undefined) {
validateString(options.type, 'options.type');
importAttributes = { __proto__: null, type: options.type };
} else if (importAttributes !== undefined) {
validateObject(importAttributes, 'options.importAttributes');
}

const { parentURL, parentPath } = normalizeClearCacheParent(options.parentURL);
const result = { __proto__: null, cjs: false, esm: false };

if (mode !== 'esm') {
try {
const filename = resolveClearCacheFilename(specifier, parentPath);
if (filename) {
let deleted = false;
if (Module._cache[filename] !== undefined) {
delete Module._cache[filename];
Copy link
Member

@joyeecheung joyeecheung Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is exhibiting a very common mistake of CommonJS cache clearing - you must also delete it from the children arrays of every module that loads it. It otherwise always leaks and is worse than most user-land solutions that takes care of it (or if somehow one module uses this API and another uses a working user-land solution, and they end up in the same process, this now defeats that working user-land solution because they can no longer purge the children references).

deleted = true;
}
if (deletePathCacheEntries(filename)) {
deleted = true;
}
if (deleteRelativeResolveCacheEntries(filename)) {
deleted = true;
}
result.cjs = deleted;
}
} catch (err) {
if (mode === 'cjs') {
throw err;
}
}
}

if (mode !== 'cjs') {
try {
const url = resolveClearCacheURL(specifier, parentURL, importAttributes);
const cascadedLoader =
require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
const loadDeleted = cascadedLoader.loadCache.deleteAll(url);
const resolveDeleted = cascadedLoader.deleteResolveCache(url);
result.esm = loadDeleted || resolveDeleted;
} catch (err) {
if (mode === 'esm') {
throw err;
}
}
}

return result;
}

/**
* Checks if a path is relative
* @param {string} path the target path
Expand All @@ -2044,6 +2284,7 @@ function isRelative(path) {
}

Module.createRequire = createRequire;
Module.clearCache = clearCache;

/**
* Define the paths to use for resolving a module.
Expand Down
9 changes: 9 additions & 0 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,15 @@ class ModuleLoader {
*/
loadCache = newLoadCache();

/**
* Delete cached resolutions that resolve to a URL.
* @param {string} url
* @returns {boolean} true if any entries were deleted.
*/
deleteResolveCache(url) {
return this.#resolveCache.deleteByResolvedURL(url);
}

/**
* Methods which translate input code or other information into ES modules
*/
Expand Down
37 changes: 37 additions & 0 deletions lib/internal/modules/esm/module_map.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,33 @@ class ResolveCache extends SafeMap {
has(serializedKey, parentURL) {
return serializedKey in this.#getModuleCachedImports(parentURL);
}

/**
* Delete cached resolutions that resolve to a URL.
* @param {string} url
* @returns {boolean} true if any entries were deleted.
*/
deleteByResolvedURL(url) {
validateString(url, 'url');
let deleted = false;
for (const entry of this) {
const parentURL = entry[0];
const entries = entry[1];
const keys = ObjectKeys(entries);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (entries[key]?.url === url) {
delete entries[key];
deleted = true;
}
}

if (ObjectKeys(entries).length === 0) {
super.delete(parentURL);
}
}
return deleted;
}
}

/**
Expand Down Expand Up @@ -123,6 +150,16 @@ class LoadCache extends SafeMap {
cached[type] = undefined;
}
}

/**
* Delete all cached module jobs for a URL.
* @param {string} url
* @returns {boolean} true if an entry was deleted.
*/
deleteAll(url) {
validateString(url, 'url');
return super.delete(url);
}
}

module.exports = {
Expand Down
Loading
Loading