@@ -700,12 +700,54 @@ namespace ts {
700
700
* @param basePath A root directory to resolve relative path entries in the config
701
701
* file to. e.g. outDir
702
702
*/
703
- export function parseJsonConfigFileContent ( json : any , host : ParseConfigHost , basePath : string , existingOptions : CompilerOptions = { } , configFileName ?: string ) : ParsedCommandLine {
703
+ export function parseJsonConfigFileContent ( json : any , host : ParseConfigHost , basePath : string , existingOptions : CompilerOptions = { } , configFileName ?: string , resolutionStack : Path [ ] = [ ] ) : ParsedCommandLine {
704
704
const errors : Diagnostic [ ] = [ ] ;
705
- const compilerOptions : CompilerOptions = convertCompilerOptionsFromJsonWorker ( json [ "compilerOptions" ] , basePath , errors , configFileName ) ;
706
- const options = extend ( existingOptions , compilerOptions ) ;
705
+ const getCanonicalFileName = createGetCanonicalFileName ( host . useCaseSensitiveFileNames ) ;
706
+ const resolvedPath = toPath ( configFileName || "" , basePath , getCanonicalFileName ) ;
707
+ if ( resolutionStack . indexOf ( resolvedPath ) >= 0 ) {
708
+ return {
709
+ options : { } ,
710
+ fileNames : [ ] ,
711
+ typingOptions : { } ,
712
+ raw : json ,
713
+ errors : [ createCompilerDiagnostic ( Diagnostics . Circularity_detected_while_resolving_configuration_Colon_0 , [ ...resolutionStack , resolvedPath ] . join ( " -> " ) ) ] ,
714
+ wildcardDirectories : { }
715
+ } ;
716
+ }
717
+
718
+ let options : CompilerOptions = convertCompilerOptionsFromJsonWorker ( json [ "compilerOptions" ] , basePath , errors , configFileName ) ;
707
719
const typingOptions : TypingOptions = convertTypingOptionsFromJsonWorker ( json [ "typingOptions" ] , basePath , errors , configFileName ) ;
708
720
721
+ if ( json [ "extends" ] ) {
722
+ let [ include , exclude , files , baseOptions ] : [ string [ ] , string [ ] , string [ ] , CompilerOptions ] = [ undefined , undefined , undefined , { } ] ;
723
+ if ( typeof json [ "extends" ] === "string" ) {
724
+ [ include , exclude , files , baseOptions ] = ( tryExtendsName ( json [ "extends" ] ) || [ include , exclude , files , baseOptions ] ) ;
725
+ }
726
+ else if ( typeof json [ "extends" ] === "object" && json [ "extends" ] . length ) {
727
+ for ( const name of json [ "extends" ] ) {
728
+ const [ tempinclude , tempexclude , tempfiles , tempBase ] : [ string [ ] , string [ ] , string [ ] , CompilerOptions ] = ( tryExtendsName ( name ) || [ include , exclude , files , baseOptions ] ) ;
729
+ include = tempinclude || include ;
730
+ exclude = tempexclude || exclude ;
731
+ files = tempfiles || files ;
732
+ baseOptions = assign ( { } , baseOptions , tempBase ) ;
733
+ }
734
+ }
735
+ else {
736
+ errors . push ( createCompilerDiagnostic ( Diagnostics . Compiler_option_0_requires_a_value_of_type_1 , "extends" , "string or string[]" ) ) ;
737
+ }
738
+ if ( include && ! json [ "include" ] ) {
739
+ json [ "include" ] = include ;
740
+ }
741
+ if ( exclude && ! json [ "exclude" ] ) {
742
+ json [ "exclude" ] = exclude ;
743
+ }
744
+ if ( files && ! json [ "files" ] ) {
745
+ json [ "files" ] = files ;
746
+ }
747
+ options = assign ( { } , baseOptions , options ) ;
748
+ }
749
+
750
+ options = extend ( existingOptions , options ) ;
709
751
options . configFilePath = configFileName ;
710
752
711
753
const { fileNames, wildcardDirectories } = getFileNames ( errors ) ;
@@ -719,6 +761,39 @@ namespace ts {
719
761
wildcardDirectories
720
762
} ;
721
763
764
+ function tryExtendsName ( extendedConfig : string ) : [ string [ ] , string [ ] , string [ ] , CompilerOptions ] {
765
+ // If the path isn't a rooted or relative path, don't try to resolve it (we reserve the right to special case module-id like paths in the future)
766
+ if ( ! ( isRootedDiskPath ( extendedConfig ) || startsWith ( normalizeSlashes ( extendedConfig ) , "./" ) || startsWith ( normalizeSlashes ( extendedConfig ) , "../" ) ) ) {
767
+ errors . push ( createCompilerDiagnostic ( Diagnostics . The_path_in_an_extends_options_must_be_relative_or_rooted ) ) ;
768
+ return ;
769
+ }
770
+ let extendedConfigPath = toPath ( extendedConfig , basePath , getCanonicalFileName ) ;
771
+ if ( ! host . fileExists ( extendedConfigPath ) && ! endsWith ( extendedConfigPath , ".json" ) ) {
772
+ extendedConfigPath = `${ extendedConfigPath } .json` as Path ;
773
+ if ( ! host . fileExists ( extendedConfigPath ) ) {
774
+ errors . push ( createCompilerDiagnostic ( Diagnostics . File_0_does_not_exist , extendedConfig ) ) ;
775
+ return ;
776
+ }
777
+ }
778
+ const extendedResult = readConfigFile ( extendedConfigPath , path => host . readFile ( path ) ) ;
779
+ if ( extendedResult . error ) {
780
+ errors . push ( extendedResult . error ) ;
781
+ return ;
782
+ }
783
+ const extendedDirname = getDirectoryPath ( extendedConfigPath ) ;
784
+ const relativeDifference = convertToRelativePath ( extendedDirname , basePath , getCanonicalFileName ) ;
785
+ const updatePath : ( path : string ) => string = path => isRootedDiskPath ( path ) ? path : combinePaths ( relativeDifference , path ) ;
786
+ // Merge configs (copy the resolution stack so it is never reused between branches in potential diamond-problem scenarios)
787
+ const result = parseJsonConfigFileContent ( extendedResult . config , host , extendedDirname , /*existingOptions*/ undefined , getBaseFileName ( extendedConfigPath ) , resolutionStack . concat ( [ resolvedPath ] ) ) ;
788
+ errors . push ( ...result . errors ) ;
789
+ const [ include , exclude , files ] = map ( [ "include" , "exclude" , "files" ] , key => {
790
+ if ( ! json [ key ] && extendedResult . config [ key ] ) {
791
+ return map ( extendedResult . config [ key ] , updatePath ) ;
792
+ }
793
+ } ) ;
794
+ return [ include , exclude , files , result . options ] ;
795
+ }
796
+
722
797
function getFileNames ( errors : Diagnostic [ ] ) : ExpandResult {
723
798
let fileNames : string [ ] ;
724
799
if ( hasProperty ( json , "files" ) ) {
0 commit comments