Skip to content

Commit a50d67a

Browse files
module: create relative resolve cache bucket lazily
Only create the per-directory relative resolve cache bucket once there is a resolved filename to store in it, so builtin loads and failed resolutions no longer leave empty buckets behind. Read parent.path once per Module._load() call. Signed-off-by: Sam Attard <sattard@anthropic.com> Assisted-by: Claude
1 parent 96860f3 commit a50d67a

2 files changed

Lines changed: 71 additions & 9 deletions

File tree

‎lib/internal/modules/cjs/loader.js‎

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,20 +1273,16 @@ function loadBuiltinWithHooks(id, url, format) {
12731273
* @returns {object}
12741274
*/
12751275
Module._load = function(request, parent, isMain, internalOptions = kEmptyObject) {
1276+
let parentPath;
12761277
let relResolveCacheByDir;
12771278
if (parent) {
12781279
debug('Module._load REQUEST %s parent: %s', request, parent.id);
12791280
// Fast path for (lazy loaded) modules in the same directory. Keyed by
12801281
// parent directory and then request, so no concatenated cache key
12811282
// string is allocated per require() call.
1282-
relResolveCacheByDir = relativeResolveCache.get(parent.path);
1283-
if (relResolveCacheByDir === undefined) {
1284-
// A plain object handles dynamically built specifier strings
1285-
// better than a Map here.
1286-
relResolveCacheByDir = { __proto__: null };
1287-
relativeResolveCache.set(parent.path, relResolveCacheByDir);
1288-
}
1289-
const filename = relResolveCacheByDir[request];
1283+
parentPath = parent.path;
1284+
relResolveCacheByDir = relativeResolveCache.get(parentPath);
1285+
const filename = relResolveCacheByDir?.[request];
12901286
if (filename !== undefined) {
12911287
reportModuleToWatchMode(filename);
12921288
reportModuleToWatchModeFromWorker(filename);
@@ -1401,7 +1397,15 @@ Module._load = function(request, parent, isMain, internalOptions = kEmptyObject)
14011397
module[kFormat] ??= format;
14021398
}
14031399

1404-
if (relResolveCacheByDir !== undefined) {
1400+
if (parent) {
1401+
// Only create the per-directory bucket once there is an entry to store,
1402+
// so builtins and failed resolutions don't leave empty buckets behind.
1403+
if (relResolveCacheByDir === undefined) {
1404+
// A plain object handles dynamically built specifier strings
1405+
// better than a Map here.
1406+
relResolveCacheByDir = { __proto__: null };
1407+
relativeResolveCache.set(parentPath, relResolveCacheByDir);
1408+
}
14051409
relResolveCacheByDir[request] = filename;
14061410
}
14071411

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
'use strict';
2+
3+
// Checks that Module._load() only creates a relative resolve cache entry for
4+
// the parent directory when there is a resolved filename to store in it.
5+
6+
require('../common');
7+
const assert = require('assert');
8+
const fs = require('fs');
9+
const path = require('path');
10+
const Module = require('module');
11+
const tmpdir = require('../common/tmpdir');
12+
13+
tmpdir.refresh();
14+
15+
let uniqueId = 0;
16+
17+
function createParent() {
18+
const dir = tmpdir.resolve(`dir-${uniqueId++}`);
19+
fs.mkdirSync(dir);
20+
const parent = new Module(path.join(dir, 'parent.js'));
21+
parent.filename = parent.id;
22+
parent.paths = Module._nodeModulePaths(dir);
23+
let pathReads = 0;
24+
Object.defineProperty(parent, 'path', {
25+
__proto__: null,
26+
get() {
27+
pathReads++;
28+
return dir;
29+
},
30+
});
31+
return { dir, parent, getPathReads: () => pathReads };
32+
}
33+
34+
// Builtins return before the relative resolve cache is populated, so the
35+
// parent directory should only be looked up, not also used to create a bucket.
36+
for (const request of ['node:path', 'path']) {
37+
const { parent, getPathReads } = createParent();
38+
assert.strictEqual(Module._load(request, parent, false), path);
39+
assert.strictEqual(getPathReads(), 1);
40+
}
41+
42+
// Same for resolutions that throw.
43+
{
44+
const { parent, getPathReads } = createParent();
45+
assert.throws(() => Module._load('./missing', parent, false),
46+
{ code: 'MODULE_NOT_FOUND' });
47+
assert.strictEqual(getPathReads(), 1);
48+
}
49+
50+
// A successful load populates the cache, and the second load hits it.
51+
{
52+
const { dir, parent, getPathReads } = createParent();
53+
fs.writeFileSync(path.join(dir, 'child.js'), 'module.exports = {};');
54+
const first = Module._load('./child', parent, false);
55+
assert.strictEqual(getPathReads(), 1);
56+
assert.strictEqual(Module._load('./child', parent, false), first);
57+
assert.strictEqual(getPathReads(), 2);
58+
}

0 commit comments

Comments
 (0)