Skip to content

Commit

Permalink
Add template variable ${configDir} for substitution of config files d…
Browse files Browse the repository at this point in the history
…irectory path (#58042)
  • Loading branch information
sheetalkamat committed Apr 16, 2024
1 parent 3d52392 commit cbae6cf
Show file tree
Hide file tree
Showing 88 changed files with 4,167 additions and 233 deletions.
159 changes: 142 additions & 17 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
getFileMatcherPatterns,
getLocaleSpecificMessage,
getNormalizedAbsolutePath,
getOwnKeys,
getRegexFromPattern,
getRegularExpressionForWildcard,
getRegularExpressionsForWildcards,
Expand Down Expand Up @@ -313,6 +314,7 @@ export const optionsForWatch: CommandLineOption[] = [
isFilePath: true,
extraValidation: specToDiagnostic,
},
allowConfigDirTemplateSubstitution: true,
category: Diagnostics.Watch_and_Build_Modes,
description: Diagnostics.Remove_a_list_of_directories_from_the_watch_process,
},
Expand All @@ -325,6 +327,7 @@ export const optionsForWatch: CommandLineOption[] = [
isFilePath: true,
extraValidation: specToDiagnostic,
},
allowConfigDirTemplateSubstitution: true,
category: Diagnostics.Watch_and_Build_Modes,
description: Diagnostics.Remove_a_list_of_files_from_the_watch_mode_s_processing,
},
Expand Down Expand Up @@ -1034,6 +1037,7 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
name: "paths",
type: "object",
affectsModuleResolution: true,
allowConfigDirTemplateSubstitution: true,
isTSConfigOnly: true,
category: Diagnostics.Modules,
description: Diagnostics.Specify_a_set_of_entries_that_re_map_imports_to_additional_lookup_locations,
Expand All @@ -1051,6 +1055,7 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
isFilePath: true,
},
affectsModuleResolution: true,
allowConfigDirTemplateSubstitution: true,
category: Diagnostics.Modules,
description: Diagnostics.Allow_multiple_folders_to_be_treated_as_one_when_resolving_modules,
transpileOptionValue: undefined,
Expand All @@ -1065,6 +1070,7 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
isFilePath: true,
},
affectsModuleResolution: true,
allowConfigDirTemplateSubstitution: true,
category: Diagnostics.Modules,
description: Diagnostics.Specify_multiple_folders_that_act_like_Slashnode_modules_Slash_types,
},
Expand Down Expand Up @@ -1600,6 +1606,15 @@ export const optionsAffectingProgramStructure: readonly CommandLineOption[] = op
/** @internal */
export const transpileOptionValueCompilerOptions: readonly CommandLineOption[] = optionDeclarations.filter(option => hasProperty(option, "transpileOptionValue"));

/** @internal */
export const configDirTemplateSubstitutionOptions: readonly CommandLineOption[] = optionDeclarations.filter(
option => option.allowConfigDirTemplateSubstitution || (!option.isCommandLineOnly && option.isFilePath),
);
/** @internal */
export const configDirTemplateSubstitutionWatchOptions: readonly CommandLineOption[] = optionsForWatch.filter(
option => option.allowConfigDirTemplateSubstitution || (!option.isCommandLineOnly && option.isFilePath),
);

// Build related options
/** @internal */
export const optionsForBuild: CommandLineOption[] = [
Expand Down Expand Up @@ -2628,6 +2643,9 @@ function serializeOptionBaseObject(
if (pathOptions && optionDefinition.isFilePath) {
result.set(name, getRelativePathFromFile(pathOptions.configFilePath, getNormalizedAbsolutePath(value as string, getDirectoryPath(pathOptions.configFilePath)), getCanonicalFileName!));
}
else if (pathOptions && optionDefinition.type === "list" && optionDefinition.element.isFilePath) {
result.set(name, (value as string[]).map(v => getRelativePathFromFile(pathOptions.configFilePath, getNormalizedAbsolutePath(v, getDirectoryPath(pathOptions.configFilePath)), getCanonicalFileName!)));
}
else {
result.set(name, value);
}
Expand Down Expand Up @@ -2890,17 +2908,23 @@ function parseJsonConfigFileContentWorker(

const parsedConfig = parseConfig(json, sourceFile, host, basePath, configFileName, resolutionStack, errors, extendedConfigCache);
const { raw } = parsedConfig;
const options = extend(existingOptions, parsedConfig.options || {});
const watchOptions = existingWatchOptions && parsedConfig.watchOptions ?
extend(existingWatchOptions, parsedConfig.watchOptions) :
parsedConfig.watchOptions || existingWatchOptions;

const options = handleOptionConfigDirTemplateSubstitution(
extend(existingOptions, parsedConfig.options || {}),
configDirTemplateSubstitutionOptions,
basePath,
) as CompilerOptions;
const watchOptions = handleWatchOptionsConfigDirTemplateSubstitution(
existingWatchOptions && parsedConfig.watchOptions ?
extend(existingWatchOptions, parsedConfig.watchOptions) :
parsedConfig.watchOptions || existingWatchOptions,
basePath,
);
options.configFilePath = configFileName && normalizeSlashes(configFileName);
const basePathForFileNames = normalizePath(configFileName ? directoryOfCombinedPath(configFileName, basePath) : basePath);
const configFileSpecs = getConfigFileSpecs();
if (sourceFile) sourceFile.configFileSpecs = configFileSpecs;
setConfigFileInOptions(options, sourceFile);

const basePathForFileNames = normalizePath(configFileName ? directoryOfCombinedPath(configFileName, basePath) : basePath);
return {
options,
watchOptions,
Expand Down Expand Up @@ -2955,27 +2979,45 @@ function parseJsonConfigFileContentWorker(
includeSpecs = [defaultIncludeSpec];
isDefaultIncludeSpec = true;
}
let validatedIncludeSpecsBeforeSubstitution: readonly string[] | undefined, validatedExcludeSpecsBeforeSubstitution: readonly string[] | undefined;
let validatedIncludeSpecs: readonly string[] | undefined, validatedExcludeSpecs: readonly string[] | undefined;

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

if (includeSpecs) {
validatedIncludeSpecs = validateSpecs(includeSpecs, errors, /*disallowTrailingRecursion*/ true, sourceFile, "include");
validatedIncludeSpecsBeforeSubstitution = validateSpecs(includeSpecs, errors, /*disallowTrailingRecursion*/ true, sourceFile, "include");
validatedIncludeSpecs = getSubstitutedStringArrayWithConfigDirTemplate(
validatedIncludeSpecsBeforeSubstitution,
basePathForFileNames,
) || validatedIncludeSpecsBeforeSubstitution;
}

if (excludeSpecs) {
validatedExcludeSpecs = validateSpecs(excludeSpecs, errors, /*disallowTrailingRecursion*/ false, sourceFile, "exclude");
validatedExcludeSpecsBeforeSubstitution = validateSpecs(excludeSpecs, errors, /*disallowTrailingRecursion*/ false, sourceFile, "exclude");
validatedExcludeSpecs = getSubstitutedStringArrayWithConfigDirTemplate(
validatedExcludeSpecsBeforeSubstitution,
basePathForFileNames,
) || validatedExcludeSpecsBeforeSubstitution;
}

const validatedFilesSpecBeforeSubstitution = filter(filesSpecs, isString);
const validatedFilesSpec = getSubstitutedStringArrayWithConfigDirTemplate(
validatedFilesSpecBeforeSubstitution,
basePathForFileNames,
) || validatedFilesSpecBeforeSubstitution;

return {
filesSpecs,
includeSpecs,
excludeSpecs,
validatedFilesSpec: filter(filesSpecs, isString),
validatedFilesSpec,
validatedIncludeSpecs,
validatedExcludeSpecs,
validatedFilesSpecBeforeSubstitution,
validatedIncludeSpecsBeforeSubstitution,
validatedExcludeSpecsBeforeSubstitution,
pathPatterns: undefined, // Initialized on first use
isDefaultIncludeSpec,
};
Expand Down Expand Up @@ -3043,6 +3085,84 @@ function parseJsonConfigFileContentWorker(
}
}

/** @internal */
export function handleWatchOptionsConfigDirTemplateSubstitution(
watchOptions: WatchOptions | undefined,
basePath: string,
) {
return handleOptionConfigDirTemplateSubstitution(watchOptions, configDirTemplateSubstitutionWatchOptions, basePath) as WatchOptions | undefined;
}

function handleOptionConfigDirTemplateSubstitution(
options: OptionsBase | undefined,
optionDeclarations: readonly CommandLineOption[],
basePath: string,
) {
if (!options) return options;
let result: OptionsBase | undefined;
for (const option of optionDeclarations) {
if (options[option.name] !== undefined) {
const value = options[option.name];
switch (option.type) {
case "string":
Debug.assert(option.isFilePath);
if (startsWithConfigDirTemplate(value)) {
setOptionValue(option, getSubstitutedPathWithConfigDirTemplate(value, basePath));
}
break;
case "list":
Debug.assert(option.element.isFilePath);
const listResult = getSubstitutedStringArrayWithConfigDirTemplate(value as string[], basePath);
if (listResult) setOptionValue(option, listResult);
break;
case "object":
Debug.assert(option.name === "paths");
const objectResult = getSubstitutedMapLikeOfStringArrayWithConfigDirTemplate(value as MapLike<string[]>, basePath);
if (objectResult) setOptionValue(option, objectResult);
break;
default:
Debug.fail("option type not supported");
}
}
}
return result || options;

function setOptionValue(option: CommandLineOption, value: CompilerOptionsValue) {
(result ??= assign({}, options))[option.name] = value;
}
}

const configDirTemplate = `\${configDir}`;
function startsWithConfigDirTemplate(value: any): value is string {
return isString(value) && startsWith(value, configDirTemplate, /*ignoreCase*/ true);
}

function getSubstitutedPathWithConfigDirTemplate(value: string, basePath: string) {
return getNormalizedAbsolutePath(value.replace(configDirTemplate, "./"), basePath);
}

function getSubstitutedStringArrayWithConfigDirTemplate(list: readonly string[] | undefined, basePath: string) {
if (!list) return list;
let result: string[] | undefined;
list.forEach((element, index) => {
if (!startsWithConfigDirTemplate(element)) return;
(result ??= list.slice())[index] = getSubstitutedPathWithConfigDirTemplate(element, basePath);
});
return result;
}

function getSubstitutedMapLikeOfStringArrayWithConfigDirTemplate(mapLike: MapLike<string[]>, basePath: string) {
let result: MapLike<string[]> | undefined;
const ownKeys = getOwnKeys(mapLike);
ownKeys.forEach(key => {
if (!isArray(mapLike[key])) return;
const subStitution = getSubstitutedStringArrayWithConfigDirTemplate(mapLike[key], basePath);
if (!subStitution) return;
(result ??= assign({}, mapLike))[key] = subStitution;
});
return result;
}

function isErrorNoInputFiles(error: Diagnostic) {
return error.code === Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code;
}
Expand Down Expand Up @@ -3144,9 +3264,10 @@ function parseConfig(
else {
ownConfig.extendedConfigPath.forEach(extendedConfigPath => applyExtendedConfig(result, extendedConfigPath));
}
if (!ownConfig.raw.include && result.include) ownConfig.raw.include = result.include;
if (!ownConfig.raw.exclude && result.exclude) ownConfig.raw.exclude = result.exclude;
if (!ownConfig.raw.files && result.files) ownConfig.raw.files = result.files;
if (result.include) ownConfig.raw.include = result.include;
if (result.exclude) ownConfig.raw.exclude = result.exclude;
if (result.files) ownConfig.raw.files = result.files;

if (ownConfig.raw.compileOnSave === undefined && result.compileOnSave) ownConfig.raw.compileOnSave = result.compileOnSave;
if (sourceFile && result.extendedSourceFiles) sourceFile.extendedSourceFiles = arrayFrom(result.extendedSourceFiles.keys());

Expand All @@ -3163,12 +3284,15 @@ function parseConfig(
const extendsRaw = extendedConfig.raw;
let relativeDifference: string | undefined;
const setPropertyInResultIfNotUndefined = (propertyName: "include" | "exclude" | "files") => {
if (ownConfig.raw[propertyName]) return; // No need to calculate if already set in own config
if (extendsRaw[propertyName]) {
result[propertyName] = map(extendsRaw[propertyName], (path: string) =>
isRootedDiskPath(path) ? path : combinePaths(
relativeDifference ||= convertToRelativePath(getDirectoryPath(extendedConfigPath), basePath, createGetCanonicalFileName(host.useCaseSensitiveFileNames)),
path,
));
startsWithConfigDirTemplate(path) || isRootedDiskPath(path) ?
path :
combinePaths(
relativeDifference ||= convertToRelativePath(getDirectoryPath(extendedConfigPath), basePath, createGetCanonicalFileName(host.useCaseSensitiveFileNames)),
path,
));
}
};
setPropertyInResultIfNotUndefined("include");
Expand Down Expand Up @@ -3527,7 +3651,8 @@ export function convertJsonOption(

function normalizeNonListOptionValue(option: CommandLineOption, basePath: string, value: any): CompilerOptionsValue {
if (option.isFilePath) {
value = getNormalizedAbsolutePath(value, basePath);
value = normalizeSlashes(value);
value = !startsWithConfigDirTemplate(value) ? getNormalizedAbsolutePath(value, basePath) : value;
if (value === "") {
value = ".";
}
Expand Down
6 changes: 5 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7471,6 +7471,9 @@ export interface ConfigFileSpecs {
validatedFilesSpec: readonly string[] | undefined;
validatedIncludeSpecs: readonly string[] | undefined;
validatedExcludeSpecs: readonly string[] | undefined;
validatedFilesSpecBeforeSubstitution: readonly string[] | undefined;
validatedIncludeSpecsBeforeSubstitution: readonly string[] | undefined;
validatedExcludeSpecsBeforeSubstitution: readonly string[] | undefined;
pathPatterns: readonly (string | Pattern)[] | undefined;
isDefaultIncludeSpec: boolean;
}
Expand Down Expand Up @@ -7517,7 +7520,8 @@ export interface CommandLineOptionBase {
affectsBuildInfo?: true; // true if this options should be emitted in buildInfo
transpileOptionValue?: boolean | undefined; // If set this means that the option should be set to this value when transpiling
extraValidation?: (value: CompilerOptionsValue) => [DiagnosticMessage, ...string[]] | undefined; // Additional validation to be performed for the value to be valid
disallowNullOrUndefined?: true; // If set option does not allow setting null
disallowNullOrUndefined?: true; // If set option does not allow setting null
allowConfigDirTemplateSubstitution?: true; // If set option allows substitution of `${configDir}` in the value
}

/** @internal */
Expand Down
7 changes: 5 additions & 2 deletions src/compiler/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
FileWatcher,
filter,
find,
findIndex,
flattenDiagnosticMessageText,
forEach,
forEachEntry,
Expand Down Expand Up @@ -418,7 +419,8 @@ export function getMatchedFileSpec(program: Program, fileName: string) {

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

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

/** @internal */
Expand Down
Loading

0 comments on commit cbae6cf

Please sign in to comment.