diff --git a/doc/api/module.md b/doc/api/module.md index 81e49882def0bf..874300c84ed3c0 100644 --- a/doc/api/module.md +++ b/doc/api/module.md @@ -66,6 +66,141 @@ const require = createRequire(import.meta.url); const siblingModule = require('./sibling-module'); ``` +### `module.clearCache(specifier, options)` + + + +> 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'`. + * `caches` {string} Specifies which caches to clear. Must be one of: + * `'resolution'` — only clear the resolution cache entry for this specifier. + * `'module'` — clear the cached module everywhere in Node.js (not counting + JS-level references). + * `'all'` — clear both resolution and module caches. + +Clears module resolution and/or module caches for a module. This enables +reload patterns similar to deleting from `require.cache` in CommonJS, and is useful for +hot module reload. + +When `caches` is `'module'` or `'all'`, the specifier is resolved using the chosen `resolver` +and the resolved module is removed from all internal caches (CommonJS `require` cache, ESM +load cache, and ESM translators cache). 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 `caches` is `'resolution'` or `'all'` with `resolver` set to `'import'`, all ESM +resolution cache entries for the given `(specifier, parentURL)` pair are cleared regardless +of import attributes. When `resolver` is `'require'`, the specific CJS relative resolve +cache entry for the `(parentDir, specifier)` pair is cleared, together with any cached +`package.json` data for the resolved module's package. + +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 entries for the exact `(specifier, parentURL)` pair +are 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', + caches: 'all', + }); + 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', + caches: 'module', +}); +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', + caches: 'module', +}); +require('./mod.js'); // eslint-disable-line node-core/no-duplicate-requires +// re-executes the module +``` + ### `module.findPackageJSON(specifier[, base])`