Skip to content

Commit cbae6cf

Browse files
authored
Add template variable ${configDir} for substitution of config files directory path (#58042)
1 parent 3d52392 commit cbae6cf

File tree

88 files changed

+4167
-233
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+4167
-233
lines changed

src/compiler/commandLineParser.ts

Lines changed: 142 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
getFileMatcherPatterns,
5454
getLocaleSpecificMessage,
5555
getNormalizedAbsolutePath,
56+
getOwnKeys,
5657
getRegexFromPattern,
5758
getRegularExpressionForWildcard,
5859
getRegularExpressionsForWildcards,
@@ -313,6 +314,7 @@ export const optionsForWatch: CommandLineOption[] = [
313314
isFilePath: true,
314315
extraValidation: specToDiagnostic,
315316
},
317+
allowConfigDirTemplateSubstitution: true,
316318
category: Diagnostics.Watch_and_Build_Modes,
317319
description: Diagnostics.Remove_a_list_of_directories_from_the_watch_process,
318320
},
@@ -325,6 +327,7 @@ export const optionsForWatch: CommandLineOption[] = [
325327
isFilePath: true,
326328
extraValidation: specToDiagnostic,
327329
},
330+
allowConfigDirTemplateSubstitution: true,
328331
category: Diagnostics.Watch_and_Build_Modes,
329332
description: Diagnostics.Remove_a_list_of_files_from_the_watch_mode_s_processing,
330333
},
@@ -1034,6 +1037,7 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
10341037
name: "paths",
10351038
type: "object",
10361039
affectsModuleResolution: true,
1040+
allowConfigDirTemplateSubstitution: true,
10371041
isTSConfigOnly: true,
10381042
category: Diagnostics.Modules,
10391043
description: Diagnostics.Specify_a_set_of_entries_that_re_map_imports_to_additional_lookup_locations,
@@ -1051,6 +1055,7 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
10511055
isFilePath: true,
10521056
},
10531057
affectsModuleResolution: true,
1058+
allowConfigDirTemplateSubstitution: true,
10541059
category: Diagnostics.Modules,
10551060
description: Diagnostics.Allow_multiple_folders_to_be_treated_as_one_when_resolving_modules,
10561061
transpileOptionValue: undefined,
@@ -1065,6 +1070,7 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
10651070
isFilePath: true,
10661071
},
10671072
affectsModuleResolution: true,
1073+
allowConfigDirTemplateSubstitution: true,
10681074
category: Diagnostics.Modules,
10691075
description: Diagnostics.Specify_multiple_folders_that_act_like_Slashnode_modules_Slash_types,
10701076
},
@@ -1600,6 +1606,15 @@ export const optionsAffectingProgramStructure: readonly CommandLineOption[] = op
16001606
/** @internal */
16011607
export const transpileOptionValueCompilerOptions: readonly CommandLineOption[] = optionDeclarations.filter(option => hasProperty(option, "transpileOptionValue"));
16021608

1609+
/** @internal */
1610+
export const configDirTemplateSubstitutionOptions: readonly CommandLineOption[] = optionDeclarations.filter(
1611+
option => option.allowConfigDirTemplateSubstitution || (!option.isCommandLineOnly && option.isFilePath),
1612+
);
1613+
/** @internal */
1614+
export const configDirTemplateSubstitutionWatchOptions: readonly CommandLineOption[] = optionsForWatch.filter(
1615+
option => option.allowConfigDirTemplateSubstitution || (!option.isCommandLineOnly && option.isFilePath),
1616+
);
1617+
16031618
// Build related options
16041619
/** @internal */
16051620
export const optionsForBuild: CommandLineOption[] = [
@@ -2628,6 +2643,9 @@ function serializeOptionBaseObject(
26282643
if (pathOptions && optionDefinition.isFilePath) {
26292644
result.set(name, getRelativePathFromFile(pathOptions.configFilePath, getNormalizedAbsolutePath(value as string, getDirectoryPath(pathOptions.configFilePath)), getCanonicalFileName!));
26302645
}
2646+
else if (pathOptions && optionDefinition.type === "list" && optionDefinition.element.isFilePath) {
2647+
result.set(name, (value as string[]).map(v => getRelativePathFromFile(pathOptions.configFilePath, getNormalizedAbsolutePath(v, getDirectoryPath(pathOptions.configFilePath)), getCanonicalFileName!)));
2648+
}
26312649
else {
26322650
result.set(name, value);
26332651
}
@@ -2890,17 +2908,23 @@ function parseJsonConfigFileContentWorker(
28902908

28912909
const parsedConfig = parseConfig(json, sourceFile, host, basePath, configFileName, resolutionStack, errors, extendedConfigCache);
28922910
const { raw } = parsedConfig;
2893-
const options = extend(existingOptions, parsedConfig.options || {});
2894-
const watchOptions = existingWatchOptions && parsedConfig.watchOptions ?
2895-
extend(existingWatchOptions, parsedConfig.watchOptions) :
2896-
parsedConfig.watchOptions || existingWatchOptions;
2897-
2911+
const options = handleOptionConfigDirTemplateSubstitution(
2912+
extend(existingOptions, parsedConfig.options || {}),
2913+
configDirTemplateSubstitutionOptions,
2914+
basePath,
2915+
) as CompilerOptions;
2916+
const watchOptions = handleWatchOptionsConfigDirTemplateSubstitution(
2917+
existingWatchOptions && parsedConfig.watchOptions ?
2918+
extend(existingWatchOptions, parsedConfig.watchOptions) :
2919+
parsedConfig.watchOptions || existingWatchOptions,
2920+
basePath,
2921+
);
28982922
options.configFilePath = configFileName && normalizeSlashes(configFileName);
2923+
const basePathForFileNames = normalizePath(configFileName ? directoryOfCombinedPath(configFileName, basePath) : basePath);
28992924
const configFileSpecs = getConfigFileSpecs();
29002925
if (sourceFile) sourceFile.configFileSpecs = configFileSpecs;
29012926
setConfigFileInOptions(options, sourceFile);
29022927

2903-
const basePathForFileNames = normalizePath(configFileName ? directoryOfCombinedPath(configFileName, basePath) : basePath);
29042928
return {
29052929
options,
29062930
watchOptions,
@@ -2955,27 +2979,45 @@ function parseJsonConfigFileContentWorker(
29552979
includeSpecs = [defaultIncludeSpec];
29562980
isDefaultIncludeSpec = true;
29572981
}
2982+
let validatedIncludeSpecsBeforeSubstitution: readonly string[] | undefined, validatedExcludeSpecsBeforeSubstitution: readonly string[] | undefined;
29582983
let validatedIncludeSpecs: readonly string[] | undefined, validatedExcludeSpecs: readonly string[] | undefined;
29592984

29602985
// The exclude spec list is converted into a regular expression, which allows us to quickly
29612986
// test whether a file or directory should be excluded before recursively traversing the
29622987
// file system.
29632988

29642989
if (includeSpecs) {
2965-
validatedIncludeSpecs = validateSpecs(includeSpecs, errors, /*disallowTrailingRecursion*/ true, sourceFile, "include");
2990+
validatedIncludeSpecsBeforeSubstitution = validateSpecs(includeSpecs, errors, /*disallowTrailingRecursion*/ true, sourceFile, "include");
2991+
validatedIncludeSpecs = getSubstitutedStringArrayWithConfigDirTemplate(
2992+
validatedIncludeSpecsBeforeSubstitution,
2993+
basePathForFileNames,
2994+
) || validatedIncludeSpecsBeforeSubstitution;
29662995
}
29672996

29682997
if (excludeSpecs) {
2969-
validatedExcludeSpecs = validateSpecs(excludeSpecs, errors, /*disallowTrailingRecursion*/ false, sourceFile, "exclude");
2998+
validatedExcludeSpecsBeforeSubstitution = validateSpecs(excludeSpecs, errors, /*disallowTrailingRecursion*/ false, sourceFile, "exclude");
2999+
validatedExcludeSpecs = getSubstitutedStringArrayWithConfigDirTemplate(
3000+
validatedExcludeSpecsBeforeSubstitution,
3001+
basePathForFileNames,
3002+
) || validatedExcludeSpecsBeforeSubstitution;
29703003
}
29713004

3005+
const validatedFilesSpecBeforeSubstitution = filter(filesSpecs, isString);
3006+
const validatedFilesSpec = getSubstitutedStringArrayWithConfigDirTemplate(
3007+
validatedFilesSpecBeforeSubstitution,
3008+
basePathForFileNames,
3009+
) || validatedFilesSpecBeforeSubstitution;
3010+
29723011
return {
29733012
filesSpecs,
29743013
includeSpecs,
29753014
excludeSpecs,
2976-
validatedFilesSpec: filter(filesSpecs, isString),
3015+
validatedFilesSpec,
29773016
validatedIncludeSpecs,
29783017
validatedExcludeSpecs,
3018+
validatedFilesSpecBeforeSubstitution,
3019+
validatedIncludeSpecsBeforeSubstitution,
3020+
validatedExcludeSpecsBeforeSubstitution,
29793021
pathPatterns: undefined, // Initialized on first use
29803022
isDefaultIncludeSpec,
29813023
};
@@ -3043,6 +3085,84 @@ function parseJsonConfigFileContentWorker(
30433085
}
30443086
}
30453087

3088+
/** @internal */
3089+
export function handleWatchOptionsConfigDirTemplateSubstitution(
3090+
watchOptions: WatchOptions | undefined,
3091+
basePath: string,
3092+
) {
3093+
return handleOptionConfigDirTemplateSubstitution(watchOptions, configDirTemplateSubstitutionWatchOptions, basePath) as WatchOptions | undefined;
3094+
}
3095+
3096+
function handleOptionConfigDirTemplateSubstitution(
3097+
options: OptionsBase | undefined,
3098+
optionDeclarations: readonly CommandLineOption[],
3099+
basePath: string,
3100+
) {
3101+
if (!options) return options;
3102+
let result: OptionsBase | undefined;
3103+
for (const option of optionDeclarations) {
3104+
if (options[option.name] !== undefined) {
3105+
const value = options[option.name];
3106+
switch (option.type) {
3107+
case "string":
3108+
Debug.assert(option.isFilePath);
3109+
if (startsWithConfigDirTemplate(value)) {
3110+
setOptionValue(option, getSubstitutedPathWithConfigDirTemplate(value, basePath));
3111+
}
3112+
break;
3113+
case "list":
3114+
Debug.assert(option.element.isFilePath);
3115+
const listResult = getSubstitutedStringArrayWithConfigDirTemplate(value as string[], basePath);
3116+
if (listResult) setOptionValue(option, listResult);
3117+
break;
3118+
case "object":
3119+
Debug.assert(option.name === "paths");
3120+
const objectResult = getSubstitutedMapLikeOfStringArrayWithConfigDirTemplate(value as MapLike<string[]>, basePath);
3121+
if (objectResult) setOptionValue(option, objectResult);
3122+
break;
3123+
default:
3124+
Debug.fail("option type not supported");
3125+
}
3126+
}
3127+
}
3128+
return result || options;
3129+
3130+
function setOptionValue(option: CommandLineOption, value: CompilerOptionsValue) {
3131+
(result ??= assign({}, options))[option.name] = value;
3132+
}
3133+
}
3134+
3135+
const configDirTemplate = `\${configDir}`;
3136+
function startsWithConfigDirTemplate(value: any): value is string {
3137+
return isString(value) && startsWith(value, configDirTemplate, /*ignoreCase*/ true);
3138+
}
3139+
3140+
function getSubstitutedPathWithConfigDirTemplate(value: string, basePath: string) {
3141+
return getNormalizedAbsolutePath(value.replace(configDirTemplate, "./"), basePath);
3142+
}
3143+
3144+
function getSubstitutedStringArrayWithConfigDirTemplate(list: readonly string[] | undefined, basePath: string) {
3145+
if (!list) return list;
3146+
let result: string[] | undefined;
3147+
list.forEach((element, index) => {
3148+
if (!startsWithConfigDirTemplate(element)) return;
3149+
(result ??= list.slice())[index] = getSubstitutedPathWithConfigDirTemplate(element, basePath);
3150+
});
3151+
return result;
3152+
}
3153+
3154+
function getSubstitutedMapLikeOfStringArrayWithConfigDirTemplate(mapLike: MapLike<string[]>, basePath: string) {
3155+
let result: MapLike<string[]> | undefined;
3156+
const ownKeys = getOwnKeys(mapLike);
3157+
ownKeys.forEach(key => {
3158+
if (!isArray(mapLike[key])) return;
3159+
const subStitution = getSubstitutedStringArrayWithConfigDirTemplate(mapLike[key], basePath);
3160+
if (!subStitution) return;
3161+
(result ??= assign({}, mapLike))[key] = subStitution;
3162+
});
3163+
return result;
3164+
}
3165+
30463166
function isErrorNoInputFiles(error: Diagnostic) {
30473167
return error.code === Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code;
30483168
}
@@ -3144,9 +3264,10 @@ function parseConfig(
31443264
else {
31453265
ownConfig.extendedConfigPath.forEach(extendedConfigPath => applyExtendedConfig(result, extendedConfigPath));
31463266
}
3147-
if (!ownConfig.raw.include && result.include) ownConfig.raw.include = result.include;
3148-
if (!ownConfig.raw.exclude && result.exclude) ownConfig.raw.exclude = result.exclude;
3149-
if (!ownConfig.raw.files && result.files) ownConfig.raw.files = result.files;
3267+
if (result.include) ownConfig.raw.include = result.include;
3268+
if (result.exclude) ownConfig.raw.exclude = result.exclude;
3269+
if (result.files) ownConfig.raw.files = result.files;
3270+
31503271
if (ownConfig.raw.compileOnSave === undefined && result.compileOnSave) ownConfig.raw.compileOnSave = result.compileOnSave;
31513272
if (sourceFile && result.extendedSourceFiles) sourceFile.extendedSourceFiles = arrayFrom(result.extendedSourceFiles.keys());
31523273

@@ -3163,12 +3284,15 @@ function parseConfig(
31633284
const extendsRaw = extendedConfig.raw;
31643285
let relativeDifference: string | undefined;
31653286
const setPropertyInResultIfNotUndefined = (propertyName: "include" | "exclude" | "files") => {
3287+
if (ownConfig.raw[propertyName]) return; // No need to calculate if already set in own config
31663288
if (extendsRaw[propertyName]) {
31673289
result[propertyName] = map(extendsRaw[propertyName], (path: string) =>
3168-
isRootedDiskPath(path) ? path : combinePaths(
3169-
relativeDifference ||= convertToRelativePath(getDirectoryPath(extendedConfigPath), basePath, createGetCanonicalFileName(host.useCaseSensitiveFileNames)),
3170-
path,
3171-
));
3290+
startsWithConfigDirTemplate(path) || isRootedDiskPath(path) ?
3291+
path :
3292+
combinePaths(
3293+
relativeDifference ||= convertToRelativePath(getDirectoryPath(extendedConfigPath), basePath, createGetCanonicalFileName(host.useCaseSensitiveFileNames)),
3294+
path,
3295+
));
31723296
}
31733297
};
31743298
setPropertyInResultIfNotUndefined("include");
@@ -3527,7 +3651,8 @@ export function convertJsonOption(
35273651

35283652
function normalizeNonListOptionValue(option: CommandLineOption, basePath: string, value: any): CompilerOptionsValue {
35293653
if (option.isFilePath) {
3530-
value = getNormalizedAbsolutePath(value, basePath);
3654+
value = normalizeSlashes(value);
3655+
value = !startsWithConfigDirTemplate(value) ? getNormalizedAbsolutePath(value, basePath) : value;
35313656
if (value === "") {
35323657
value = ".";
35333658
}

src/compiler/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7471,6 +7471,9 @@ export interface ConfigFileSpecs {
74717471
validatedFilesSpec: readonly string[] | undefined;
74727472
validatedIncludeSpecs: readonly string[] | undefined;
74737473
validatedExcludeSpecs: readonly string[] | undefined;
7474+
validatedFilesSpecBeforeSubstitution: readonly string[] | undefined;
7475+
validatedIncludeSpecsBeforeSubstitution: readonly string[] | undefined;
7476+
validatedExcludeSpecsBeforeSubstitution: readonly string[] | undefined;
74747477
pathPatterns: readonly (string | Pattern)[] | undefined;
74757478
isDefaultIncludeSpec: boolean;
74767479
}
@@ -7517,7 +7520,8 @@ export interface CommandLineOptionBase {
75177520
affectsBuildInfo?: true; // true if this options should be emitted in buildInfo
75187521
transpileOptionValue?: boolean | undefined; // If set this means that the option should be set to this value when transpiling
75197522
extraValidation?: (value: CompilerOptionsValue) => [DiagnosticMessage, ...string[]] | undefined; // Additional validation to be performed for the value to be valid
7520-
disallowNullOrUndefined?: true; // If set option does not allow setting null
7523+
disallowNullOrUndefined?: true; // If set option does not allow setting null
7524+
allowConfigDirTemplateSubstitution?: true; // If set option allows substitution of `${configDir}` in the value
75217525
}
75227526

75237527
/** @internal */

src/compiler/watch.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
FileWatcher,
4545
filter,
4646
find,
47+
findIndex,
4748
flattenDiagnosticMessageText,
4849
forEach,
4950
forEachEntry,
@@ -418,7 +419,8 @@ export function getMatchedFileSpec(program: Program, fileName: string) {
418419

419420
const filePath = program.getCanonicalFileName(fileName);
420421
const basePath = getDirectoryPath(getNormalizedAbsolutePath(configFile.fileName, program.getCurrentDirectory()));
421-
return find(configFile.configFileSpecs.validatedFilesSpec, fileSpec => program.getCanonicalFileName(getNormalizedAbsolutePath(fileSpec, basePath)) === filePath);
422+
const index = findIndex(configFile.configFileSpecs.validatedFilesSpec, fileSpec => program.getCanonicalFileName(getNormalizedAbsolutePath(fileSpec, basePath)) === filePath);
423+
return index !== -1 ? configFile.configFileSpecs.validatedFilesSpecBeforeSubstitution![index] : undefined;
422424
}
423425

424426
/** @internal */
@@ -432,11 +434,12 @@ export function getMatchedIncludeSpec(program: Program, fileName: string) {
432434
const isJsonFile = fileExtensionIs(fileName, Extension.Json);
433435
const basePath = getDirectoryPath(getNormalizedAbsolutePath(configFile.fileName, program.getCurrentDirectory()));
434436
const useCaseSensitiveFileNames = program.useCaseSensitiveFileNames();
435-
return find(configFile?.configFileSpecs?.validatedIncludeSpecs, includeSpec => {
437+
const index = findIndex(configFile?.configFileSpecs?.validatedIncludeSpecs, includeSpec => {
436438
if (isJsonFile && !endsWith(includeSpec, Extension.Json)) return false;
437439
const pattern = getPatternFromSpec(includeSpec, basePath, "files");
438440
return !!pattern && getRegexFromPattern(`(${pattern})$`, useCaseSensitiveFileNames).test(fileName);
439441
});
442+
return index !== -1 ? configFile.configFileSpecs.validatedIncludeSpecsBeforeSubstitution![index] : undefined;
440443
}
441444

442445
/** @internal */

0 commit comments

Comments
 (0)