Skip to content

Commit b7788b4

Browse files
committed
module: add clearCache for CJS and ESM
1 parent 2e1265a commit b7788b4

7 files changed

Lines changed: 305 additions & 2 deletions

File tree

doc/api/module.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,51 @@ const require = createRequire(import.meta.url);
6666
const siblingModule = require('./sibling-module');
6767
```
6868
69+
### `module.clearCache(specifier[, options])`
70+
71+
<!-- YAML
72+
added: REPLACEME
73+
-->
74+
75+
> Stability: 1.1 - Active development
76+
77+
* `specifier` {string|URL} The module specifier or URL to clear.
78+
* `options` {Object}
79+
* `mode` {string} Which caches to clear. Supported values are `'all'`, `'cjs'`, and `'esm'`.
80+
**Default:** `'all'`.
81+
* `parentURL` {string|URL} The parent URL or absolute path used to resolve non-URL specifiers.
82+
For CommonJS, pass `__filename`. For ES modules, pass `import.meta.url`.
83+
* `type` {string} Import attributes `type` used for ESM resolution.
84+
* `importAttributes` {Object} Import attributes for ESM resolution. Cannot be used with `type`.
85+
* Returns: {Object} An object with `{ cjs: boolean, esm: boolean }` indicating whether entries
86+
were removed from each cache.
87+
88+
Clears the CommonJS `require` cache and/or the ESM module cache for a module. This enables
89+
reload patterns similar to deleting from `require.cache` in CommonJS, and is useful for HMR.
90+
When `mode` is `'all'`, resolution failures for one module system do not throw; check the
91+
returned flags to see what was cleared.
92+
93+
```mjs
94+
import { clearCache } from 'node:module';
95+
96+
const url = new URL('./mod.mjs', import.meta.url);
97+
await import(url.href);
98+
99+
clearCache(url);
100+
await import(url.href); // re-executes the module
101+
```
102+
103+
```cjs
104+
const { clearCache } = require('node:module');
105+
const path = require('node:path');
106+
107+
const file = path.join(__dirname, 'mod.js');
108+
require(file);
109+
110+
clearCache(file);
111+
require(file); // re-executes the module
112+
```
113+
69114
### `module.findPackageJSON(specifier[, base])`
70115
71116
<!-- YAML

lib/internal/modules/cjs/loader.js

Lines changed: 194 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ const { BuiltinModule } = require('internal/bootstrap/realm');
135135
const {
136136
maybeCacheSourceMap,
137137
} = require('internal/source_map/source_map_cache');
138-
const { pathToFileURL, fileURLToPath, isURL, URL } = require('internal/url');
138+
const { pathToFileURL, fileURLToPath, isURL, URL, URLParse } = require('internal/url');
139139
const {
140140
pendingDeprecate,
141141
emitExperimentalWarning,
@@ -200,7 +200,11 @@ const {
200200
},
201201
setArrowMessage,
202202
} = require('internal/errors');
203-
const { validateString } = require('internal/validators');
203+
const {
204+
validateObject,
205+
validateOneOf,
206+
validateString,
207+
} = require('internal/validators');
204208

205209
const {
206210
CHAR_BACKWARD_SLASH,
@@ -2037,6 +2041,193 @@ function createRequire(filenameOrURL) {
20372041
return createRequireFromPath(filepath, fileURL);
20382042
}
20392043

2044+
/**
2045+
* Normalize the parent URL/path for cache clearing.
2046+
* @param {string|URL|undefined} parentURL
2047+
* @returns {{ parentURL: string|undefined, parentPath: string|undefined }}
2048+
*/
2049+
function normalizeClearCacheParent(parentURL) {
2050+
if (parentURL === undefined) {
2051+
return { __proto__: null, parentURL: undefined, parentPath: undefined };
2052+
}
2053+
2054+
if (isURL(parentURL)) {
2055+
const parentPath =
2056+
parentURL.protocol === 'file:' && parentURL.search === '' && parentURL.hash === '' ?
2057+
fileURLToPath(parentURL) :
2058+
undefined;
2059+
return { __proto__: null, parentURL: parentURL.href, parentPath };
2060+
}
2061+
2062+
validateString(parentURL, 'options.parentURL');
2063+
if (path.isAbsolute(parentURL)) {
2064+
return {
2065+
__proto__: null,
2066+
parentURL: pathToFileURL(parentURL).href,
2067+
parentPath: parentURL,
2068+
};
2069+
}
2070+
2071+
const url = URLParse(parentURL);
2072+
if (!url) {
2073+
throw new ERR_INVALID_ARG_VALUE('options.parentURL', parentURL,
2074+
'must be an absolute path or URL');
2075+
}
2076+
2077+
const parentPath =
2078+
url.protocol === 'file:' && url.search === '' && url.hash === '' ?
2079+
fileURLToPath(url) :
2080+
undefined;
2081+
return { __proto__: null, parentURL: url.href, parentPath };
2082+
}
2083+
2084+
/**
2085+
* Parse a specifier as a URL when possible.
2086+
* @param {string|URL} specifier
2087+
* @returns {URL|null}
2088+
*/
2089+
function getURLFromClearCacheSpecifier(specifier) {
2090+
if (isURL(specifier)) {
2091+
return specifier;
2092+
}
2093+
2094+
if (typeof specifier !== 'string' || path.isAbsolute(specifier)) {
2095+
return null;
2096+
}
2097+
2098+
return URLParse(specifier) ?? null;
2099+
}
2100+
2101+
/**
2102+
* Create a synthetic parent module for CJS resolution.
2103+
* @param {string} parentPath
2104+
* @returns {Module}
2105+
*/
2106+
function createParentModuleForClearCache(parentPath) {
2107+
const parent = new Module(parentPath);
2108+
parent.filename = parentPath;
2109+
parent.paths = Module._nodeModulePaths(path.dirname(parentPath));
2110+
return parent;
2111+
}
2112+
2113+
/**
2114+
* Resolve a cache filename for CommonJS.
2115+
* @param {string|URL} specifier
2116+
* @param {string|undefined} parentPath
2117+
* @returns {string|null}
2118+
*/
2119+
function resolveClearCacheFilename(specifier, parentPath) {
2120+
if (!parentPath && typeof specifier === 'string' && isRelative(specifier)) {
2121+
return null;
2122+
}
2123+
2124+
const parsedURL = getURLFromClearCacheSpecifier(specifier);
2125+
let request = specifier;
2126+
if (parsedURL) {
2127+
if (parsedURL.protocol !== 'file:' || parsedURL.search !== '' || parsedURL.hash !== '') {
2128+
return null;
2129+
}
2130+
request = fileURLToPath(parsedURL);
2131+
}
2132+
2133+
const parent = parentPath ? createParentModuleForClearCache(parentPath) : null;
2134+
return Module._resolveFilename(request, parent, false);
2135+
}
2136+
2137+
/**
2138+
* Resolve a cache URL for ESM.
2139+
* @param {string|URL} specifier
2140+
* @param {string|undefined} parentURL
2141+
* @param {Record<string, string>|undefined} importAttributes
2142+
* @returns {string}
2143+
*/
2144+
function resolveClearCacheURL(specifier, parentURL, importAttributes) {
2145+
const parsedURL = getURLFromClearCacheSpecifier(specifier);
2146+
if (parsedURL) {
2147+
return parsedURL.href;
2148+
}
2149+
2150+
if (path.isAbsolute(specifier)) {
2151+
return pathToFileURL(specifier).href;
2152+
}
2153+
2154+
if (parentURL === undefined) {
2155+
throw new ERR_INVALID_ARG_VALUE('options.parentURL', parentURL,
2156+
'must be provided for non-URL ESM specifiers');
2157+
}
2158+
2159+
const cascadedLoader =
2160+
require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
2161+
const request = { specifier, attributes: importAttributes, __proto__: null };
2162+
return cascadedLoader.resolveSync(parentURL, request).url;
2163+
}
2164+
2165+
/**
2166+
* Clear CommonJS and/or ESM module cache entries.
2167+
* @param {string|URL} specifier
2168+
* @param {object} [options]
2169+
* @param {'all'|'cjs'|'esm'} [options.mode]
2170+
* @param {string|URL} [options.parentURL]
2171+
* @param {string} [options.type]
2172+
* @param {Record<string, string>} [options.importAttributes]
2173+
* @returns {{ cjs: boolean, esm: boolean }}
2174+
*/
2175+
function clearCache(specifier, options = kEmptyObject) {
2176+
const isSpecifierURL = isURL(specifier);
2177+
if (!isSpecifierURL) {
2178+
validateString(specifier, 'specifier');
2179+
}
2180+
2181+
validateObject(options, 'options');
2182+
const mode = options.mode === undefined ? 'all' : options.mode;
2183+
validateOneOf(mode, 'options.mode', ['all', 'cjs', 'esm']);
2184+
2185+
if (options.importAttributes !== undefined && options.type !== undefined) {
2186+
throw new ERR_INVALID_ARG_VALUE('options.importAttributes', options.importAttributes,
2187+
'cannot be used with options.type');
2188+
}
2189+
2190+
let importAttributes = options.importAttributes;
2191+
if (options.type !== undefined) {
2192+
validateString(options.type, 'options.type');
2193+
importAttributes = { __proto__: null, type: options.type };
2194+
} else if (importAttributes !== undefined) {
2195+
validateObject(importAttributes, 'options.importAttributes');
2196+
}
2197+
2198+
const { parentURL, parentPath } = normalizeClearCacheParent(options.parentURL);
2199+
const result = { __proto__: null, cjs: false, esm: false };
2200+
2201+
if (mode !== 'esm') {
2202+
try {
2203+
const filename = resolveClearCacheFilename(specifier, parentPath);
2204+
if (filename && Module._cache[filename] !== undefined) {
2205+
delete Module._cache[filename];
2206+
result.cjs = true;
2207+
}
2208+
} catch (err) {
2209+
if (mode === 'cjs') {
2210+
throw err;
2211+
}
2212+
}
2213+
}
2214+
2215+
if (mode !== 'cjs') {
2216+
try {
2217+
const url = resolveClearCacheURL(specifier, parentURL, importAttributes);
2218+
const cascadedLoader =
2219+
require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
2220+
result.esm = cascadedLoader.loadCache.deleteAll(url);
2221+
} catch (err) {
2222+
if (mode === 'esm') {
2223+
throw err;
2224+
}
2225+
}
2226+
}
2227+
2228+
return result;
2229+
}
2230+
20402231
/**
20412232
* Checks if a path is relative
20422233
* @param {string} path the target path
@@ -2053,6 +2244,7 @@ function isRelative(path) {
20532244
}
20542245

20552246
Module.createRequire = createRequire;
2247+
Module.clearCache = clearCache;
20562248

20572249
/**
20582250
* Define the paths to use for resolving a module.

lib/internal/modules/esm/module_map.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,16 @@ class LoadCache extends SafeMap {
123123
cached[type] = undefined;
124124
}
125125
}
126+
127+
/**
128+
* Delete all cached module jobs for a URL.
129+
* @param {string} url
130+
* @returns {boolean} true if an entry was deleted.
131+
*/
132+
deleteAll(url) {
133+
validateString(url, 'url');
134+
return super.delete(url);
135+
}
126136
}
127137

128138
module.exports = {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import '../common/index.mjs';
2+
3+
import assert from 'node:assert';
4+
import { clearCache } from 'node:module';
5+
6+
const specifier = '../fixtures/module-cache/esm-counter.mjs';
7+
8+
const first = await import(specifier);
9+
const second = await import(specifier);
10+
11+
assert.strictEqual(first.count, 1);
12+
assert.strictEqual(second.count, 1);
13+
assert.strictEqual(first, second);
14+
15+
const result = clearCache(specifier, { parentURL: import.meta.url });
16+
assert.strictEqual(result.cjs, false);
17+
assert.strictEqual(result.esm, true);
18+
19+
const third = await import(specifier);
20+
assert.strictEqual(third.count, 2);
21+
22+
delete globalThis.__module_cache_esm_counter;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
globalThis.__module_cache_cjs_counter = (globalThis.__module_cache_cjs_counter ?? 0) + 1;
2+
3+
module.exports = {
4+
count: globalThis.__module_cache_cjs_counter,
5+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
globalThis.__module_cache_esm_counter = (globalThis.__module_cache_esm_counter ?? 0) + 1;
2+
3+
export const count = globalThis.__module_cache_esm_counter;
4+
export default count;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use strict';
2+
3+
require('../common');
4+
5+
const assert = require('node:assert');
6+
const path = require('node:path');
7+
const { clearCache } = require('node:module');
8+
9+
const fixture = path.join(__dirname, '..', 'fixtures', 'module-cache', 'cjs-counter.js');
10+
11+
const first = require(fixture);
12+
const second = require(fixture);
13+
14+
assert.strictEqual(first.count, 1);
15+
assert.strictEqual(second.count, 1);
16+
assert.strictEqual(first, second);
17+
18+
const result = clearCache(fixture);
19+
assert.strictEqual(result.cjs, true);
20+
assert.strictEqual(result.esm, false);
21+
22+
const third = require(fixture);
23+
assert.strictEqual(third.count, 2);
24+
25+
delete globalThis.__module_cache_cjs_counter;

0 commit comments

Comments
 (0)