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
197 changes: 197 additions & 0 deletions doc/api/module.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,201 @@ 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()`. When `resolver` is `'require'`, this must be a
string (the same kind of path or identifier `require()` accepts). Passing a
`URL` object with `resolver: 'require'` throws `ERR_INVALID_ARG_TYPE`.
* `options` {Object} Required.
* `parentURL` {string|URL} Required. 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} Required. 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.

Both `options.parentURL` and `options.resolver` are required. There is no
recursive option: `clearCache` invalidates only the resolved module, not its
dependencies. Callers that need to reload a graph must track and clear each
module themselves.

The specifier is resolved using the chosen `resolver`, then the resolved module
is removed from all Node.js 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 internal caches (the ESM
load cache, resolve cache, CJS `require.cache`, and related structures). It does
**not** affect references created by user modules, for example through a static
`import`. If one module imports another, `clearCache` will not clean up that
link. It only clears references from Node.js internal caches to user modules.

When a module M is **statically imported** by a live parent module P (via a
top-level `import … from '…'` statement that has already been evaluated), the
engine keeps 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 module becomes eligible for
garbage collection once `clearCache` removes it from Node.js caches and all
JavaScript references (for example, 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

Relative specifiers are resolved against `parentURL`, not against the process
working directory:

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

// Resolves to the `mod.mjs` sibling of *this* module, then clears it.
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
```

Bare specifiers are resolved the same way `import`/`require` would resolve them
from `parentURL` (including `node_modules` lookup and `package.json` `"exports"`):

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

await import('some-package');
clearCache('some-package', {
parentURL: import.meta.url,
resolver: 'import',
});
await import('some-package'); // re-executes the package entry point
```

An absolute `file:` URL still requires `parentURL` and `resolver`. The URL is
the cache key; `parentURL` is used if the loader needs to resolve it again
(for example, through customization hooks):

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

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

Reloading a CommonJS module between tests (the ESM equivalent should use
cache-busting search parameters; see [ECMA-262 spec considerations][]):

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

function loadFresh() {
clearCache('./app.js', {
parentURL: pathToFileURL(__filename),
resolver: 'require',
});
return require('./app.js');
}

const first = loadFresh();
const second = loadFresh();
// `first` and `second` are independently evaluated copies.
```

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

<!-- YAML
Expand DownExpand Up@@ -2047,6 +2242,7 @@ returned object contains the following keys:
[CommonJS]: modules.md
[Conditional exports]: packages.md#conditional-exports
[Customization hooks]: #customization-hooks
[ECMA-262 spec considerations]: #ecma-262-spec-considerations
[ES Modules]: esm.md
[Permission Model]: permissions.md#permission-model
[Source Map]: https://tc39.es/ecma426/
Expand All@@ -2057,6 +2253,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 numberDiff line numberDiff 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 DownExpand Up@@ -229,7 +232,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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
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
197 changes: 197 additions & 0 deletions doc/api/module.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,201 @@ 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()`. When `resolver` is `'require'`, this must be a
string (the same kind of path or identifier `require()` accepts). Passing a
`URL` object with `resolver: 'require'` throws `ERR_INVALID_ARG_TYPE`.
* `options` {Object} Required.
* `parentURL` {string|URL} Required. 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} Required. 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.

Both `options.parentURL` and `options.resolver` are required. There is no
recursive option: `clearCache` invalidates only the resolved module, not its
dependencies. Callers that need to reload a graph must track and clear each
module themselves.

The specifier is resolved using the chosen `resolver`, then the resolved module
is removed from all Node.js 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 internal caches (the ESM
load cache, resolve cache, CJS `require.cache`, and related structures). It does
**not** affect references created by user modules, for example through a static
`import`. If one module imports another, `clearCache` will not clean up that
link. It only clears references from Node.js internal caches to user modules.

When a module M is **statically imported** by a live parent module P (via a
top-level `import … from '…'` statement that has already been evaluated), the
engine keeps 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 module becomes eligible for
garbage collection once `clearCache` removes it from Node.js caches and all
JavaScript references (for example, 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

Relative specifiers are resolved against `parentURL`, not against the process
working directory:

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

// Resolves to the `mod.mjs` sibling of *this* module, then clears it.
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
```

Bare specifiers are resolved the same way `import`/`require` would resolve them
from `parentURL` (including `node_modules` lookup and `package.json` `"exports"`):

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

await import('some-package');
clearCache('some-package', {
parentURL: import.meta.url,
resolver: 'import',
});
await import('some-package'); // re-executes the package entry point
```

An absolute `file:` URL still requires `parentURL` and `resolver`. The URL is
the cache key; `parentURL` is used if the loader needs to resolve it again
(for example, through customization hooks):

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

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

Reloading a CommonJS module between tests (the ESM equivalent should use
cache-busting search parameters; see [ECMA-262 spec considerations][]):

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

function loadFresh() {
clearCache('./app.js', {
parentURL: pathToFileURL(__filename),
resolver: 'require',
});
return require('./app.js');
}

const first = loadFresh();
const second = loadFresh();
// `first` and `second` are independently evaluated copies.
```

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

<!-- YAML
Expand DownExpand Up@@ -2047,6 +2242,7 @@ returned object contains the following keys:
[CommonJS]: modules.md
[Conditional exports]: packages.md#conditional-exports
[Customization hooks]: #customization-hooks
[ECMA-262 spec considerations]: #ecma-262-spec-considerations
[ES Modules]: esm.md
[Permission Model]: permissions.md#permission-model
[Source Map]: https://tc39.es/ecma426/
Expand All@@ -2057,6 +2253,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 numberDiff line numberDiff 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 DownExpand Up@@ -229,7 +232,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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
197 changes: 197 additions & 0 deletions doc/api/module.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,201 @@ 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()`. When `resolver` is `'require'`, this must be a
string (the same kind of path or identifier `require()` accepts). Passing a
`URL` object with `resolver: 'require'` throws `ERR_INVALID_ARG_TYPE`.
* `options` {Object} Required.
* `parentURL` {string|URL} Required. 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} Required. 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.

Both `options.parentURL` and `options.resolver` are required. There is no
recursive option: `clearCache` invalidates only the resolved module, not its
dependencies. Callers that need to reload a graph must track and clear each
module themselves.

The specifier is resolved using the chosen `resolver`, then the resolved module
is removed from all Node.js 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 internal caches (the ESM
load cache, resolve cache, CJS `require.cache`, and related structures). It does
**not** affect references created by user modules, for example through a static
`import`. If one module imports another, `clearCache` will not clean up that
link. It only clears references from Node.js internal caches to user modules.

When a module M is **statically imported** by a live parent module P (via a
top-level `import … from '…'` statement that has already been evaluated), the
engine keeps 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 module becomes eligible for
garbage collection once `clearCache` removes it from Node.js caches and all
JavaScript references (for example, 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

Relative specifiers are resolved against `parentURL`, not against the process
working directory:

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

// Resolves to the `mod.mjs` sibling of *this* module, then clears it.
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
```

Bare specifiers are resolved the same way `import`/`require` would resolve them
from `parentURL` (including `node_modules` lookup and `package.json` `"exports"`):

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

await import('some-package');
clearCache('some-package', {
parentURL: import.meta.url,
resolver: 'import',
});
await import('some-package'); // re-executes the package entry point
```

An absolute `file:` URL still requires `parentURL` and `resolver`. The URL is
the cache key; `parentURL` is used if the loader needs to resolve it again
(for example, through customization hooks):

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

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

Reloading a CommonJS module between tests (the ESM equivalent should use
cache-busting search parameters; see [ECMA-262 spec considerations][]):

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

function loadFresh() {
clearCache('./app.js', {
parentURL: pathToFileURL(__filename),
resolver: 'require',
});
return require('./app.js');
}

const first = loadFresh();
const second = loadFresh();
// `first` and `second` are independently evaluated copies.
```

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

<!-- YAML
Expand DownExpand Up@@ -2047,6 +2242,7 @@ returned object contains the following keys:
[CommonJS]: modules.md
[Conditional exports]: packages.md#conditional-exports
[Customization hooks]: #customization-hooks
[ECMA-262 spec considerations]: #ecma-262-spec-considerations
[ES Modules]: esm.md
[Permission Model]: permissions.md#permission-model
[Source Map]: https://tc39.es/ecma426/
Expand All@@ -2057,6 +2253,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 numberDiff line numberDiff 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 DownExpand Up@@ -229,7 +232,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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
197 changes: 197 additions & 0 deletions doc/api/module.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,201 @@ 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()`. When `resolver` is `'require'`, this must be a
string (the same kind of path or identifier `require()` accepts). Passing a
`URL` object with `resolver: 'require'` throws `ERR_INVALID_ARG_TYPE`.
* `options` {Object} Required.
* `parentURL` {string|URL} Required. 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} Required. 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.

Both `options.parentURL` and `options.resolver` are required. There is no
recursive option: `clearCache` invalidates only the resolved module, not its
dependencies. Callers that need to reload a graph must track and clear each
module themselves.

The specifier is resolved using the chosen `resolver`, then the resolved module
is removed from all Node.js 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 internal caches (the ESM
load cache, resolve cache, CJS `require.cache`, and related structures). It does
**not** affect references created by user modules, for example through a static
`import`. If one module imports another, `clearCache` will not clean up that
link. It only clears references from Node.js internal caches to user modules.

When a module M is **statically imported** by a live parent module P (via a
top-level `import … from '…'` statement that has already been evaluated), the
engine keeps 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 module becomes eligible for
garbage collection once `clearCache` removes it from Node.js caches and all
JavaScript references (for example, 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

Relative specifiers are resolved against `parentURL`, not against the process
working directory:

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

// Resolves to the `mod.mjs` sibling of *this* module, then clears it.
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
```

Bare specifiers are resolved the same way `import`/`require` would resolve them
from `parentURL` (including `node_modules` lookup and `package.json` `"exports"`):

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

await import('some-package');
clearCache('some-package', {
parentURL: import.meta.url,
resolver: 'import',
});
await import('some-package'); // re-executes the package entry point
```

An absolute `file:` URL still requires `parentURL` and `resolver`. The URL is
the cache key; `parentURL` is used if the loader needs to resolve it again
(for example, through customization hooks):

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

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

Reloading a CommonJS module between tests (the ESM equivalent should use
cache-busting search parameters; see [ECMA-262 spec considerations][]):

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

function loadFresh() {
clearCache('./app.js', {
parentURL: pathToFileURL(__filename),
resolver: 'require',
});
return require('./app.js');
}

const first = loadFresh();
const second = loadFresh();
// `first` and `second` are independently evaluated copies.
```

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

<!-- YAML
Expand DownExpand Up@@ -2047,6 +2242,7 @@ returned object contains the following keys:
[CommonJS]: modules.md
[Conditional exports]: packages.md#conditional-exports
[Customization hooks]: #customization-hooks
[ECMA-262 spec considerations]: #ecma-262-spec-considerations
[ES Modules]: esm.md
[Permission Model]: permissions.md#permission-model
[Source Map]: https://tc39.es/ecma426/
Expand All@@ -2057,6 +2253,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 numberDiff line numberDiff 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 DownExpand Up@@ -229,7 +232,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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
197 changes: 197 additions & 0 deletions doc/api/module.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,201 @@ 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()`. When `resolver` is `'require'`, this must be a
string (the same kind of path or identifier `require()` accepts). Passing a
`URL` object with `resolver: 'require'` throws `ERR_INVALID_ARG_TYPE`.
* `options` {Object} Required.
* `parentURL` {string|URL} Required. 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} Required. 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.

Both `options.parentURL` and `options.resolver` are required. There is no
recursive option: `clearCache` invalidates only the resolved module, not its
dependencies. Callers that need to reload a graph must track and clear each
module themselves.

The specifier is resolved using the chosen `resolver`, then the resolved module
is removed from all Node.js 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 internal caches (the ESM
load cache, resolve cache, CJS `require.cache`, and related structures). It does
**not** affect references created by user modules, for example through a static
`import`. If one module imports another, `clearCache` will not clean up that
link. It only clears references from Node.js internal caches to user modules.

When a module M is **statically imported** by a live parent module P (via a
top-level `import … from '…'` statement that has already been evaluated), the
engine keeps 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 module becomes eligible for
garbage collection once `clearCache` removes it from Node.js caches and all
JavaScript references (for example, 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

Relative specifiers are resolved against `parentURL`, not against the process
working directory:

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

// Resolves to the `mod.mjs` sibling of *this* module, then clears it.
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
```

Bare specifiers are resolved the same way `import`/`require` would resolve them
from `parentURL` (including `node_modules` lookup and `package.json` `"exports"`):

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

await import('some-package');
clearCache('some-package', {
parentURL: import.meta.url,
resolver: 'import',
});
await import('some-package'); // re-executes the package entry point
```

An absolute `file:` URL still requires `parentURL` and `resolver`. The URL is
the cache key; `parentURL` is used if the loader needs to resolve it again
(for example, through customization hooks):

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

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

Reloading a CommonJS module between tests (the ESM equivalent should use
cache-busting search parameters; see [ECMA-262 spec considerations][]):

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

function loadFresh() {
clearCache('./app.js', {
parentURL: pathToFileURL(__filename),
resolver: 'require',
});
return require('./app.js');
}

const first = loadFresh();
const second = loadFresh();
// `first` and `second` are independently evaluated copies.
```

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

<!-- YAML
Expand DownExpand Up@@ -2047,6 +2242,7 @@ returned object contains the following keys:
[CommonJS]: modules.md
[Conditional exports]: packages.md#conditional-exports
[Customization hooks]: #customization-hooks
[ECMA-262 spec considerations]: #ecma-262-spec-considerations
[ES Modules]: esm.md
[Permission Model]: permissions.md#permission-model
[Source Map]: https://tc39.es/ecma426/
Expand All@@ -2057,6 +2253,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 numberDiff line numberDiff 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 DownExpand Up@@ -229,7 +232,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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
197 changes: 197 additions & 0 deletions doc/api/module.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,201 @@ 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()`. When `resolver` is `'require'`, this must be a
string (the same kind of path or identifier `require()` accepts). Passing a
`URL` object with `resolver: 'require'` throws `ERR_INVALID_ARG_TYPE`.
* `options` {Object} Required.
* `parentURL` {string|URL} Required. 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} Required. 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.

Both `options.parentURL` and `options.resolver` are required. There is no
recursive option: `clearCache` invalidates only the resolved module, not its
dependencies. Callers that need to reload a graph must track and clear each
module themselves.

The specifier is resolved using the chosen `resolver`, then the resolved module
is removed from all Node.js 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 internal caches (the ESM
load cache, resolve cache, CJS `require.cache`, and related structures). It does
**not** affect references created by user modules, for example through a static
`import`. If one module imports another, `clearCache` will not clean up that
link. It only clears references from Node.js internal caches to user modules.

When a module M is **statically imported** by a live parent module P (via a
top-level `import … from '…'` statement that has already been evaluated), the
engine keeps 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 module becomes eligible for
garbage collection once `clearCache` removes it from Node.js caches and all
JavaScript references (for example, 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

Relative specifiers are resolved against `parentURL`, not against the process
working directory:

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

// Resolves to the `mod.mjs` sibling of *this* module, then clears it.
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
```

Bare specifiers are resolved the same way `import`/`require` would resolve them
from `parentURL` (including `node_modules` lookup and `package.json` `"exports"`):

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

await import('some-package');
clearCache('some-package', {
parentURL: import.meta.url,
resolver: 'import',
});
await import('some-package'); // re-executes the package entry point
```

An absolute `file:` URL still requires `parentURL` and `resolver`. The URL is
the cache key; `parentURL` is used if the loader needs to resolve it again
(for example, through customization hooks):

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

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

Reloading a CommonJS module between tests (the ESM equivalent should use
cache-busting search parameters; see [ECMA-262 spec considerations][]):

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

function loadFresh() {
clearCache('./app.js', {
parentURL: pathToFileURL(__filename),
resolver: 'require',
});
return require('./app.js');
}

const first = loadFresh();
const second = loadFresh();
// `first` and `second` are independently evaluated copies.
```

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

<!-- YAML
Expand DownExpand Up@@ -2047,6 +2242,7 @@ returned object contains the following keys:
[CommonJS]: modules.md
[Conditional exports]: packages.md#conditional-exports
[Customization hooks]: #customization-hooks
[ECMA-262 spec considerations]: #ecma-262-spec-considerations
[ES Modules]: esm.md
[Permission Model]: permissions.md#permission-model
[Source Map]: https://tc39.es/ecma426/
Expand All@@ -2057,6 +2253,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 numberDiff line numberDiff 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 DownExpand Up@@ -229,7 +232,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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
197 changes: 197 additions & 0 deletions doc/api/module.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,201 @@ 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()`. When `resolver` is `'require'`, this must be a
string (the same kind of path or identifier `require()` accepts). Passing a
`URL` object with `resolver: 'require'` throws `ERR_INVALID_ARG_TYPE`.
* `options` {Object} Required.
* `parentURL` {string|URL} Required. 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} Required. 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.

Both `options.parentURL` and `options.resolver` are required. There is no
recursive option: `clearCache` invalidates only the resolved module, not its
dependencies. Callers that need to reload a graph must track and clear each
module themselves.

The specifier is resolved using the chosen `resolver`, then the resolved module
is removed from all Node.js 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 internal caches (the ESM
load cache, resolve cache, CJS `require.cache`, and related structures). It does
**not** affect references created by user modules, for example through a static
`import`. If one module imports another, `clearCache` will not clean up that
link. It only clears references from Node.js internal caches to user modules.

When a module M is **statically imported** by a live parent module P (via a
top-level `import … from '…'` statement that has already been evaluated), the
engine keeps 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 module becomes eligible for
garbage collection once `clearCache` removes it from Node.js caches and all
JavaScript references (for example, 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

Relative specifiers are resolved against `parentURL`, not against the process
working directory:

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

// Resolves to the `mod.mjs` sibling of *this* module, then clears it.
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
```

Bare specifiers are resolved the same way `import`/`require` would resolve them
from `parentURL` (including `node_modules` lookup and `package.json` `"exports"`):

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

await import('some-package');
clearCache('some-package', {
parentURL: import.meta.url,
resolver: 'import',
});
await import('some-package'); // re-executes the package entry point
```

An absolute `file:` URL still requires `parentURL` and `resolver`. The URL is
the cache key; `parentURL` is used if the loader needs to resolve it again
(for example, through customization hooks):

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

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

Reloading a CommonJS module between tests (the ESM equivalent should use
cache-busting search parameters; see [ECMA-262 spec considerations][]):

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

function loadFresh() {
clearCache('./app.js', {
parentURL: pathToFileURL(__filename),
resolver: 'require',
});
return require('./app.js');
}

const first = loadFresh();
const second = loadFresh();
// `first` and `second` are independently evaluated copies.
```

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

<!-- YAML
Expand DownExpand Up@@ -2047,6 +2242,7 @@ returned object contains the following keys:
[CommonJS]: modules.md
[Conditional exports]: packages.md#conditional-exports
[Customization hooks]: #customization-hooks
[ECMA-262 spec considerations]: #ecma-262-spec-considerations
[ES Modules]: esm.md
[Permission Model]: permissions.md#permission-model
[Source Map]: https://tc39.es/ecma426/
Expand All@@ -2057,6 +2253,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 numberDiff line numberDiff 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 DownExpand Up@@ -229,7 +232,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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
197 changes: 197 additions & 0 deletions doc/api/module.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,201 @@ 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()`. When `resolver` is `'require'`, this must be a
string (the same kind of path or identifier `require()` accepts). Passing a
`URL` object with `resolver: 'require'` throws `ERR_INVALID_ARG_TYPE`.
* `options` {Object} Required.
* `parentURL` {string|URL} Required. 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} Required. 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.

Both `options.parentURL` and `options.resolver` are required. There is no
recursive option: `clearCache` invalidates only the resolved module, not its
dependencies. Callers that need to reload a graph must track and clear each
module themselves.

The specifier is resolved using the chosen `resolver`, then the resolved module
is removed from all Node.js 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 internal caches (the ESM
load cache, resolve cache, CJS `require.cache`, and related structures). It does
**not** affect references created by user modules, for example through a static
`import`. If one module imports another, `clearCache` will not clean up that
link. It only clears references from Node.js internal caches to user modules.

When a module M is **statically imported** by a live parent module P (via a
top-level `import … from '…'` statement that has already been evaluated), the
engine keeps 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 module becomes eligible for
garbage collection once `clearCache` removes it from Node.js caches and all
JavaScript references (for example, 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

Relative specifiers are resolved against `parentURL`, not against the process
working directory:

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

// Resolves to the `mod.mjs` sibling of *this* module, then clears it.
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
```

Bare specifiers are resolved the same way `import`/`require` would resolve them
from `parentURL` (including `node_modules` lookup and `package.json` `"exports"`):

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

await import('some-package');
clearCache('some-package', {
parentURL: import.meta.url,
resolver: 'import',
});
await import('some-package'); // re-executes the package entry point
```

An absolute `file:` URL still requires `parentURL` and `resolver`. The URL is
the cache key; `parentURL` is used if the loader needs to resolve it again
(for example, through customization hooks):

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

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

Reloading a CommonJS module between tests (the ESM equivalent should use
cache-busting search parameters; see [ECMA-262 spec considerations][]):

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

function loadFresh() {
clearCache('./app.js', {
parentURL: pathToFileURL(__filename),
resolver: 'require',
});
return require('./app.js');
}

const first = loadFresh();
const second = loadFresh();
// `first` and `second` are independently evaluated copies.
```

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

<!-- YAML
Expand DownExpand Up@@ -2047,6 +2242,7 @@ returned object contains the following keys:
[CommonJS]: modules.md
[Conditional exports]: packages.md#conditional-exports
[Customization hooks]: #customization-hooks
[ECMA-262 spec considerations]: #ecma-262-spec-considerations
[ES Modules]: esm.md
[Permission Model]: permissions.md#permission-model
[Source Map]: https://tc39.es/ecma426/
Expand All@@ -2057,6 +2253,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 numberDiff line numberDiff 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 DownExpand Up@@ -229,7 +232,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