Skip to content

Conversation

@anonrig
Copy link
Member

@anonrig anonrig commented Feb 10, 2026

Introduce Module.clearCache() to invalidate CommonJS and ESM module caches with optional resolution context, enabling HMR-like reloads. Document the API and add tests/fixtures to cover cache invalidation behavior.

@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/loaders

@nodejs-github-bot nodejs-github-bot added esm Issues and PRs related to the ECMAScript Modules implementation. module Issues and PRs related to the module subsystem. needs-ci PRs that need a full CI run. labels Feb 10, 2026
@anonrig anonrig force-pushed the yagiz/node-module-clear-cache branch from 4f7a659 to 90303e6 Compare February 10, 2026 21:22
@anonrig anonrig force-pushed the yagiz/node-module-clear-cache branch from 90303e6 to 1d0accc Compare February 10, 2026 21:25
@anonrig anonrig added semver-minor PRs that contain new features and should be released in the next minor version. notable-change PRs with changes that should be highlighted in changelogs. labels Feb 10, 2026
@github-actions
Copy link
Contributor

The notable-change PRs with changes that should be highlighted in changelogs. label has been added by @anonrig.

Please suggest a text for the release notes if you'd like to include a more detailed summary, then proceed to update the PR description with the text or a link to the notable change suggested text comment. Otherwise, the commit will be placed in the Other Notable Changes section.

@mcollina
Copy link
Member

I’m relatively +1 on having this in Node.js, but I recall having a a lot of discussions about this @GeoffreyBooth and @nodejs/loaders teams about this, and it would massively break the spec, expectations, and invariants regarding ESM.

(Note, this is what people have been asking us to add for a long time).

My personal objection to this API is that it would inadvertently leak memory at every turn, so while this sounds good in theory, in practice it would significantly backfire in long-running scenarios. An option could be to expose it only behind a flag, putting the user in charge of choosing this behavior.

Every single scenario where I saw HMR in Node.js ends up in memory leaks. This is the reason why I had so much interest and hopes for ShadowRealm.

Copy link
Member

@benjamingr benjamingr left a comment

Choose a reason for hiding this comment

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

I am still +1 on the feature from a user usability point of view. Code lgtm.

@benjamingr
Copy link
Member

Every single scenario where I saw HMR in Node.js ends up in memory leaks. This is the reason why I had so much interest and hopes for ShadowRealm.

We're giving users a tool, it may be seen as a footgun by some but hopefully libraries that use the API correctly and warn users about incorrect usage emerge.

@anonrig
Copy link
Member Author

anonrig commented Feb 10, 2026

@mcollina Thanks for the feedback. I agree the ESM semantics concerns are real. This API doesn’t change the core ESM invariants (single instance per URL); it only removes Node's internal cache entries to allow explicit reloads in opt‑in workflows. Even with that, existing references (namespaces, listeners, closures) can keep old graphs alive, so this is still potentially leaky unless the app does explicit disposal. I’ll make sure the docs call out the risks and the fact that this only clears Node’s internal caches, and I’d like loader team input on the final shape of the API.

This commit should address some of your concerns. b3bd79a

I am still +1 on the feature from a user usability point of view. Code lgtm.

Thanks for the review @benjamingr. Would you mind re-reviewing again so I can trigger CI?

@Nsttt
Copy link

Nsttt commented Feb 10, 2026

Thanks a lot for this ❤️

@Jamesernator
Copy link

Jamesernator commented Feb 10, 2026

Rather than violating ESM invariants, can't node just provide a function that imports a url?

i.e. While the given example of:

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

clearCache(url);
await import(url.href); // re-executes the module

is indeed not spec compliant, it's perfectly legal to have something like:

import { clearCache, importModule } from "node:module";

await importModule(someUrl);
clearCache();
await importModule(someUrl); // reexecute

@codecov
Copy link

codecov bot commented Feb 10, 2026

Codecov Report

❌ Patch coverage is 98.96194% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.76%. Comparing base (3819c7f) to head (0507308).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
lib/internal/modules/esm/module_map.js 91.89% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #61767      +/-   ##
==========================================
+ Coverage   89.74%   89.76%   +0.02%     
==========================================
  Files         675      675              
  Lines      204642   204929     +287     
  Branches    39322    39363      +41     
==========================================
+ Hits       183657   183959     +302     
+ Misses      13257    13255       -2     
+ Partials     7728     7715      -13     
Files with missing lines Coverage Δ
lib/internal/modules/cjs/loader.js 98.56% <100.00%> (+0.20%) ⬆️
lib/internal/modules/esm/loader.js 97.00% <100.00%> (+0.02%) ⬆️
lib/internal/modules/esm/module_map.js 97.02% <91.89%> (-1.45%) ⬇️

... and 31 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Member

@joyeecheung joyeecheung left a comment

Choose a reason for hiding this comment

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

While I am +1 to the idea in general, I am afraid the current API may bring more problem than it solves...see the comments.

(Granted it isn't really a problem unique to this specific design, I think the issue is more that this is not a very well solved problem so far, I don't really know what it should look like, though I think I might be able to point out what it should not look like to avoid adding/re-introducing issues that user land workarounds can already manage)


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

Choose a reason for hiding this comment

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

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

* `mode` {string} Which caches to clear. Supported values are `'all'`, `'cjs'`, and `'esm'`.
**Default:** `'all'`.
* `parentURL` {string|URL} The parent URL or absolute path used to resolve non-URL specifiers.
For CommonJS, pass `__filename`. For ES modules, pass `import.meta.url`.
Copy link
Member

Choose a reason for hiding this comment

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

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

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

Choose a reason for hiding this comment

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

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


> Stability: 1.1 - Active development

* `specifier` {string|URL} The module specifier or URL to clear.
Copy link
Member

@joyeecheung joyeecheung Feb 11, 2026

Choose a reason for hiding this comment

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

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

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

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

}

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

@joyeecheung joyeecheung Feb 11, 2026

Choose a reason for hiding this comment

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

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

if (filename) {
let deleted = false;
if (Module._cache[filename] !== undefined) {
delete Module._cache[filename];
Copy link
Member

@joyeecheung joyeecheung Feb 11, 2026

Choose a reason for hiding this comment

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

esm Issues and PRs related to the ECMAScript Modules implementation. module Issues and PRs related to the module subsystem. needs-ci PRs that need a full CI run. notable-change PRs with changes that should be highlighted in changelogs. semver-minor PRs that contain new features and should be released in the next minor version.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants