Skip to content

Commit 985a475

Browse files
committed
Allow $configDir as a string to be substituted in config file options
1 parent 50622ff commit 985a475

32 files changed

+3488
-34
lines changed

src/compiler/commandLineParser.ts

Lines changed: 150 additions & 14 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
},
@@ -1033,6 +1036,7 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
10331036
name: "paths",
10341037
type: "object",
10351038
affectsModuleResolution: true,
1039+
allowConfigDirTemplateSubstitution: true,
10361040
isTSConfigOnly: true,
10371041
category: Diagnostics.Modules,
10381042
description: Diagnostics.Specify_a_set_of_entries_that_re_map_imports_to_additional_lookup_locations,
@@ -1050,6 +1054,7 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
10501054
isFilePath: true,
10511055
},
10521056
affectsModuleResolution: true,
1057+
allowConfigDirTemplateSubstitution: true,
10531058
category: Diagnostics.Modules,
10541059
description: Diagnostics.Allow_multiple_folders_to_be_treated_as_one_when_resolving_modules,
10551060
transpileOptionValue: undefined,
@@ -1064,6 +1069,7 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
10641069
isFilePath: true,
10651070
},
10661071
affectsModuleResolution: true,
1072+
allowConfigDirTemplateSubstitution: true,
10671073
category: Diagnostics.Modules,
10681074
description: Diagnostics.Specify_multiple_folders_that_act_like_Slashnode_modules_Slash_types,
10691075
},
@@ -1599,6 +1605,15 @@ export const optionsAffectingProgramStructure: readonly CommandLineOption[] = op
15991605
/** @internal */
16001606
export const transpileOptionValueCompilerOptions: readonly CommandLineOption[] = optionDeclarations.filter(option => hasProperty(option, "transpileOptionValue"));
16011607

1608+
/** @internal */
1609+
export const configDirTemplateSubstitutionOptions: readonly CommandLineOption[] = optionDeclarations.filter(
1610+
option => option.allowConfigDirTemplateSubstitution || (!option.isCommandLineOnly && option.isFilePath),
1611+
);
1612+
/** @internal */
1613+
export const configDirTemplateSubstitutionWatchOptions: readonly CommandLineOption[] = optionsForWatch.filter(
1614+
option => option.allowConfigDirTemplateSubstitution || (!option.isCommandLineOnly && option.isFilePath),
1615+
);
1616+
16021617
// Build related options
16031618
/** @internal */
16041619
export const optionsForBuild: CommandLineOption[] = [
@@ -2627,6 +2642,9 @@ function serializeOptionBaseObject(
26272642
if (pathOptions && optionDefinition.isFilePath) {
26282643
result.set(name, getRelativePathFromFile(pathOptions.configFilePath, getNormalizedAbsolutePath(value as string, getDirectoryPath(pathOptions.configFilePath)), getCanonicalFileName!));
26292644
}
2645+
else if (pathOptions && optionDefinition.type === "list" && optionDefinition.element.isFilePath) {
2646+
result.set(name, (value as string[]).map(v => getRelativePathFromFile(pathOptions.configFilePath, getNormalizedAbsolutePath(v, getDirectoryPath(pathOptions.configFilePath)), getCanonicalFileName!)));
2647+
}
26302648
else {
26312649
result.set(name, value);
26322650
}
@@ -2890,16 +2908,17 @@ function parseJsonConfigFileContentWorker(
28902908
const parsedConfig = parseConfig(json, sourceFile, host, basePath, configFileName, resolutionStack, errors, extendedConfigCache);
28912909
const { raw } = parsedConfig;
28922910
const options = extend(existingOptions, parsedConfig.options || {});
2893-
const watchOptions = existingWatchOptions && parsedConfig.watchOptions ?
2911+
let watchOptions = existingWatchOptions && parsedConfig.watchOptions ?
28942912
extend(existingWatchOptions, parsedConfig.watchOptions) :
28952913
parsedConfig.watchOptions || existingWatchOptions;
2896-
2914+
handleOptionConfigDirTemplateSubstitution(options, configDirTemplateSubstitutionOptions, basePath);
2915+
watchOptions = handleWatchOptionsConfigDirTemplateSubstitution(watchOptions, basePath, !existingWatchOptions || !parsedConfig.watchOptions);
28972916
options.configFilePath = configFileName && normalizeSlashes(configFileName);
2917+
const basePathForFileNames = normalizePath(configFileName ? directoryOfCombinedPath(configFileName, basePath) : basePath);
28982918
const configFileSpecs = getConfigFileSpecs();
28992919
if (sourceFile) sourceFile.configFileSpecs = configFileSpecs;
29002920
setConfigFileInOptions(options, sourceFile);
29012921

2902-
const basePathForFileNames = normalizePath(configFileName ? directoryOfCombinedPath(configFileName, basePath) : basePath);
29032922
return {
29042923
options,
29052924
watchOptions,
@@ -2954,27 +2973,48 @@ function parseJsonConfigFileContentWorker(
29542973
includeSpecs = [defaultIncludeSpec];
29552974
isDefaultIncludeSpec = true;
29562975
}
2976+
let validatedIncludeSpecsBeforeSubstitution: readonly string[] | undefined, validatedExcludeSpecsBeforeSubstitution: readonly string[] | undefined;
29572977
let validatedIncludeSpecs: readonly string[] | undefined, validatedExcludeSpecs: readonly string[] | undefined;
29582978

29592979
// The exclude spec list is converted into a regular expression, which allows us to quickly
29602980
// test whether a file or directory should be excluded before recursively traversing the
29612981
// file system.
29622982

29632983
if (includeSpecs) {
2964-
validatedIncludeSpecs = validateSpecs(includeSpecs, errors, /*disallowTrailingRecursion*/ true, sourceFile, "include");
2984+
validatedIncludeSpecsBeforeSubstitution = validateSpecs(includeSpecs, errors, /*disallowTrailingRecursion*/ true, sourceFile, "include");
2985+
validatedIncludeSpecs = getSubstitutedStringArrayWithConfigDirTemplate(
2986+
validatedIncludeSpecsBeforeSubstitution,
2987+
basePathForFileNames,
2988+
/*createCopyOnSubstitute*/ true,
2989+
) || validatedIncludeSpecsBeforeSubstitution;
29652990
}
29662991

29672992
if (excludeSpecs) {
2968-
validatedExcludeSpecs = validateSpecs(excludeSpecs, errors, /*disallowTrailingRecursion*/ false, sourceFile, "exclude");
2993+
validatedExcludeSpecsBeforeSubstitution = validateSpecs(excludeSpecs, errors, /*disallowTrailingRecursion*/ false, sourceFile, "exclude");
2994+
validatedExcludeSpecs = getSubstitutedStringArrayWithConfigDirTemplate(
2995+
validatedExcludeSpecsBeforeSubstitution,
2996+
basePathForFileNames,
2997+
/*createCopyOnSubstitute*/ true,
2998+
) || validatedExcludeSpecsBeforeSubstitution;
29692999
}
29703000

3001+
const validatedFilesSpecBeforeSubstitution = filter(filesSpecs, isString);
3002+
const validatedFilesSpec = getSubstitutedStringArrayWithConfigDirTemplate(
3003+
validatedFilesSpecBeforeSubstitution,
3004+
basePathForFileNames,
3005+
/*createCopyOnSubstitute*/ true,
3006+
) || validatedFilesSpecBeforeSubstitution;
3007+
29713008
return {
29723009
filesSpecs,
29733010
includeSpecs,
29743011
excludeSpecs,
2975-
validatedFilesSpec: filter(filesSpecs, isString),
3012+
validatedFilesSpec,
29763013
validatedIncludeSpecs,
29773014
validatedExcludeSpecs,
3015+
validatedFilesSpecBeforeSubstitution,
3016+
validatedIncludeSpecsBeforeSubstitution,
3017+
validatedExcludeSpecsBeforeSubstitution,
29783018
pathPatterns: undefined, // Initialized on first use
29793019
isDefaultIncludeSpec,
29803020
};
@@ -3042,6 +3082,97 @@ function parseJsonConfigFileContentWorker(
30423082
}
30433083
}
30443084

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

@@ -3162,12 +3294,15 @@ function parseConfig(
31623294
const extendsRaw = extendedConfig.raw;
31633295
let relativeDifference: string | undefined;
31643296
const setPropertyInResultIfNotUndefined = (propertyName: "include" | "exclude" | "files") => {
3297+
if (ownConfig.raw[propertyName]) return; // No need to calculate if already set in own config
31653298
if (extendsRaw[propertyName]) {
31663299
result[propertyName] = map(extendsRaw[propertyName], (path: string) =>
3167-
isRootedDiskPath(path) ? path : combinePaths(
3168-
relativeDifference ||= convertToRelativePath(getDirectoryPath(extendedConfigPath), basePath, createGetCanonicalFileName(host.useCaseSensitiveFileNames)),
3169-
path,
3170-
));
3300+
startsWithConfigDirTemplate(path) || isRootedDiskPath(path) ?
3301+
path :
3302+
combinePaths(
3303+
relativeDifference ||= convertToRelativePath(getDirectoryPath(extendedConfigPath), basePath, createGetCanonicalFileName(host.useCaseSensitiveFileNames)),
3304+
path,
3305+
));
31713306
}
31723307
};
31733308
setPropertyInResultIfNotUndefined("include");
@@ -3526,7 +3661,8 @@ export function convertJsonOption(
35263661

35273662
function normalizeNonListOptionValue(option: CommandLineOption, basePath: string, value: any): CompilerOptionsValue {
35283663
if (option.isFilePath) {
3529-
value = getNormalizedAbsolutePath(value, basePath);
3664+
value = normalizeSlashes(value);
3665+
value = !startsWithConfigDirTemplate(value) ? getNormalizedAbsolutePath(value, basePath) : value;
35303666
if (value === "") {
35313667
value = ".";
35323668
}

src/compiler/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7349,6 +7349,9 @@ export interface ConfigFileSpecs {
73497349
validatedFilesSpec: readonly string[] | undefined;
73507350
validatedIncludeSpecs: readonly string[] | undefined;
73517351
validatedExcludeSpecs: readonly string[] | undefined;
7352+
validatedFilesSpecBeforeSubstitution: readonly string[] | undefined;
7353+
validatedIncludeSpecsBeforeSubstitution: readonly string[] | undefined;
7354+
validatedExcludeSpecsBeforeSubstitution: readonly string[] | undefined;
73527355
pathPatterns: readonly (string | Pattern)[] | undefined;
73537356
isDefaultIncludeSpec: boolean;
73547357
}
@@ -7395,7 +7398,8 @@ export interface CommandLineOptionBase {
73957398
affectsBuildInfo?: true; // true if this options should be emitted in buildInfo
73967399
transpileOptionValue?: boolean | undefined; // If set this means that the option should be set to this value when transpiling
73977400
extraValidation?: (value: CompilerOptionsValue) => [DiagnosticMessage, ...string[]] | undefined; // Additional validation to be performed for the value to be valid
7398-
disallowNullOrUndefined?: true; // If set option does not allow setting null
7401+
disallowNullOrUndefined?: true; // If set option does not allow setting null
7402+
allowConfigDirTemplateSubstitution?: true; // If set option allows substitution of `${configDir}` in the value
73997403
}
74007404

74017405
/** @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,
@@ -416,7 +417,8 @@ export function getMatchedFileSpec(program: Program, fileName: string) {
416417

417418
const filePath = program.getCanonicalFileName(fileName);
418419
const basePath = getDirectoryPath(getNormalizedAbsolutePath(configFile.fileName, program.getCurrentDirectory()));
419-
return find(configFile.configFileSpecs.validatedFilesSpec, fileSpec => program.getCanonicalFileName(getNormalizedAbsolutePath(fileSpec, basePath)) === filePath);
420+
const index = findIndex(configFile.configFileSpecs.validatedFilesSpec, fileSpec => program.getCanonicalFileName(getNormalizedAbsolutePath(fileSpec, basePath)) === filePath);
421+
return index !== -1 ? configFile.configFileSpecs.validatedFilesSpecBeforeSubstitution![index] : undefined;
420422
}
421423

422424
/** @internal */
@@ -430,11 +432,12 @@ export function getMatchedIncludeSpec(program: Program, fileName: string) {
430432
const isJsonFile = fileExtensionIs(fileName, Extension.Json);
431433
const basePath = getDirectoryPath(getNormalizedAbsolutePath(configFile.fileName, program.getCurrentDirectory()));
432434
const useCaseSensitiveFileNames = program.useCaseSensitiveFileNames();
433-
return find(configFile?.configFileSpecs?.validatedIncludeSpecs, includeSpec => {
435+
const index = findIndex(configFile?.configFileSpecs?.validatedIncludeSpecs, includeSpec => {
434436
if (isJsonFile && !endsWith(includeSpec, Extension.Json)) return false;
435437
const pattern = getPatternFromSpec(includeSpec, basePath, "files");
436438
return !!pattern && getRegexFromPattern(`(${pattern})$`, useCaseSensitiveFileNames).test(fileName);
437439
});
440+
return index !== -1 ? configFile.configFileSpecs.validatedIncludeSpecsBeforeSubstitution![index] : undefined;
438441
}
439442

440443
/** @internal */

0 commit comments

Comments
 (0)