-
-
Notifications
You must be signed in to change notification settings - Fork 34.7k
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?
Conversation
|
Review requested:
|
4f7a659 to
90303e6
Compare
90303e6 to
1d0accc
Compare
|
The
notable-change
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. |
|
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. |
benjamingr
left a comment
There was a problem hiding this 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.
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. |
|
@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
Thanks for the review @benjamingr. Would you mind re-reviewing again so I can trigger CI? |
|
Thanks a lot for this ❤️ |
|
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 moduleis 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 Report❌ Patch coverage is
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
🚀 New features to boost your workflow:
|
There was a problem hiding this 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'`. |
There was a problem hiding this comment.
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`. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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:
- For resolution, the specifier makes a bit more sense (together with other parameters, though)
- 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); |
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
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).
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.