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
130 changes: 130 additions & 0 deletions doc/api/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,135 @@ const require = createRequire(import.meta.url);
const siblingModule = require('./sibling-module');
```

### `module.clearCache(specifier, options)`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.0 - Early development

* `specifier` {string|URL} The module specifier, as it would have been passed to
`import()` or `require()`.
* `options` {Object}
* `parentURL` {string|URL} The parent URL used to resolve the specifier. Parent identity
is part of the resolution cache key. For CommonJS, pass `pathToFileURL(__filename)`.
For ES modules, pass `import.meta.url`.
* `resolver` {string} Specifies how resolution should be performed. Must be either
`'import'` or `'require'`.
* `importAttributes` {Object} Optional import attributes. Only meaningful when
`resolver` is `'import'`.

Clears the module resolution and module caches for a module. This enables
reload patterns similar to deleting from `require.cache` in CommonJS, and is useful for
hot module reload.

The specifier is resolved using the chosen `resolver`, then the resolved module is removed
from all internal caches (CommonJS `require` cache, CommonJS resolution caches, ESM resolve
cache, ESM load cache, and ESM translators cache). When `resolver` is `'import'`,
`importAttributes` are part of the ESM resolve-cache key, so only the exact
`(specifier, parentURL, importAttributes)` resolution entry is removed. When a `file:` URL is
resolved, cached module jobs for the same file path are cleared even if they differ by search
or hash. This means clearing `'./mod.mjs?v=1'` will also clear `'./mod.mjs?v=2'` and any
other query/hash variants that resolve to the same file.

When `resolver` is `'require'`, cached `package.json` data for the resolved module's package
is also cleared so that updated exports/imports conditions are picked up on the next
resolution.

Clearing a module does not clear cached entries for its dependencies. When using
`resolver: 'import'`, resolution cache entries for other specifiers that resolve to the
same target are not cleared — only the exact `(specifier, parentURL, importAttributes)`
entry is removed. The module cache itself is cleared by resolved file path, so all
specifiers pointing to the same file will see a fresh execution on next import.

#### Memory retention and static imports

`clearCache` only removes references from the Node.js **JavaScript-level** caches
(the ESM load cache, resolve cache, CJS `require.cache`, and related structures).
It does **not** affect V8-internal module graph references.

When a module M is **statically imported** by a live parent module P
(i.e., via a top-level `import … from '…'` statement that has already been
evaluated), V8's module instantiation creates a permanent internal strong
reference from P's compiled module record to M's module record. Calling
`clearCache(M)` cannot sever that link. Consequences:

* The old instance of M **stays alive in memory** for as long as P is alive,
regardless of how many times M is cleared and re-imported.
* A fresh `import(M)` after clearing will create a **separate** module instance
that new importers see. P, however, continues to use the original instance —
the two coexist simultaneously (sometimes called a "split-brain" state).
* This is a **bounded** retention: one stale module instance per cleared module
per live static parent. It does not grow unboundedly across clear/re-import
cycles.

For **dynamically imported** modules (`await import('./M.mjs')` with no live
static parent holding the result), the old `ModuleWrap` becomes eligible for
garbage collection once `clearCache` removes it from Node.js caches and all
JS-land references (e.g., stored namespace objects) are dropped.

The safest pattern for hot-reload of ES modules is to use cache-busting search
parameters (so each version is a distinct module URL) and use dynamic imports for modules that need to be reloaded:

#### ECMA-262 spec considerations

Re-importing the exact same `(specifier, parentURL, importAttributes)` tuple after clearing the module cache
technically violates the idempotency invariant of the ECMA-262
[`HostLoadImportedModule`][] host hook, which expects that the same module request always
returns the same Module Record for a given referrer. The result of violating this requirement
is undefined — e.g. it can lead to crashes. For spec-compliant usage, use
cache-busting search parameters so that each reload uses a distinct module request:

```mjs
import { clearCache } from 'node:module';
import { watch } from 'node:fs';

let version = 0;
const base = new URL('./app.mjs', import.meta.url);

watch(base, async () => {
// Clear the module cache for the previous version.
clearCache(new URL(`${base.href}?v=${version}`), {
parentURL: import.meta.url,
resolver: 'import',
});
version++;
// Re-import with a new search parameter — this is a distinct module request
// and does not violate the ECMA-262 invariant.
const mod = await import(`${base.href}?v=${version}`);
console.log('reloaded:', mod);
});
```

#### Examples

```mjs
import { clearCache } from 'node:module';

await import('./mod.mjs');

clearCache('./mod.mjs', {
parentURL: import.meta.url,
resolver: 'import',
});
await import('./mod.mjs'); // re-executes the module
```

```cjs
const { clearCache } = require('node:module');
const { pathToFileURL } = require('node:url');

require('./mod.js');

clearCache('./mod.js', {
parentURL: pathToFileURL(__filename),
resolver: 'require',
});
require('./mod.js'); // eslint-disable-line node-core/no-duplicate-requires
// re-executes the module
```

### `module.findPackageJSON(specifier[, base])`

<!-- YAML
Expand Down Expand Up @@ -2040,6 +2169,7 @@ returned object contains the following keys:
[`--enable-source-maps`]: cli.md#--enable-source-maps
[`--import`]: cli.md#--importmodule
[`--require`]: cli.md#-r---require-module
[`HostLoadImportedModule`]: https://tc39.es/ecma262/#sec-HostLoadImportedModule
[`NODE_COMPILE_CACHE=dir`]: cli.md#node_compile_cachedir
[`NODE_COMPILE_CACHE_PORTABLE=1`]: cli.md#node_compile_cache_portable1
[`NODE_DISABLE_COMPILE_CACHE=1`]: cli.md#node_disable_compile_cache1
Expand Down
27 changes: 26 additions & 1 deletion lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ const kIsExecuting = Symbol('kIsExecuting');
const kURL = Symbol('kURL');
const kFormat = Symbol('kFormat');

const relativeResolveCache = { __proto__: null };

// Set first due to cycle with ESM loader functions.
module.exports = {
kModuleSource,
Expand All @@ -119,6 +121,7 @@ module.exports = {
kModuleCircularVisited,
initializeCJS,
Module,
clearCJSResolutionCaches,
findLongestRegisteredExtension,
resolveForCJSWithHooks,
loadSourceForCJSWithHooks: loadSource,
Expand Down Expand Up @@ -223,7 +226,29 @@ let { startTimer, endTimer } = debugWithTimer('module_timer', (start, end) => {
const { tracingChannel } = require('diagnostics_channel');
const onRequire = getLazy(() => tracingChannel('module.require'));

const relativeResolveCache = { __proto__: null };
/**
* Clear all entries in the CJS relative resolve cache and _pathCache
* that map to a given filename. This is needed by clearCache() to
* prevent stale resolution results after a module is removed.
* @param {string} filename The resolved filename to purge.
*/
function clearCJSResolutionCaches(filename) {
// Clear from relativeResolveCache (keyed by parent.path + '\x00' + request).
const relKeys = ObjectKeys(relativeResolveCache);
for (let i = 0; i < relKeys.length; i++) {
if (relativeResolveCache[relKeys[i]] === filename) {
delete relativeResolveCache[relKeys[i]];
}
}

// Clear from Module._pathCache (keyed by request + '\x00' + paths).
const pathKeys = ObjectKeys(Module._pathCache);
for (let i = 0; i < pathKeys.length; i++) {
if (Module._pathCache[pathKeys[i]] === filename) {
delete Module._pathCache[pathKeys[i]];
}
}
}

let requireDepth = 0;
let isPreloading = false;
Expand Down
Loading
Loading