-
-
Notifications
You must be signed in to change notification settings - Fork 35.3k
module: add clearCache for CJS and ESM #61767
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -66,6 +66,135 @@ const require = createRequire(import.meta.url); | |||||||||||||
| const siblingModule = require('./sibling-module'); | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| ### `module.clearCache(specifier, options)` | ||||||||||||||
|
|
||||||||||||||
| <!-- YAML | ||||||||||||||
| added: REPLACEME | ||||||||||||||
| --> | ||||||||||||||
|
|
||||||||||||||
| > Stability: 1.0 - Early development | ||||||||||||||
|
|
||||||||||||||
| * `specifier` {string|URL} The module specifier, as it would have been passed to | ||||||||||||||
| `import()` or `require()`. | ||||||||||||||
| * `options` {Object} | ||||||||||||||
| * `parentURL` {string|URL} The parent URL used to resolve the specifier. Parent identity | ||||||||||||||
| is part of the resolution cache key. For CommonJS, pass `pathToFileURL(__filename)`. | ||||||||||||||
| For ES modules, pass `import.meta.url`. | ||||||||||||||
| * `resolver` {string} Specifies how resolution should be performed. Must be either | ||||||||||||||
| `'import'` or `'require'`. | ||||||||||||||
| * `importAttributes` {Object} Optional import attributes. Only meaningful when | ||||||||||||||
| `resolver` is `'import'`. | ||||||||||||||
|
|
||||||||||||||
| Clears the module resolution and module caches for a module. This enables | ||||||||||||||
| reload patterns similar to deleting from `require.cache` in CommonJS, and is useful for | ||||||||||||||
| hot module reload. | ||||||||||||||
|
|
||||||||||||||
| The specifier is resolved using the chosen `resolver`, then the resolved module is removed | ||||||||||||||
| from all internal caches (CommonJS `require` cache, CommonJS resolution caches, ESM resolve | ||||||||||||||
| cache, ESM load cache, and ESM translators cache). When `resolver` is `'import'`, | ||||||||||||||
| `importAttributes` are part of the ESM resolve-cache key, so only the exact | ||||||||||||||
| `(specifier, parentURL, importAttributes)` resolution entry is removed. When a `file:` URL is | ||||||||||||||
| resolved, cached module jobs for the same file path are cleared even if they differ by search | ||||||||||||||
| or hash. This means clearing `'./mod.mjs?v=1'` will also clear `'./mod.mjs?v=2'` and any | ||||||||||||||
| other query/hash variants that resolve to the same file. | ||||||||||||||
|
|
||||||||||||||
| When `resolver` is `'require'`, cached `package.json` data for the resolved module's package | ||||||||||||||
| is also cleared so that updated exports/imports conditions are picked up on the next | ||||||||||||||
| resolution. | ||||||||||||||
|
|
||||||||||||||
| Clearing a module does not clear cached entries for its dependencies. When using | ||||||||||||||
| `resolver: 'import'`, resolution cache entries for other specifiers that resolve to the | ||||||||||||||
| same target are not cleared — only the exact `(specifier, parentURL, importAttributes)` | ||||||||||||||
| entry is removed. The module cache itself is cleared by resolved file path, so all | ||||||||||||||
| specifiers pointing to the same file will see a fresh execution on next import. | ||||||||||||||
|
|
||||||||||||||
| #### Memory retention and static imports | ||||||||||||||
|
|
||||||||||||||
| `clearCache` only removes references from the Node.js **JavaScript-level** caches | ||||||||||||||
| (the ESM load cache, resolve cache, CJS `require.cache`, and related structures). | ||||||||||||||
| It does **not** affect V8-internal module graph references. | ||||||||||||||
|
Comment on lines
+113
to
+115
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I feel that from a user's POV, the description is the other way around: JS-level references essentially comes from user code, the JS engine is only holding them on the user's behalf, not that V8 itself needs them for some reason. The cleared references are technically Node.js internals. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think "JavaScript-level module graph" imparts what you're hoping it does. If I didn't read this diff and associated review comment, I would have really struggled to understand what it means. In my opinion, there should be an example that shows what this feature accomplishes. I keep getting the impression that it will solve the testing scenario I described above. But it's not clear to me from the explanation if that is true, or if this is meant to solve some other cache problem. |
||||||||||||||
|
|
||||||||||||||
| When a module M is **statically imported** by a live parent module P | ||||||||||||||
| (i.e., via a top-level `import … from '…'` statement that has already been | ||||||||||||||
| evaluated), V8's module instantiation creates a permanent internal strong | ||||||||||||||
| reference from P's compiled module record to M's module record. Calling | ||||||||||||||
| `clearCache(M)` cannot sever that link. Consequences: | ||||||||||||||
|
|
||||||||||||||
| * The old instance of M **stays alive in memory** for as long as P is alive, | ||||||||||||||
| regardless of how many times M is cleared and re-imported. | ||||||||||||||
| * A fresh `import(M)` after clearing will create a **separate** module instance | ||||||||||||||
| that new importers see. P, however, continues to use the original instance — | ||||||||||||||
| the two coexist simultaneously (sometimes called a "split-brain" state). | ||||||||||||||
| * This is a **bounded** retention: one stale module instance per cleared module | ||||||||||||||
| per live static parent. It does not grow unboundedly across clear/re-import | ||||||||||||||
| cycles. | ||||||||||||||
|
|
||||||||||||||
| For **dynamically imported** modules (`await import('./M.mjs')` with no live | ||||||||||||||
| static parent holding the result), the old `ModuleWrap` becomes eligible for | ||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
We shouldn't be talking about |
||||||||||||||
| garbage collection once `clearCache` removes it from Node.js caches and all | ||||||||||||||
| JS-land references (e.g., stored namespace objects) are dropped. | ||||||||||||||
|
|
||||||||||||||
| The safest pattern for hot-reload of ES modules is to use cache-busting search | ||||||||||||||
| parameters (so each version is a distinct module URL) and use dynamic imports for modules that need to be reloaded: | ||||||||||||||
|
|
||||||||||||||
| #### ECMA-262 spec considerations | ||||||||||||||
|
|
||||||||||||||
| Re-importing the exact same `(specifier, parentURL, importAttributes)` tuple after clearing the module cache | ||||||||||||||
| technically violates the idempotency invariant of the ECMA-262 | ||||||||||||||
| [`HostLoadImportedModule`][] host hook, which expects that the same module request always | ||||||||||||||
| returns the same Module Record for a given referrer. The result of violating this requirement | ||||||||||||||
| is undefined — e.g. it can lead to crashes. For spec-compliant usage, use | ||||||||||||||
| cache-busting search parameters so that each reload uses a distinct module request: | ||||||||||||||
|
|
||||||||||||||
| ```mjs | ||||||||||||||
| import { clearCache } from 'node:module'; | ||||||||||||||
| import { watch } from 'node:fs'; | ||||||||||||||
|
|
||||||||||||||
| let version = 0; | ||||||||||||||
| const base = new URL('./app.mjs', import.meta.url); | ||||||||||||||
|
|
||||||||||||||
| watch(base, async () => { | ||||||||||||||
| // Clear the module cache for the previous version. | ||||||||||||||
| clearCache(new URL(`${base.href}?v=${version}`), { | ||||||||||||||
| parentURL: import.meta.url, | ||||||||||||||
| resolver: 'import', | ||||||||||||||
| }); | ||||||||||||||
| version++; | ||||||||||||||
| // Re-import with a new search parameter — this is a distinct module request | ||||||||||||||
| // and does not violate the ECMA-262 invariant. | ||||||||||||||
| const mod = await import(`${base.href}?v=${version}`); | ||||||||||||||
| console.log('reloaded:', mod); | ||||||||||||||
| }); | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| #### Examples | ||||||||||||||
|
|
||||||||||||||
| ```mjs | ||||||||||||||
| import { clearCache } from 'node:module'; | ||||||||||||||
|
|
||||||||||||||
| await import('./mod.mjs'); | ||||||||||||||
|
|
||||||||||||||
| clearCache('./mod.mjs', { | ||||||||||||||
| parentURL: import.meta.url, | ||||||||||||||
| resolver: 'import', | ||||||||||||||
| }); | ||||||||||||||
| await import('./mod.mjs'); // re-executes the module | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| ```cjs | ||||||||||||||
| const { clearCache } = require('node:module'); | ||||||||||||||
| const { pathToFileURL } = require('node:url'); | ||||||||||||||
|
|
||||||||||||||
| require('./mod.js'); | ||||||||||||||
|
|
||||||||||||||
| clearCache('./mod.js', { | ||||||||||||||
| parentURL: pathToFileURL(__filename), | ||||||||||||||
| resolver: 'require', | ||||||||||||||
| }); | ||||||||||||||
| require('./mod.js'); // eslint-disable-line node-core/no-duplicate-requires | ||||||||||||||
| // re-executes the module | ||||||||||||||
| ``` | ||||||||||||||
anonrig marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
|
|
||||||||||||||
| ### `module.findPackageJSON(specifier[, base])` | ||||||||||||||
|
|
||||||||||||||
| <!-- YAML | ||||||||||||||
|
|
@@ -2040,6 +2169,7 @@ returned object contains the following keys: | |||||||||||||
| [`--enable-source-maps`]: cli.md#--enable-source-maps | ||||||||||||||
| [`--import`]: cli.md#--importmodule | ||||||||||||||
| [`--require`]: cli.md#-r---require-module | ||||||||||||||
| [`HostLoadImportedModule`]: https://tc39.es/ecma262/#sec-HostLoadImportedModule | ||||||||||||||
| [`NODE_COMPILE_CACHE=dir`]: cli.md#node_compile_cachedir | ||||||||||||||
| [`NODE_COMPILE_CACHE_PORTABLE=1`]: cli.md#node_compile_cache_portable1 | ||||||||||||||
| [`NODE_DISABLE_COMPILE_CACHE=1`]: cli.md#node_disable_compile_cache1 | ||||||||||||||
|
|
||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.