From 03f5234be51b63dfc8e726f8fa18046f83d712c8 Mon Sep 17 00:00:00 2001
From: trikadin <trikadin+github@pm.me>
Date: Wed, 11 Sep 2024 13:42:07 +0400
Subject: [PATCH 1/6] Support ${configDir} interpolation for all the fields

---
 .gitignore                  |  3 ++
 src/parse-tsconfig/index.ts | 60 ++++++++++++++++++++++++++++---------
 2 files changed, 49 insertions(+), 14 deletions(-)

diff --git a/.gitignore b/.gitignore
index 1fca471..7b3fe23 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,9 @@ yarn-debug.log*
 yarn-error.log*
 lerna-debug.log*
 
+# JetBrains (WebStorm)
+.idea
+
 # Dependency directories
 node_modules/
 
diff --git a/src/parse-tsconfig/index.ts b/src/parse-tsconfig/index.ts
index d8d6879..8b3fcaf 100644
--- a/src/parse-tsconfig/index.ts
+++ b/src/parse-tsconfig/index.ts
@@ -37,7 +37,7 @@ const resolveExtends = (
 	const { compilerOptions } = extendsConfig;
 	if (compilerOptions) {
 		const { baseUrl } = compilerOptions;
-		if (baseUrl) {
+		if (baseUrl != null && !baseUrl.startsWith(configDirPlaceholder)) {
 			compilerOptions.baseUrl = slash(
 				path.relative(
 					fromDirectoryPath,
@@ -166,7 +166,7 @@ const _parseTsconfig = (
 
 		for (const property of normalizedPaths) {
 			const unresolvedPath = compilerOptions[property];
-			if (unresolvedPath) {
+			if (unresolvedPath != null && !unresolvedPath.startsWith(configDirPlaceholder)) {
 				const resolvedBaseUrl = path.resolve(directoryPath, unresolvedPath);
 				const relativeBaseUrl = normalizeRelativePath(path.relative(
 					directoryPath,
@@ -222,14 +222,26 @@ const _parseTsconfig = (
 	return config;
 };
 
-const interpolateConfigDir = (
-	filePath: string,
+function interpolateConfigDir<T extends string | string[]> (
+	filePaths: T,
 	configDir: string,
-) => {
-	if (filePath.startsWith(configDirPlaceholder)) {
-		return slash(path.join(configDir, filePath.slice(configDirPlaceholder.length)));
+	postProcess?: (input: string) => string
+): T extends string ? string : string[];
+function interpolateConfigDir (
+	filePaths: string | string[],
+	configDir: string,
+	postProcess: (input: string) => string = (value) => value
+): string | string[] {
+	if (Array.isArray(filePaths)) {
+		return filePaths.map((filePath) => interpolateConfigDir(filePath, configDir, postProcess));
+	}
+
+	if (filePaths.startsWith(configDirPlaceholder)) {
+		return postProcess(slash(path.join(configDir, filePaths.slice(configDirPlaceholder.length))));
 	}
-};
+
+	return filePaths;
+}
 
 export const parseTsconfig = (
 	tsconfigPath: string,
@@ -238,14 +250,34 @@ export const parseTsconfig = (
 	const resolvedTsconfigPath = path.resolve(tsconfigPath);
 	const config = _parseTsconfig(resolvedTsconfigPath, cache);
 
+	/**
+	 * @see https://github.com/microsoft/TypeScript/issues/57485#issuecomment-2027787456
+	 * exclude paths, as it requires custom processing
+	 */
+	const compilerFieldsWithConfigDir = [
+		'outDir',
+		'declarationDir',
+		'outFile',
+		'rootDir',
+		'baseUrl',
+		'tsBuildInfoFile',
+		'rootDirs',
+		'typeRoots',
+	] as const satisfies Array<keyof NonNullable<TsConfigJson['compilerOptions']>>;
+
 	const configDir = path.dirname(resolvedTsconfigPath);
 	if (config.compilerOptions) {
-		let { outDir } = config.compilerOptions;
-		if (outDir) {
-			const interpolated = interpolateConfigDir(outDir, configDir);
-			if (interpolated) {
-				outDir = normalizeRelativePath(path.relative(configDir, interpolated));
-				config.compilerOptions.outDir = outDir;
+		for (const field of compilerFieldsWithConfigDir) {
+			const value = config.compilerOptions[field];
+
+			if (value != null) {
+				/**
+				 * I used Object.assign instead of the direct assignment to work around TS bug (it fails to infer types correctly).
+				 * @see https://github.com/microsoft/TypeScript/issues/33912
+ 				 */
+				Object.assign(config.compilerOptions, {
+					[field]: interpolateConfigDir(value, configDir, (interpolated) => normalizeRelativePath(path.relative(configDir, interpolated)))
+				});
 			}
 		}
 

From 774fc2ff4076a49754eba31da2d7e948ea3cd2ea Mon Sep 17 00:00:00 2001
From: trikadin <trikadin+github@pm.me>
Date: Wed, 11 Sep 2024 17:07:38 +0400
Subject: [PATCH 2/6] fix linting

---
 src/parse-tsconfig/index.ts | 24 +++++++++++++++---------
 1 file changed, 15 insertions(+), 9 deletions(-)

diff --git a/src/parse-tsconfig/index.ts b/src/parse-tsconfig/index.ts
index 8b3fcaf..a2f7373 100644
--- a/src/parse-tsconfig/index.ts
+++ b/src/parse-tsconfig/index.ts
@@ -37,7 +37,7 @@ const resolveExtends = (
 	const { compilerOptions } = extendsConfig;
 	if (compilerOptions) {
 		const { baseUrl } = compilerOptions;
-		if (baseUrl != null && !baseUrl.startsWith(configDirPlaceholder)) {
+		if (baseUrl && !baseUrl.startsWith(configDirPlaceholder)) {
 			compilerOptions.baseUrl = slash(
 				path.relative(
 					fromDirectoryPath,
@@ -166,7 +166,7 @@ const _parseTsconfig = (
 
 		for (const property of normalizedPaths) {
 			const unresolvedPath = compilerOptions[property];
-			if (unresolvedPath != null && !unresolvedPath.startsWith(configDirPlaceholder)) {
+			if (unresolvedPath && !unresolvedPath.startsWith(configDirPlaceholder)) {
 				const resolvedBaseUrl = path.resolve(directoryPath, unresolvedPath);
 				const relativeBaseUrl = normalizeRelativePath(path.relative(
 					directoryPath,
@@ -222,18 +222,19 @@ const _parseTsconfig = (
 	return config;
 };
 
-function interpolateConfigDir<T extends string | string[]> (
+function interpolateConfigDir<T extends string | string[]>(
 	filePaths: T,
 	configDir: string,
 	postProcess?: (input: string) => string
 ): T extends string ? string : string[];
-function interpolateConfigDir (
+// eslint-disable-next-line pvtnbr/prefer-arrow-functions
+function interpolateConfigDir(
 	filePaths: string | string[],
 	configDir: string,
-	postProcess: (input: string) => string = (value) => value
+	postProcess: (input: string) => string = value => value,
 ): string | string[] {
 	if (Array.isArray(filePaths)) {
-		return filePaths.map((filePath) => interpolateConfigDir(filePath, configDir, postProcess));
+		return filePaths.map(filePath => interpolateConfigDir(filePath, configDir, postProcess));
 	}
 
 	if (filePaths.startsWith(configDirPlaceholder)) {
@@ -270,13 +271,18 @@ export const parseTsconfig = (
 		for (const field of compilerFieldsWithConfigDir) {
 			const value = config.compilerOptions[field];
 
-			if (value != null) {
+			if (value) {
 				/**
-				 * I used Object.assign instead of the direct assignment to work around TS bug (it fails to infer types correctly).
+				 * I used Object.assign instead of the direct assignment to work around TS bug
+				 * (it fails to infer types correctly).
 				 * @see https://github.com/microsoft/TypeScript/issues/33912
  				 */
 				Object.assign(config.compilerOptions, {
-					[field]: interpolateConfigDir(value, configDir, (interpolated) => normalizeRelativePath(path.relative(configDir, interpolated)))
+					[field]: interpolateConfigDir(
+						value,
+						configDir,
+						interpolated => normalizeRelativePath(path.relative(configDir, interpolated)),
+					),
 				});
 			}
 		}

From 29381de7d98fb25b2186bb6daa5c4af543f48e1b Mon Sep 17 00:00:00 2001
From: trikadin <trikadin+github@pm.me>
Date: Wed, 11 Sep 2024 17:28:42 +0400
Subject: [PATCH 3/6] added tests + fixed missing declarationDir in excludes

---
 src/parse-tsconfig/index.ts                   | 31 ++++++++++++-------
 .../parse-tsconfig/extends/merges.spec.ts     |  7 +++++
 2 files changed, 27 insertions(+), 11 deletions(-)

diff --git a/src/parse-tsconfig/index.ts b/src/parse-tsconfig/index.ts
index a2f7373..ce4829c 100644
--- a/src/parse-tsconfig/index.ts
+++ b/src/parse-tsconfig/index.ts
@@ -176,20 +176,29 @@ const _parseTsconfig = (
 			}
 		}
 
-		let { outDir } = compilerOptions;
-		if (outDir) {
-			if (!Array.isArray(config.exclude)) {
-				config.exclude = [];
-			}
+		const outputFields = [
+			'outDir',
+			'declarationDir'
+		] as const satisfies Array<keyof NonNullable<TsConfigJson['compilerOptions']>>;
 
-			if (!config.exclude.includes(outDir)) {
-				config.exclude.push(outDir);
-			}
+		for (const outputField of outputFields) {
+			let outputPath = compilerOptions[outputField];
 
-			if (!outDir.startsWith(configDirPlaceholder)) {
-				outDir = normalizeRelativePath(outDir);
+			if (outputPath) {
+				if (!Array.isArray(config.exclude)) {
+					config.exclude = [];
+				}
+
+				if (!config.exclude.includes(outputPath)) {
+					config.exclude.push(outputPath);
+				}
+
+				if (!outputPath.startsWith(configDirPlaceholder)) {
+					outputPath = normalizeRelativePath(outputPath);
+				}
+
+				compilerOptions[outputField] = outputPath;
 			}
-			compilerOptions.outDir = outDir;
 		}
 	} else {
 		config.compilerOptions = {};
diff --git a/tests/specs/parse-tsconfig/extends/merges.spec.ts b/tests/specs/parse-tsconfig/extends/merges.spec.ts
index c9008bf..6e71fd2 100644
--- a/tests/specs/parse-tsconfig/extends/merges.spec.ts
+++ b/tests/specs/parse-tsconfig/extends/merges.spec.ts
@@ -492,6 +492,13 @@ export default testSuite(({ describe }) => {
 					'tsconfig.json': createTsconfigJson({
 						compilerOptions: {
 							outDir: '${configDir}-asdf/dist',
+							declarationDir: '${configDir}/dist/declaration',
+							outFile: '${configDir}/dist/outfile.js',
+							rootDir: '${configDir}/dist/src',
+							baseUrl: '${configDir}/dist/src',
+							tsBuildInfoFile: '${configDir}/dist/dist.tsbuildinfo',
+							rootDirs: ['${configDir}/src', '${configDir}/static'],
+							typeRoots: ['${configDir}/src/type', '${configDir}/types'],
 							paths: {
 								a: ['${configDir}_a/*'],
 								b: ['ignores/${configDir}/*'],

From 73c3ab236f5731f1bb661f65564789c85fc0b898 Mon Sep 17 00:00:00 2001
From: trikadin <trikadin+github@pm.me>
Date: Wed, 11 Sep 2024 17:31:55 +0400
Subject: [PATCH 4/6] fix linting

---
 src/parse-tsconfig/index.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/parse-tsconfig/index.ts b/src/parse-tsconfig/index.ts
index ce4829c..aabc7f1 100644
--- a/src/parse-tsconfig/index.ts
+++ b/src/parse-tsconfig/index.ts
@@ -178,7 +178,7 @@ const _parseTsconfig = (
 
 		const outputFields = [
 			'outDir',
-			'declarationDir'
+			'declarationDir',
 		] as const satisfies Array<keyof NonNullable<TsConfigJson['compilerOptions']>>;
 
 		for (const outputField of outputFields) {

From 0125a34df213d859d06defb98d3062e4521b9676 Mon Sep 17 00:00:00 2001
From: Hiroki Osame <hiroki.osame@gmail.com>
Date: Thu, 12 Sep 2024 16:21:26 +0900
Subject: [PATCH 5/6] wip

---
 src/parse-tsconfig/index.ts | 111 ++++++++++++++++--------------------
 1 file changed, 50 insertions(+), 61 deletions(-)

diff --git a/src/parse-tsconfig/index.ts b/src/parse-tsconfig/index.ts
index aabc7f1..da90231 100644
--- a/src/parse-tsconfig/index.ts
+++ b/src/parse-tsconfig/index.ts
@@ -6,7 +6,13 @@ import { readJsonc } from '../utils/read-jsonc.js';
 import { implicitBaseUrlSymbol, configDirPlaceholder } from '../utils/constants.js';
 import { resolveExtendsPath } from './resolve-extends-path.js';
 
-const filesProperties = ['files', 'include', 'exclude'] as const;
+const pathRelative = (from: string, to: string) => normalizeRelativePath(path.relative(from, to));
+
+const filesProperties = [
+	'files',
+	'include',
+	'exclude',
+] as const;
 
 const resolveExtends = (
 	extendsPath: string,
@@ -80,6 +86,11 @@ const resolveExtends = (
 	return extendsConfig;
 };
 
+const outputFields = [
+	'outDir',
+	'declarationDir',
+] as const;
+
 const _parseTsconfig = (
 	tsconfigPath: string,
 	cache?: Cache<string>,
@@ -168,19 +179,11 @@ const _parseTsconfig = (
 			const unresolvedPath = compilerOptions[property];
 			if (unresolvedPath && !unresolvedPath.startsWith(configDirPlaceholder)) {
 				const resolvedBaseUrl = path.resolve(directoryPath, unresolvedPath);
-				const relativeBaseUrl = normalizeRelativePath(path.relative(
-					directoryPath,
-					resolvedBaseUrl,
-				));
+				const relativeBaseUrl = pathRelative(directoryPath, resolvedBaseUrl);
 				compilerOptions[property] = relativeBaseUrl;
 			}
 		}
 
-		const outputFields = [
-			'outDir',
-			'declarationDir',
-		] as const satisfies Array<keyof NonNullable<TsConfigJson['compilerOptions']>>;
-
 		for (const outputField of outputFields) {
 			let outputPath = compilerOptions[outputField];
 
@@ -231,27 +234,27 @@ const _parseTsconfig = (
 	return config;
 };
 
-function interpolateConfigDir<T extends string | string[]>(
-	filePaths: T,
+const interpolateConfigDir = (
+	filePath: string,
 	configDir: string,
-	postProcess?: (input: string) => string
-): T extends string ? string : string[];
-// eslint-disable-next-line pvtnbr/prefer-arrow-functions
-function interpolateConfigDir(
-	filePaths: string | string[],
-	configDir: string,
-	postProcess: (input: string) => string = value => value,
-): string | string[] {
-	if (Array.isArray(filePaths)) {
-		return filePaths.map(filePath => interpolateConfigDir(filePath, configDir, postProcess));
-	}
-
-	if (filePaths.startsWith(configDirPlaceholder)) {
-		return postProcess(slash(path.join(configDir, filePaths.slice(configDirPlaceholder.length))));
+) => {
+	if (filePath.startsWith(configDirPlaceholder)) {
+		return slash(path.join(configDir, filePath.slice(configDirPlaceholder.length)));
 	}
+};
 
-	return filePaths;
-}
+/**
+ * @see https://github.com/microsoft/TypeScript/issues/57485#issuecomment-2027787456
+ * exclude paths, as it requires custom processing
+ */
+const compilerFieldsWithConfigDir = [
+	'outDir',
+	'declarationDir',
+	'outFile',
+	'rootDir',
+	'baseUrl',
+	'tsBuildInfoFile',
+] as const;
 
 export const parseTsconfig = (
 	tsconfigPath: string,
@@ -259,44 +262,29 @@ export const parseTsconfig = (
 ): TsConfigJsonResolved => {
 	const resolvedTsconfigPath = path.resolve(tsconfigPath);
 	const config = _parseTsconfig(resolvedTsconfigPath, cache);
-
-	/**
-	 * @see https://github.com/microsoft/TypeScript/issues/57485#issuecomment-2027787456
-	 * exclude paths, as it requires custom processing
-	 */
-	const compilerFieldsWithConfigDir = [
-		'outDir',
-		'declarationDir',
-		'outFile',
-		'rootDir',
-		'baseUrl',
-		'tsBuildInfoFile',
-		'rootDirs',
-		'typeRoots',
-	] as const satisfies Array<keyof NonNullable<TsConfigJson['compilerOptions']>>;
-
 	const configDir = path.dirname(resolvedTsconfigPath);
-	if (config.compilerOptions) {
-		for (const field of compilerFieldsWithConfigDir) {
-			const value = config.compilerOptions[field];
 
+	const { compilerOptions } = config;
+	if (compilerOptions) {
+		for (const property of compilerFieldsWithConfigDir) {
+			const value = compilerOptions[property];
 			if (value) {
-				/**
-				 * I used Object.assign instead of the direct assignment to work around TS bug
-				 * (it fails to infer types correctly).
-				 * @see https://github.com/microsoft/TypeScript/issues/33912
- 				 */
-				Object.assign(config.compilerOptions, {
-					[field]: interpolateConfigDir(
-						value,
-						configDir,
-						interpolated => normalizeRelativePath(path.relative(configDir, interpolated)),
-					),
+				const resolvedPath = interpolateConfigDir(value, configDir);
+				compilerOptions[property] = resolvedPath ? pathRelative(configDir, resolvedPath) : value;
+			}
+		}
+
+		for (const property of ['rootDirs', 'typeRoots'] as const) {
+			const value = compilerOptions[property];
+			if (value) {
+				compilerOptions[property] = value.map((v) => {
+					const resolvedPath = interpolateConfigDir(v, configDir);
+					return resolvedPath ? pathRelative(configDir, resolvedPath) : v;
 				});
 			}
 		}
 
-		const { paths } = config.compilerOptions;
+		const { paths } = compilerOptions;
 		if (paths) {
 			for (const name of Object.keys(paths)) {
 				paths[name] = paths[name].map(
@@ -307,8 +295,9 @@ export const parseTsconfig = (
 	}
 
 	for (const property of filesProperties) {
-		if (config[property]) {
-			config[property] = config[property].map(
+		const value = config[property];
+		if (value) {
+			config[property] = value.map(
 				filePath => interpolateConfigDir(filePath, configDir) ?? filePath,
 			);
 		}

From 2b5b72181949505f81529b6dc035e4b8c15589b6 Mon Sep 17 00:00:00 2001
From: Hiroki Osame <hiroki.osame@gmail.com>
Date: Thu, 12 Sep 2024 16:27:36 +0900
Subject: [PATCH 6/6] wip

---
 tests/specs/parse-tsconfig/parses.spec.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/tests/specs/parse-tsconfig/parses.spec.ts b/tests/specs/parse-tsconfig/parses.spec.ts
index 8f1f649..4398405 100644
--- a/tests/specs/parse-tsconfig/parses.spec.ts
+++ b/tests/specs/parse-tsconfig/parses.spec.ts
@@ -73,6 +73,7 @@ export default testSuite(({ describe }) => {
 						esModuleInterop: true,
 						declaration: true,
 						outDir: 'dist',
+						declarationDir: 'dist-declaration',
 						strict: true,
 						target: 'esnext',
 						rootDir: 'root-dir',