Skip to content

Commit f7f15d9

Browse files
authored
fix(tsconfig): detect circular extends to prevent infinite loop (#477)
1 parent 5cb58ca commit f7f15d9

File tree

6 files changed

+65
-1
lines changed

6 files changed

+65
-1
lines changed

lib/TsconfigPathsPlugin.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,13 +413,15 @@ module.exports = class TsconfigPathsPlugin {
413413
* @param {string} configFilePath current config file path
414414
* @param {string} extendedConfigValue extends value
415415
* @param {Set<string>} fileDependencies the file dependencies
416+
* @param {Set<string>} visitedConfigPaths config paths being loaded (for circular extends detection)
416417
* @returns {Promise<Tsconfig>} the extended tsconfig
417418
*/
418419
async _loadTsconfigFromExtends(
419420
fileSystem,
420421
configFilePath,
421422
extendedConfigValue,
422423
fileDependencies,
424+
visitedConfigPaths,
423425
) {
424426
const currentDir = dirname(configFilePath);
425427

@@ -451,6 +453,7 @@ module.exports = class TsconfigPathsPlugin {
451453
fileSystem,
452454
extendedConfigPath,
453455
fileDependencies,
456+
visitedConfigPaths,
454457
);
455458
const compilerOptions = config.compilerOptions || { baseUrl: undefined };
456459

@@ -527,9 +530,19 @@ module.exports = class TsconfigPathsPlugin {
527530
* @param {FileSystem} fileSystem the file system
528531
* @param {string} configFilePath absolute path to tsconfig.json
529532
* @param {Set<string>} fileDependencies the file dependencies
533+
* @param {Set<string>=} visitedConfigPaths config paths being loaded (for circular extends detection)
530534
* @returns {Promise<Tsconfig>} the merged tsconfig
531535
*/
532-
async _loadTsconfig(fileSystem, configFilePath, fileDependencies) {
536+
async _loadTsconfig(
537+
fileSystem,
538+
configFilePath,
539+
fileDependencies,
540+
visitedConfigPaths = new Set(),
541+
) {
542+
if (visitedConfigPaths.has(configFilePath)) {
543+
return /** @type {Tsconfig} */ ({});
544+
}
545+
visitedConfigPaths.add(configFilePath);
533546
const config = await readJson(fileSystem, configFilePath);
534547
fileDependencies.add(configFilePath);
535548

@@ -547,6 +560,7 @@ module.exports = class TsconfigPathsPlugin {
547560
configFilePath,
548561
extendedConfigElement,
549562
fileDependencies,
563+
visitedConfigPaths,
550564
);
551565
base = mergeTsconfigs(base, extendedTsconfig);
552566
}
@@ -556,6 +570,7 @@ module.exports = class TsconfigPathsPlugin {
556570
configFilePath,
557571
extendedConfig,
558572
fileDependencies,
573+
visitedConfigPaths,
559574
);
560575
}
561576

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const foo = "foo";
2+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"extends": "${configDir}/../b/tsconfig",
3+
"compilerOptions": {
4+
"baseUrl": ".",
5+
"paths": {
6+
"@lib/*": ["${configDir}/src/lib/*"]
7+
}
8+
}
9+
}
10+
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const bar = "bar";
2+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"extends": "${configDir}/../a/tsconfig",
3+
"compilerOptions": {
4+
"baseUrl": ".",
5+
"paths": {
6+
"@util/*": ["${configDir}/src/util/*"]
7+
}
8+
}
9+
}
10+

test/tsconfig-paths.test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ const extendsNpmDir = path.resolve(
2626
"tsconfig-paths",
2727
"extends-npm",
2828
);
29+
const extendsCircularDir = path.resolve(
30+
__dirname,
31+
"fixtures",
32+
"tsconfig-paths",
33+
"extends-circular",
34+
);
2935
const referencesProjectDir = path.resolve(
3036
__dirname,
3137
"fixtures",
@@ -547,6 +553,25 @@ describe("TsconfigPathsPlugin", () => {
547553
);
548554
});
549555

556+
it("should handle circular extends without hanging", (done) => {
557+
const aDir = path.join(extendsCircularDir, "a");
558+
const resolver = ResolverFactory.createResolver({
559+
fileSystem,
560+
extensions: [".ts", ".tsx"],
561+
mainFields: ["browser", "main"],
562+
mainFiles: ["index"],
563+
tsconfig: path.join(aDir, "tsconfig.json"),
564+
});
565+
566+
// a extends b, b extends a - circular. Should break cycle and resolve.
567+
resolver.resolve({}, aDir, "@lib/foo", {}, (err, result) => {
568+
if (err) return done(err);
569+
if (!result) return done(new Error("No result"));
570+
expect(result).toEqual(path.join(aDir, "src", "lib", "foo.ts"));
571+
done();
572+
});
573+
});
574+
550575
// eslint-disable-next-line no-template-curly-in-string
551576
it("should substitute ${configDir} in references field", (done) => {
552577
const sharedDir = path.join(referencesProjectDir, "packages", "shared");

0 commit comments

Comments
 (0)