Skip to content

Commit e2a581d

Browse files
committed
fixup! module: add clearCache for CJS and ESM
1 parent 2496f25 commit e2a581d

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
@@ -2211,6 +2211,14 @@ function deleteModuleFromParents(targetModule) {
22112211
let deleted = false;
22122212
for (let i = 0; i < keys.length; i++) {
22132213
const cachedModule = Module._cache[keys[i]];
2214+
if (cachedModule?.[kFirstModuleParent] === targetModule) {
2215+
cachedModule[kFirstModuleParent] = undefined;
2216+
deleted = true;
2217+
}
2218+
if (cachedModule?.[kLastModuleParent] === targetModule) {
2219+
cachedModule[kLastModuleParent] = undefined;
2220+
deleted = true;
2221+
}
22142222
const children = cachedModule?.children;
22152223
if (!ArrayIsArray(children)) {
22162224
continue;
@@ -2353,7 +2361,9 @@ function clearCache(specifier, options = kEmptyObject) {
23532361
if (resolvedPath) {
23542362
resolveDeleted = cascadedLoader.deleteResolveCacheByFilename(resolvedPath) || resolveDeleted;
23552363
}
2356-
result.module = loadDeleted || resolveDeleted;
2364+
const { clearCjsCache } = require('internal/modules/esm/translators');
2365+
const cjsCacheDeleted = clearCjsCache(url);
2366+
result.module = loadDeleted || resolveDeleted || cjsCacheDeleted;
23572367
} catch (err) {
23582368
if (mode === 'module') {
23592369
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
});
@@ -184,6 +184,64 @@ function loadCJSModule(module, source, url, filename, isMain) {
184184
// TODO: can we use a weak map instead?
185185
const cjsCache = new SafeMap();
186186

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

315373
// This goes through Module._load to accommodate monkey-patchers.
316374
function loadCJSModuleWithModuleLoad(module, source, url, filename, isMain) {
317-
assert(module === CJSModule._cache[filename]);
375+
if (CJSModule._cache[filename] !== module) {
376+
CJSModule._cache[filename] = module;
377+
}
318378
// If it gets here in the translators, the hooks must have already been invoked
319379
// in the loader. Skip them in the synthetic module evaluation step.
320380
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)