Skip to content

Commit 1d0accc

Browse files
committed
module: add clearCache for CJS and ESM
1 parent 4dc0d20 commit 1d0accc

File tree

7 files changed

+305
-2
lines changed

7 files changed

+305
-2
lines changed

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,
@@ -2028,6 +2032,193 @@ function createRequire(filenameOrURL) {
20282032
return createRequireFromPath(filepath, fileURL);
20292033
}
20302034

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

20462237
Module.createRequire = createRequire;
2238+
Module.clearCache = clearCache;
20472239

20482240
/**
20492241
* 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)