@@ -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 */
16001606export 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 */
16041619export 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,98 @@ 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 ) ) {
3109+ setOptionValue ( option , getSubstitutedPathWithConfigDirTemplate ( value , 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 : any ) : value is string {
3142+ return isString ( value ) && 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+ if ( ! isArray ( mapLike [ key ] ) ) return ;
3168+ const subStitution = getSubstitutedStringArrayWithConfigDirTemplate ( mapLike [ key ] , basePath , createCopyOnSubstitute ) ;
3169+ if ( ! subStitution ) return ;
3170+ if ( createCopyOnSubstitute ) result ??= assign ( { } , mapLike ) ;
3171+ else result ??= mapLike ;
3172+ mapLike [ key ] = subStitution ;
3173+ } ) ;
3174+ return result ;
3175+ }
3176+
30453177function isErrorNoInputFiles ( error : Diagnostic ) {
30463178 return error . code === Diagnostics . No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2 . code ;
30473179}
@@ -3143,9 +3275,10 @@ function parseConfig(
31433275 else {
31443276 ownConfig . extendedConfigPath . forEach ( extendedConfigPath => applyExtendedConfig ( result , extendedConfigPath ) ) ;
31453277 }
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 ;
3278+ if ( result . include ) ownConfig . raw . include = result . include ;
3279+ if ( result . exclude ) ownConfig . raw . exclude = result . exclude ;
3280+ if ( result . files ) ownConfig . raw . files = result . files ;
3281+
31493282 if ( ownConfig . raw . compileOnSave === undefined && result . compileOnSave ) ownConfig . raw . compileOnSave = result . compileOnSave ;
31503283 if ( sourceFile && result . extendedSourceFiles ) sourceFile . extendedSourceFiles = arrayFrom ( result . extendedSourceFiles . keys ( ) ) ;
31513284
@@ -3162,12 +3295,15 @@ function parseConfig(
31623295 const extendsRaw = extendedConfig . raw ;
31633296 let relativeDifference : string | undefined ;
31643297 const setPropertyInResultIfNotUndefined = ( propertyName : "include" | "exclude" | "files" ) => {
3298+ if ( ownConfig . raw [ propertyName ] ) return ; // No need to calculate if already set in own config
31653299 if ( extendsRaw [ propertyName ] ) {
31663300 result [ propertyName ] = map ( extendsRaw [ propertyName ] , ( path : string ) =>
3167- isRootedDiskPath ( path ) ? path : combinePaths (
3168- relativeDifference ||= convertToRelativePath ( getDirectoryPath ( extendedConfigPath ) , basePath , createGetCanonicalFileName ( host . useCaseSensitiveFileNames ) ) ,
3169- path ,
3170- ) ) ;
3301+ startsWithConfigDirTemplate ( path ) || isRootedDiskPath ( path ) ?
3302+ path :
3303+ combinePaths (
3304+ relativeDifference ||= convertToRelativePath ( getDirectoryPath ( extendedConfigPath ) , basePath , createGetCanonicalFileName ( host . useCaseSensitiveFileNames ) ) ,
3305+ path ,
3306+ ) ) ;
31713307 }
31723308 } ;
31733309 setPropertyInResultIfNotUndefined ( "include" ) ;
@@ -3526,7 +3662,8 @@ export function convertJsonOption(
35263662
35273663function normalizeNonListOptionValue ( option : CommandLineOption , basePath : string , value : any ) : CompilerOptionsValue {
35283664 if ( option . isFilePath ) {
3529- value = getNormalizedAbsolutePath ( value , basePath ) ;
3665+ value = normalizeSlashes ( value ) ;
3666+ value = ! startsWithConfigDirTemplate ( value ) ? getNormalizedAbsolutePath ( value , basePath ) : value ;
35303667 if ( value === "" ) {
35313668 value = "." ;
35323669 }
0 commit comments