Skip to content

Commit 83d2402

Browse files
committed
fixup! module: add clearCache for CJS and ESM
1 parent af0f7d0 commit 83d2402

10 files changed

Lines changed: 254 additions & 3 deletions

lib/internal/modules/cjs/loader.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2202,6 +2202,14 @@ function deleteModuleFromParents(targetModule) {
22022202
let deleted = false;
22032203
for (let i = 0; i < keys.length; i++) {
22042204
const cachedModule = Module._cache[keys[i]];
2205+
if (cachedModule?.[kFirstModuleParent] === targetModule) {
2206+
cachedModule[kFirstModuleParent] = undefined;
2207+
deleted = true;
2208+
}
2209+
if (cachedModule?.[kLastModuleParent] === targetModule) {
2210+
cachedModule[kLastModuleParent] = undefined;
2211+
deleted = true;
2212+
}
22052213
const children = cachedModule?.children;
22062214
if (!ArrayIsArray(children)) {
22072215
continue;
@@ -2344,7 +2352,9 @@ function clearCache(specifier, options = kEmptyObject) {
23442352
if (resolvedPath) {
23452353
resolveDeleted = cascadedLoader.deleteResolveCacheByFilename(resolvedPath) || resolveDeleted;
23462354
}
2347-
result.module = loadDeleted || resolveDeleted;
2355+
const { clearCjsCache } = require('internal/modules/esm/translators');
2356+
const cjsCacheDeleted = clearCjsCache(url);
2357+
result.module = loadDeleted || resolveDeleted || cjsCacheDeleted;
23482358
} catch (err) {
23492359
if (mode === 'module') {
23502360
throw err;

lib/internal/modules/esm/translators.js

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const {
4444
loadSourceForCJSWithHooks,
4545
populateCJSExportsFromESM,
4646
} = require('internal/modules/cjs/loader');
47-
const { fileURLToPath, pathToFileURL, URL } = require('internal/url');
47+
const { fileURLToPath, pathToFileURL, URL, URLParse } = require('internal/url');
4848
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
4949
debug = fn;
5050
});
@@ -193,6 +193,64 @@ function loadCJSModule(module, source, url, filename, isMain) {
193193
// TODO: can we use a weak map instead?
194194
const cjsCache = new SafeMap();
195195

196+
/**
197+
* Resolve a file path for a file URL, stripping search/hash.
198+
* @param {string} url
199+
* @returns {string|null}
200+
*/
201+
function getFilePathFromCjsCacheURL(url) {
202+
const parsedURL = URLParse(url);
203+
if (!parsedURL) {
204+
return null;
205+
}
206+
if (parsedURL.protocol !== 'file:') {
207+
return null;
208+
}
209+
if (parsedURL.search !== '' || parsedURL.hash !== '') {
210+
parsedURL.search = '';
211+
parsedURL.hash = '';
212+
}
213+
try {
214+
return fileURLToPath(parsedURL);
215+
} catch {
216+
return null;
217+
}
218+
}
219+
220+
/**
221+
* Remove cjsCache entries for a URL and its file-path variants.
222+
* @param {string} url
223+
* @returns {boolean} true if any entries were deleted.
224+
*/
225+
function clearCjsCache(url) {
226+
let deleted = cjsCache.delete(url);
227+
const filename = getFilePathFromCjsCacheURL(url);
228+
if (!filename) {
229+
return deleted;
230+
}
231+
232+
const urls = [];
233+
for (const entry of cjsCache) {
234+
ArrayPrototypePush(urls, entry[0]);
235+
}
236+
237+
for (let i = 0; i < urls.length; i++) {
238+
const cachedURL = urls[i];
239+
if (cachedURL === url) {
240+
continue;
241+
}
242+
const cachedFilename = getFilePathFromCjsCacheURL(cachedURL);
243+
if (cachedFilename === filename) {
244+
cjsCache.delete(cachedURL);
245+
deleted = true;
246+
}
247+
}
248+
249+
return deleted;
250+
}
251+
252+
exports.clearCjsCache = clearCjsCache;
253+
196254
/**
197255
* Creates a ModuleWrap object for a CommonJS module.
198256
* @param {string} url - The URL of the module.
@@ -323,7 +381,9 @@ translators.set('require-commonjs-typescript', (url, translateContext, parentURL
323381

324382
// This goes through Module._load to accommodate monkey-patchers.
325383
function loadCJSModuleWithModuleLoad(module, source, url, filename, isMain) {
326-
assert(module === CJSModule._cache[filename]);
384+
if (CJSModule._cache[filename] !== module) {
385+
CJSModule._cache[filename] = module;
386+
}
327387
// If it gets here in the translators, the hooks must have already been invoked
328388
// in the loader. Skip them in the synthetic module evaluation step.
329389
wrapModuleLoad(filename, undefined, isMain, kShouldSkipModuleHooks);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import '../common/index.mjs';
2+
import assert from 'node:assert';
3+
import { clearCache } from 'node:module';
4+
5+
const url = new URL('../fixtures/module-cache/cjs-counter.js', import.meta.url);
6+
7+
const importPromise = import(url.href);
8+
clearCache(url);
9+
10+
const first = await importPromise;
11+
assert.strictEqual(first.default.count, 1);
12+
13+
clearCache(url);
14+
const second = await import(url.href);
15+
assert.strictEqual(second.default.count, 2);
16+
17+
delete globalThis.__module_cache_cjs_counter;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Flags: --no-warnings
2+
3+
import { mustCall } from '../common/index.mjs';
4+
import assert from 'node:assert';
5+
import { clearCache, createRequire } from 'node:module';
6+
7+
const require = createRequire(import.meta.url);
8+
const { checkIfCollectableByCounting } = require('../common/gc');
9+
10+
const baseUrl = new URL('../fixtures/simple.wasm', import.meta.url);
11+
12+
const outer = 8;
13+
const inner = 4;
14+
15+
const runIteration = mustCall(async (i) => {
16+
for (let j = 0; j < inner; j++) {
17+
const url = new URL(baseUrl);
18+
url.search = `?v=${i}-${j}`;
19+
const mod = await import(url.href);
20+
assert.strictEqual(mod.add(1, 2), 3);
21+
clearCache(url);
22+
}
23+
return inner;
24+
}, outer);
25+
26+
checkIfCollectableByCounting(runIteration, WebAssembly.Instance, outer).then(mustCall());
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Flags: --no-warnings
2+
3+
import assert from 'node:assert';
4+
import { clearCache } from 'node:module';
5+
6+
const url = new URL('../fixtures/simple.wasm', import.meta.url);
7+
8+
const first = await import(url.href);
9+
assert.strictEqual(first.add(1, 2), 3);
10+
11+
const result = clearCache(url);
12+
assert.strictEqual(result.commonjs, false);
13+
assert.strictEqual(result.module, true);
14+
15+
const second = await import(url.href);
16+
assert.strictEqual(second.add(2, 3), 5);
17+
assert.notStrictEqual(first, second);

test/fixtures/source-map/cjs-closure-source-map.js

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
4+
const common = require('../common');
5+
6+
const assert = require('node:assert');
7+
const path = require('node:path');
8+
const { pathToFileURL } = require('node:url');
9+
const { clearCache } = require('node:module');
10+
const { clearCjsCache } = require('internal/modules/esm/translators');
11+
12+
const fixturePath = path.join(__dirname, '..', 'fixtures', 'module-cache', 'cjs-counter.js');
13+
const url = pathToFileURL(fixturePath);
14+
15+
(async () => {
16+
const first = await import(`${url.href}?v=1`);
17+
assert.strictEqual(first.default.count, 1);
18+
19+
const result = clearCache(url);
20+
assert.strictEqual(result.commonjs, true);
21+
assert.strictEqual(result.module, true);
22+
23+
assert.strictEqual(clearCjsCache(`${url.href}?v=1`), false);
24+
delete globalThis.__module_cache_cjs_counter;
25+
})().then(common.mustCall());
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
5+
const path = require('node:path');
6+
const Module = require('node:module');
7+
const { clearCache } = require('node:module');
8+
const { checkIfCollectableByCounting } = require('../common/gc');
9+
10+
const fixture = path.join(__dirname, '..', 'fixtures', 'module-cache', 'cjs-counter.js');
11+
12+
const outer = 16;
13+
const inner = 64;
14+
15+
checkIfCollectableByCounting(() => {
16+
for (let i = 0; i < inner; i++) {
17+
require(fixture);
18+
clearCache(fixture, { mode: 'commonjs' });
19+
}
20+
return inner;
21+
}, Module, outer).then(common.mustCall(() => {
22+
delete globalThis.__module_cache_cjs_counter;
23+
}));
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
4+
require('../common');
5+
6+
const assert = require('node:assert');
7+
const path = require('node:path');
8+
const { clearCache } = require('node:module');
9+
const { internalBinding } = require('internal/test/binding');
10+
11+
const {
12+
privateSymbols: {
13+
module_first_parent_private_symbol,
14+
module_last_parent_private_symbol,
15+
},
16+
} = internalBinding('util');
17+
18+
const parentPath = path.join(__dirname, '..', 'fixtures', 'module-cache', 'cjs-parent.js');
19+
const childPath = path.join(__dirname, '..', 'fixtures', 'module-cache', 'cjs-child.js');
20+
21+
require(parentPath);
22+
23+
const childModule = require.cache[childPath];
24+
const parentModule = require.cache[parentPath];
25+
26+
assert.strictEqual(childModule[module_first_parent_private_symbol], parentModule);
27+
assert.strictEqual(childModule[module_last_parent_private_symbol], parentModule);
28+
29+
clearCache(parentPath, { mode: 'commonjs' });
30+
31+
assert.strictEqual(childModule[module_first_parent_private_symbol], undefined);
32+
assert.strictEqual(childModule[module_last_parent_private_symbol], undefined);
33+
34+
clearCache(childPath, { mode: 'commonjs' });
35+
delete globalThis.__module_cache_cjs_child_counter;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Flags: --enable-source-maps
2+
'use strict';
3+
4+
require('../common');
5+
6+
const assert = require('node:assert');
7+
const path = require('node:path');
8+
const { clearCache } = require('node:module');
9+
10+
const fixture = path.join(
11+
__dirname,
12+
'..',
13+
'fixtures',
14+
'source-map',
15+
'cjs-closure-source-map.js',
16+
);
17+
18+
const { crash } = require(fixture);
19+
20+
const result = clearCache(fixture);
21+
assert.strictEqual(result.commonjs, true);
22+
assert.strictEqual(result.module, false);
23+
24+
try {
25+
crash();
26+
assert.fail('Expected crash() to throw');
27+
} catch (err) {
28+
assert.match(err.stack, /cjs-closure-source-map-original\.js/);
29+
}

0 commit comments

Comments
 (0)