Skip to content

Commit 78da849

Browse files
xiaoxiaojxclaude
andcommitted
fix: move join/dirname caches to Resolver instance to prevent memory leak
Move cachedJoin/cachedDirname from module-level globals to per-Resolver instance caches. When a Resolver is garbage collected, its join/dirname caches are released with it, preventing unbounded memory growth in long-running processes. - Extract createCachedJoin/createCachedDirname factory functions in path.js - Resolver constructor creates instance-level join/dirname via factories - TsconfigPathsPlugin and UnsafeCachePlugin now use resolver.join/dirname instead of global cachedJoin/cachedDirname - Global cachedJoin/cachedDirname retained for direct resolve() usage Ref: #418 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 986a974 commit 78da849

5 files changed

Lines changed: 96 additions & 59 deletions

File tree

lib/Resolver.js

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ const createInnerContext = require("./createInnerContext");
1010
const { parseIdentifier } = require("./util/identifier");
1111
const {
1212
PathType,
13-
cachedJoin: join,
13+
createCachedDirname,
14+
createCachedJoin,
1415
getType,
1516
normalize,
1617
} = require("./util/path");
@@ -397,6 +398,8 @@ class Resolver {
397398
constructor(fileSystem, options) {
398399
this.fileSystem = fileSystem;
399400
this.options = options;
401+
this.join = createCachedJoin();
402+
this.dirname = createCachedDirname();
400403
/** @type {KnownHooks} */
401404
this.hooks = {
402405
resolveStep: new SyncHook(["hook", "request"], "resolveStep"),
@@ -800,15 +803,6 @@ class Resolver {
800803
return path.endsWith("/");
801804
}
802805

803-
/**
804-
* @param {string} path path
805-
* @param {string} request request
806-
* @returns {string} joined path
807-
*/
808-
join(path, request) {
809-
return join(path, request);
810-
}
811-
812806
/**
813807
* @param {string} path path
814808
* @returns {string} normalized path

lib/TsconfigPathsPlugin.js

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,7 @@
88
const { aliasResolveHandler } = require("./AliasUtils");
99
const { modulesResolveHandler } = require("./ModulesUtils");
1010
const { readJson } = require("./util/fs");
11-
const {
12-
PathType: _PathType,
13-
cachedDirname: dirname,
14-
cachedJoin: join,
15-
isSubPath,
16-
normalize,
17-
} = require("./util/path");
11+
const { PathType: _PathType, isSubPath, normalize } = require("./util/path");
1812

1913
/** @typedef {import("./Resolver")} Resolver */
2014
/** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */
@@ -102,10 +96,11 @@ function substituteConfigDir(pathValue, configDir) {
10296
* Convert tsconfig paths to resolver options
10397
* @param {string} configDir Config file directory
10498
* @param {{ [key: string]: string[] }} paths TypeScript paths mapping
99+
* @param {(rootPath: string, request: string) => string} join join function
105100
* @param {string=} baseUrl Base URL for resolving paths (relative to configDir)
106101
* @returns {TsconfigPathsData} the resolver options
107102
*/
108-
function tsconfigPathsToResolveOptions(configDir, paths, baseUrl) {
103+
function tsconfigPathsToResolveOptions(configDir, paths, join, baseUrl) {
109104
// Calculate absolute base URL
110105
const absoluteBaseUrl = !baseUrl ? configDir : join(configDir, baseUrl);
111106

@@ -155,10 +150,11 @@ function tsconfigPathsToResolveOptions(configDir, paths, baseUrl) {
155150
/**
156151
* Get the base context for the current project
157152
* @param {string} context the context
153+
* @param {(rootPath: string, request: string) => string} join join function
158154
* @param {string=} baseUrl base URL for resolving paths
159155
* @returns {string} the base context
160156
*/
161-
function getAbsoluteBaseUrl(context, baseUrl) {
157+
function getAbsoluteBaseUrl(context, join, baseUrl) {
162158
return !baseUrl ? context : join(context, baseUrl);
163159
}
164160

@@ -293,6 +289,7 @@ module.exports = class TsconfigPathsPlugin {
293289
* @returns {Promise<TsconfigPathsMap | null>} the tsconfig paths map or null
294290
*/
295291
async _getTsconfigPathsMap(resolver, request, resolveContext) {
292+
const { join, dirname } = resolver;
296293
if (typeof request.tsconfigPathsMap === "undefined") {
297294
try {
298295
const absTsconfigPath = join(
@@ -302,6 +299,8 @@ module.exports = class TsconfigPathsPlugin {
302299
const result = await this._loadTsconfigPathsMap(
303300
resolver.fileSystem,
304301
absTsconfigPath,
302+
join,
303+
dirname,
305304
);
306305

307306
request.tsconfigPathsMap = result;
@@ -334,15 +333,19 @@ module.exports = class TsconfigPathsPlugin {
334333
* Includes main project paths and all referenced projects
335334
* @param {FileSystem} fileSystem the file system
336335
* @param {string} absTsconfigPath absolute path to tsconfig.json
336+
* @param {(rootPath: string, request: string) => string} join join function
337+
* @param {(maybePath: string) => string} dirname dirname function
337338
* @returns {Promise<TsconfigPathsMap>} the complete tsconfig paths map
338339
*/
339-
async _loadTsconfigPathsMap(fileSystem, absTsconfigPath) {
340+
async _loadTsconfigPathsMap(fileSystem, absTsconfigPath, join, dirname) {
340341
/** @type {Set<string>} */
341342
const fileDependencies = new Set();
342343
const config = await this._loadTsconfig(
343344
fileSystem,
344345
absTsconfigPath,
345346
fileDependencies,
347+
join,
348+
dirname,
346349
);
347350

348351
const compilerOptions = config.compilerOptions || {};
@@ -354,6 +357,7 @@ module.exports = class TsconfigPathsPlugin {
354357
const main = tsconfigPathsToResolveOptions(
355358
mainContext,
356359
compilerOptions.paths || {},
360+
join,
357361
baseUrl,
358362
);
359363
/** @type {{ [baseUrl: string]: TsconfigPathsData }} */
@@ -373,6 +377,8 @@ module.exports = class TsconfigPathsPlugin {
373377
referencesToUse,
374378
fileDependencies,
375379
refs,
380+
join,
381+
dirname,
376382
);
377383
}
378384

@@ -423,6 +429,8 @@ module.exports = class TsconfigPathsPlugin {
423429
* @param {string} extendedConfigValue extends value
424430
* @param {Set<string>} fileDependencies the file dependencies
425431
* @param {Set<string>} visitedConfigPaths config paths being loaded (for circular extends detection)
432+
* @param {(rootPath: string, request: string) => string} join join function
433+
* @param {(maybePath: string) => string} dirname dirname function
426434
* @returns {Promise<Tsconfig>} the extended tsconfig
427435
*/
428436
async _loadTsconfigFromExtends(
@@ -431,6 +439,8 @@ module.exports = class TsconfigPathsPlugin {
431439
extendedConfigValue,
432440
fileDependencies,
433441
visitedConfigPaths,
442+
join,
443+
dirname,
434444
) {
435445
const currentDir = dirname(configFilePath);
436446

@@ -485,6 +495,8 @@ module.exports = class TsconfigPathsPlugin {
485495
fileSystem,
486496
extendedConfigPath,
487497
fileDependencies,
498+
join,
499+
dirname,
488500
visitedConfigPaths,
489501
);
490502
const compilerOptions = config.compilerOptions || { baseUrl: undefined };
@@ -493,6 +505,7 @@ module.exports = class TsconfigPathsPlugin {
493505
const extendedConfigDir = dirname(extendedConfigPath);
494506
compilerOptions.baseUrl = getAbsoluteBaseUrl(
495507
extendedConfigDir,
508+
join,
496509
compilerOptions.baseUrl,
497510
);
498511
}
@@ -511,6 +524,8 @@ module.exports = class TsconfigPathsPlugin {
511524
* @param {TsconfigReference[]} references array of references
512525
* @param {Set<string>} fileDependencies the file dependencies
513526
* @param {{ [baseUrl: string]: TsconfigPathsData }} referenceMatchMap the map to populate
527+
* @param {(rootPath: string, request: string) => string} join join function
528+
* @param {(maybePath: string) => string} dirname dirname function
514529
* @returns {Promise<void>}
515530
*/
516531
async _loadTsconfigReferences(
@@ -519,6 +534,8 @@ module.exports = class TsconfigPathsPlugin {
519534
references,
520535
fileDependencies,
521536
referenceMatchMap,
537+
join,
538+
dirname,
522539
) {
523540
await Promise.all(
524541
references.map(async (ref) => {
@@ -530,6 +547,8 @@ module.exports = class TsconfigPathsPlugin {
530547
fileSystem,
531548
refConfigPath,
532549
fileDependencies,
550+
join,
551+
dirname,
533552
);
534553

535554
if (refConfig.compilerOptions && refConfig.compilerOptions.paths) {
@@ -538,6 +557,7 @@ module.exports = class TsconfigPathsPlugin {
538557
referenceMatchMap[refContext] = tsconfigPathsToResolveOptions(
539558
refContext,
540559
refConfig.compilerOptions.paths || {},
560+
join,
541561
refConfig.compilerOptions.baseUrl,
542562
);
543563
}
@@ -552,6 +572,8 @@ module.exports = class TsconfigPathsPlugin {
552572
refConfig.references,
553573
fileDependencies,
554574
referenceMatchMap,
575+
join,
576+
dirname,
555577
);
556578
}
557579
} catch (_err) {
@@ -566,13 +588,17 @@ module.exports = class TsconfigPathsPlugin {
566588
* @param {FileSystem} fileSystem the file system
567589
* @param {string} configFilePath absolute path to tsconfig.json
568590
* @param {Set<string>} fileDependencies the file dependencies
591+
* @param {(rootPath: string, request: string) => string} join join function
592+
* @param {(maybePath: string) => string} dirname dirname function
569593
* @param {Set<string>=} visitedConfigPaths config paths being loaded (for circular extends detection)
570594
* @returns {Promise<Tsconfig>} the merged tsconfig
571595
*/
572596
async _loadTsconfig(
573597
fileSystem,
574598
configFilePath,
575599
fileDependencies,
600+
join,
601+
dirname,
576602
visitedConfigPaths = new Set(),
577603
) {
578604
if (visitedConfigPaths.has(configFilePath)) {
@@ -599,6 +625,8 @@ module.exports = class TsconfigPathsPlugin {
599625
extendedConfigElement,
600626
fileDependencies,
601627
visitedConfigPaths,
628+
join,
629+
dirname,
602630
);
603631
base = mergeTsconfigs(base, extendedTsconfig);
604632
}
@@ -609,6 +637,8 @@ module.exports = class TsconfigPathsPlugin {
609637
extendedConfig,
610638
fileDependencies,
611639
visitedConfigPaths,
640+
join,
641+
dirname,
612642
);
613643
}
614644

lib/UnsafeCachePlugin.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55

66
"use strict";
77

8-
const { cachedJoin } = require("./util/path");
9-
108
/** @typedef {import("./Resolver")} Resolver */
119
/** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */
1210
/** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */
@@ -18,10 +16,11 @@ const RELATIVE_REQUEST_REGEXP = /^\.\.?(?:\/|$)/;
1816
/**
1917
* @param {string} relativePath relative path from package root
2018
* @param {string} request relative request
19+
* @param {(rootPath: string, request: string) => string} join join function
2120
* @returns {string} normalized request with a preserved leading dot
2221
*/
23-
function joinRelativePreservingLeadingDot(relativePath, request) {
24-
const normalized = cachedJoin(relativePath, request);
22+
function joinRelativePreservingLeadingDot(relativePath, request, join) {
23+
const normalized = join(relativePath, request);
2524
return RELATIVE_REQUEST_REGEXP.test(normalized)
2625
? normalized
2726
: `./${normalized}`;
@@ -40,9 +39,10 @@ function getCachePath(request) {
4039

4140
/**
4241
* @param {ResolveRequest} request request
42+
* @param {(rootPath: string, request: string) => string} join join function
4343
* @returns {string | undefined} normalized request string
4444
*/
45-
function getCacheRequest(request) {
45+
function getCacheRequest(request, join) {
4646
const requestString = request.request;
4747
if (
4848
!requestString ||
@@ -51,23 +51,28 @@ function getCacheRequest(request) {
5151
) {
5252
return requestString;
5353
}
54-
return joinRelativePreservingLeadingDot(request.relativePath, requestString);
54+
return joinRelativePreservingLeadingDot(
55+
request.relativePath,
56+
requestString,
57+
join,
58+
);
5559
}
5660

5761
/**
5862
* @param {string} type type of cache
5963
* @param {ResolveRequest} request request
6064
* @param {boolean} withContext cache with context?
65+
* @param {(rootPath: string, request: string) => string} join join function
6166
* @returns {string} cache id
6267
*/
63-
function getCacheId(type, request, withContext) {
68+
function getCacheId(type, request, withContext, join) {
6469
return JSON.stringify({
6570
type,
6671
context: withContext ? request.context : "",
6772
path: getCachePath(request),
6873
query: request.query,
6974
fragment: request.fragment,
70-
request: getCacheRequest(request),
75+
request: getCacheRequest(request, join),
7176
});
7277
}
7378

@@ -93,6 +98,7 @@ module.exports = class UnsafeCachePlugin {
9398
*/
9499
apply(resolver) {
95100
const target = resolver.ensureHook(this.target);
101+
const { join } = resolver;
96102
resolver
97103
.getHook(this.source)
98104
.tapAsync("UnsafeCachePlugin", (request, resolveContext, callback) => {
@@ -110,6 +116,7 @@ module.exports = class UnsafeCachePlugin {
110116
isYield ? "yield" : "default",
111117
request,
112118
this.withContext,
119+
join,
113120
);
114121
const cacheEntry = this.cache[cacheId];
115122
if (cacheEntry) {

0 commit comments

Comments
 (0)