Skip to content

Commit

Permalink
Allow $configDir as a string to be substituted in config file options
Browse files Browse the repository at this point in the history
  • Loading branch information
sheetalkamat committed Apr 3, 2024
1 parent 7b1a848 commit 9fcd0d6
Show file tree
Hide file tree
Showing 32 changed files with 3,489 additions and 34 deletions.
165 changes: 151 additions & 14 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 @@ -1033,6 +1036,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 @@ -1050,6 +1054,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 @@ -1064,6 +1069,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 @@ -1599,6 +1605,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 @@ -2627,6 +2642,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,16 +2908,17 @@ 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 ?
let watchOptions = existingWatchOptions && parsedConfig.watchOptions ?
extend(existingWatchOptions, parsedConfig.watchOptions) :
parsedConfig.watchOptions || existingWatchOptions;

handleOptionConfigDirTemplateSubstitution(options, configDirTemplateSubstitutionOptions, basePath);
watchOptions = handleWatchOptionsConfigDirTemplateSubstitution(watchOptions, basePath, !existingWatchOptions || !parsedConfig.watchOptions);
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 @@ -2954,27 +2973,48 @@ 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,
/*createCopyOnSubstitute*/ true,
) || validatedIncludeSpecsBeforeSubstitution;
}

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

const validatedFilesSpecBeforeSubstitution = filter(filesSpecs, isString);
const validatedFilesSpec = getSubstitutedStringArrayWithConfigDirTemplate(
validatedFilesSpecBeforeSubstitution,
basePathForFileNames,
/*createCopyOnSubstitute*/ true,
) || 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 @@ -3042,6 +3082,98 @@ function parseJsonConfigFileContentWorker(
}
}

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

function handleOptionConfigDirTemplateSubstitution(
options: OptionsBase | undefined,
optionDeclarations: readonly CommandLineOption[],
basePath: string,
createCopyOnSubstitute?: boolean,
) {
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, createCopyOnSubstitute);
if (listResult) setOptionValue(option, listResult);
break;
case "object":
Debug.assert(option.name === "paths");
const objectResult = getSubstitutedMapLikeOfStringArrayWithConfigDirTemplate(value as MapLike<string[]>, basePath, createCopyOnSubstitute);
if (objectResult) setOptionValue(option, objectResult);
break;
default:
Debug.fail("option type not supported");
}
}
}
return result || options;

function setOptionValue(option: CommandLineOption, value: CompilerOptionsValue) {
if (createCopyOnSubstitute) {
if (!result) result = assign({}, options);
result[option.name] = value;
}
else {
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: string[] | undefined, basePath: string, createCopyOnSubstitute?: boolean): string[] | undefined;
function getSubstitutedStringArrayWithConfigDirTemplate(list: readonly string[] | undefined, basePath: string, createCopyOnSubstitute: true): string[] | undefined;
function getSubstitutedStringArrayWithConfigDirTemplate(list: readonly string[] | string[] | undefined, basePath: string, createCopyOnSubstitute?: boolean) {
if (!list) return list;
let result: string[] | undefined;
list.forEach((element, index) => {
if (!startsWithConfigDirTemplate(element)) return;
if (createCopyOnSubstitute) result ??= list.slice();
else result ??= list as unknown as string[];
result[index] = getSubstitutedPathWithConfigDirTemplate(element, basePath);
});
return result;
}

function getSubstitutedMapLikeOfStringArrayWithConfigDirTemplate(mapLike: MapLike<string[]>, basePath: string, createCopyOnSubstitute?: boolean) {
let result: MapLike<string[]> | undefined;
const ownKeys = getOwnKeys(mapLike);
ownKeys.forEach(key => {
if (!isArray(mapLike[key])) return;
const subStitution = getSubstitutedStringArrayWithConfigDirTemplate(mapLike[key], basePath, createCopyOnSubstitute);
if (!subStitution) return;
if (createCopyOnSubstitute) result ??= assign({}, mapLike);
else result ??= mapLike;
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 @@ -3143,9 +3275,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 @@ -3162,12 +3295,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 @@ -3526,7 +3662,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 @@ -7360,6 +7360,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 @@ -7406,7 +7409,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 @@ -416,7 +417,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 @@ -430,11 +432,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 9fcd0d6

Please sign in to comment.