@@ -289,8 +289,10 @@ export interface IJsonPathsMetadata<TConfigurationFile> {
289
289
290
290
/**
291
291
* A function to invoke after schema validation to validate the configuration file.
292
- * This function log errors using the terminal and return false if the configuration file
293
- * is invalid.
292
+ * If this function returns any value other than `true`, the configuration file API
293
+ * will throw an error indicating that custom validation failed. If the function wishes
294
+ * to provide its own error message, it may use any combination of the terminal and throwing
295
+ * its own error.
294
296
* @beta
295
297
*/
296
298
export type CustomValidationFunction < TConfigurationFile > = (
@@ -324,7 +326,8 @@ export interface IConfigurationFileOptionsBase<TConfigurationFile> {
324
326
* Use this property if you need to validate the configuration file in ways beyond what JSON schema can handle.
325
327
* This function will be invoked after JSON schema validation.
326
328
*
327
- * The function should throw if the configuration file is invalid.
329
+ * If the file is valid, this function should return `true`, otherwise `ConfigurationFile` will throw an error
330
+ * indicating that custom validation failed. To suppress this error, the function may itself choose to throw.
328
331
*/
329
332
customValidationFunction ?: CustomValidationFunction < TConfigurationFile > ;
330
333
}
@@ -388,7 +391,12 @@ interface IConfigurationFileCacheEntry<TConfigFile> {
388
391
configurationFile : TConfigFile & IConfigurationJson ;
389
392
}
390
393
391
- export type IOnFileNotFoundCallback = ( resolvedConfigurationFilePathForLogging : string ) => string | undefined ;
394
+ /**
395
+ * Callback that returns a fallback configuration file path if the original configuration file was not found.
396
+ */
397
+ export type IOnConfigurationFileNotFoundCallback = (
398
+ resolvedConfigurationFilePathForLogging : string
399
+ ) => string | undefined ;
392
400
393
401
/**
394
402
* @beta
@@ -469,15 +477,15 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
469
477
terminal : ITerminal ,
470
478
resolvedConfigurationFilePath : string ,
471
479
projectFolderPath : string | undefined ,
472
- onFileNotFound ?: IOnFileNotFoundCallback
480
+ onConfigurationFileNotFound ?: IOnConfigurationFileNotFoundCallback
473
481
) : TConfigurationFile {
474
482
const visitedConfigurationFilePaths : Set < string > = new Set < string > ( ) ;
475
483
const cacheEntry : IConfigurationFileCacheEntry < TConfigurationFile > =
476
484
this . _loadConfigurationFileEntryWithCache (
477
485
terminal ,
478
486
resolvedConfigurationFilePath ,
479
487
visitedConfigurationFilePaths ,
480
- onFileNotFound
488
+ onConfigurationFileNotFound
481
489
) ;
482
490
483
491
const result : TConfigurationFile = this . _finalizeConfigurationFile (
@@ -493,7 +501,7 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
493
501
terminal : ITerminal ,
494
502
resolvedConfigurationFilePath : string ,
495
503
projectFolderPath : string | undefined ,
496
- onFileNotFound ?: IOnFileNotFoundCallback
504
+ onFileNotFound ?: IOnConfigurationFileNotFoundCallback
497
505
) : Promise < TConfigurationFile > {
498
506
const visitedConfigurationFilePaths : Set < string > = new Set < string > ( ) ;
499
507
const cacheEntry : IConfigurationFileCacheEntry < TConfigurationFile > =
@@ -517,7 +525,7 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
517
525
terminal : ITerminal ,
518
526
resolvedConfigurationFilePath : string ,
519
527
visitedConfigurationFilePaths : Set < string > ,
520
- onFileNotFound ?: IOnFileNotFoundCallback
528
+ onFileNotFound ?: IOnConfigurationFileNotFoundCallback
521
529
) : IConfigurationFileCacheEntry < TConfigurationFile > {
522
530
if ( visitedConfigurationFilePaths . has ( resolvedConfigurationFilePath ) ) {
523
531
const resolvedConfigurationFilePathForLogging : string = ConfigurationFileBase . _formatPathForLogging (
@@ -550,7 +558,7 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
550
558
terminal : ITerminal ,
551
559
resolvedConfigurationFilePath : string ,
552
560
visitedConfigurationFilePaths : Set < string > ,
553
- onFileNotFound ?: IOnFileNotFoundCallback
561
+ onConfigurationFileNotFound ?: IOnConfigurationFileNotFoundCallback
554
562
) : Promise < IConfigurationFileCacheEntry < TConfigurationFile > > {
555
563
if ( visitedConfigurationFilePaths . has ( resolvedConfigurationFilePath ) ) {
556
564
const resolvedConfigurationFilePathForLogging : string = ConfigurationFileBase . _formatPathForLogging (
@@ -570,7 +578,7 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
570
578
terminal ,
571
579
resolvedConfigurationFilePath ,
572
580
visitedConfigurationFilePaths ,
573
- onFileNotFound
581
+ onConfigurationFileNotFound
574
582
) . then ( ( value : IConfigurationFileCacheEntry < TConfigurationFile > ) => {
575
583
this . _configCache . set ( resolvedConfigurationFilePath , value ) ;
576
584
return value ;
@@ -595,7 +603,7 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
595
603
try {
596
604
configurationJson = JsonFile . parseString ( fileText ) ;
597
605
} catch ( e ) {
598
- throw new Error ( `In config file "${ resolvedConfigurationFilePathForLogging } ": ${ e } ` ) ;
606
+ throw new Error ( `In configuration file "${ resolvedConfigurationFilePathForLogging } ": ${ e } ` ) ;
599
607
}
600
608
601
609
return configurationJson ;
@@ -699,12 +707,15 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
699
707
}
700
708
701
709
if (
702
- this . _customValidationFunction ?.(
710
+ this . _customValidationFunction &&
711
+ ! this . _customValidationFunction (
703
712
result as TConfigurationFile ,
704
713
resolvedConfigurationFilePathForLogging ,
705
714
terminal
706
- ) === false
715
+ )
707
716
) {
717
+ // To suppress this error, the function may throw its own error, such as an AlreadyReportedError if it already
718
+ // logged to the terminal.
708
719
throw new Error (
709
720
`Resolved configuration file at "${ resolvedConfigurationFilePathForLogging } " failed custom validation.`
710
721
) ;
@@ -721,7 +732,7 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
721
732
terminal : ITerminal ,
722
733
resolvedConfigurationFilePath : string ,
723
734
visitedConfigurationFilePaths : Set < string > ,
724
- fileNotFoundFallback ?: IOnFileNotFoundCallback
735
+ fileNotFoundFallback ?: IOnConfigurationFileNotFoundCallback
725
736
) : IConfigurationFileCacheEntry < TConfigurationFile > {
726
737
const resolvedConfigurationFilePathForLogging : string = ConfigurationFileBase . _formatPathForLogging (
727
738
resolvedConfigurationFilePath
@@ -736,11 +747,18 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
736
747
resolvedConfigurationFilePathForLogging
737
748
) ;
738
749
if ( fallbackPath ) {
739
- return this . _loadConfigurationFileEntryWithCache (
740
- terminal ,
741
- fallbackPath ,
742
- visitedConfigurationFilePaths
743
- ) ;
750
+ try {
751
+ return this . _loadConfigurationFileEntryWithCache (
752
+ terminal ,
753
+ fallbackPath ,
754
+ visitedConfigurationFilePaths
755
+ ) ;
756
+ } catch ( fallbackError ) {
757
+ if ( ! FileSystem . isNotExistError ( fallbackError as Error ) ) {
758
+ throw fallbackError ;
759
+ }
760
+ // Otherwise report the missing original file.
761
+ }
744
762
}
745
763
746
764
terminal . writeDebugLine ( `Configuration file "${ resolvedConfigurationFilePathForLogging } " not found.` ) ;
@@ -794,26 +812,33 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
794
812
terminal : ITerminal ,
795
813
resolvedConfigurationFilePath : string ,
796
814
visitedConfigurationFilePaths : Set < string > ,
797
- fileNotFoundFallback ?: IOnFileNotFoundCallback
815
+ fileNotFoundFallback ?: IOnConfigurationFileNotFoundCallback
798
816
) : Promise < IConfigurationFileCacheEntry < TConfigurationFile > > {
799
817
const resolvedConfigurationFilePathForLogging : string = ConfigurationFileBase . _formatPathForLogging (
800
818
resolvedConfigurationFilePath
801
819
) ;
802
820
803
821
let fileText : string ;
804
822
try {
805
- fileText = FileSystem . readFile ( resolvedConfigurationFilePath ) ;
823
+ fileText = await FileSystem . readFileAsync ( resolvedConfigurationFilePath ) ;
806
824
} catch ( e ) {
807
825
if ( FileSystem . isNotExistError ( e as Error ) ) {
808
826
const fallbackPath : string | undefined = fileNotFoundFallback ?.(
809
827
resolvedConfigurationFilePathForLogging
810
828
) ;
811
829
if ( fallbackPath ) {
812
- return this . _loadConfigurationFileEntryWithCacheAsync (
813
- terminal ,
814
- fallbackPath ,
815
- visitedConfigurationFilePaths
816
- ) ;
830
+ try {
831
+ return await this . _loadConfigurationFileEntryWithCacheAsync (
832
+ terminal ,
833
+ fallbackPath ,
834
+ visitedConfigurationFilePaths
835
+ ) ;
836
+ } catch ( fallbackError ) {
837
+ if ( ! FileSystem . isNotExistError ( fallbackError as Error ) ) {
838
+ throw fallbackError ;
839
+ }
840
+ // Otherwise report the missing original file.
841
+ }
817
842
}
818
843
819
844
terminal . writeDebugLine ( `Configuration file "${ resolvedConfigurationFilePathForLogging } " not found.` ) ;
@@ -830,7 +855,7 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
830
855
let parentConfiguration : IConfigurationFileCacheEntry < TConfigurationFile > | undefined ;
831
856
if ( configurationJson . extends ) {
832
857
try {
833
- const resolvedParentConfigPath : string = Import . resolveModule ( {
858
+ const resolvedParentConfigPath : string = await Import . resolveModuleAsync ( {
834
859
modulePath : configurationJson . extends ,
835
860
baseFolderPath : nodeJsPath . dirname ( resolvedConfigurationFilePath )
836
861
} ) ;
@@ -974,11 +999,13 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
974
999
// contain inheritance type annotation keys, or other built-in properties that we ignore
975
1000
// (eg. "extends", "$schema").
976
1001
const currentObjectPropertyNames : Set < string > = new Set ( Object . keys ( currentObject ) ) ;
977
- // An array of property names that should be included in the resulting object.
978
- const filteredObjectPropertyNames : ( keyof TField ) [ ] = [ ] ;
979
1002
// A map of property names to their inheritance type.
980
1003
const inheritanceTypeMap : Map < keyof TField , IPropertyInheritance < InheritanceType > > = new Map ( ) ;
981
1004
1005
+ // The set of property names that should be included in the resulting object
1006
+ // All names from the parent are assumed to already be filtered.
1007
+ const mergedPropertyNames : Set < keyof TField > = new Set ( Object . keys ( parentObject ) ) ;
1008
+
982
1009
// Do a first pass to gather and strip the inheritance type annotations from the merging object.
983
1010
for ( const propertyName of currentObjectPropertyNames ) {
984
1011
if ( ignoreProperties && ignoreProperties . has ( propertyName ) ) {
@@ -1032,18 +1059,12 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
1032
1059
) ;
1033
1060
}
1034
1061
} else {
1035
- filteredObjectPropertyNames . push ( propertyName ) ;
1062
+ mergedPropertyNames . add ( propertyName ) ;
1036
1063
}
1037
1064
}
1038
1065
1039
- // We only filter the currentObject because the parent object should already be filtered
1040
- const propertyNames : Set < keyof TField > = new Set ( [
1041
- ...Object . keys ( parentObject ) ,
1042
- ...filteredObjectPropertyNames
1043
- ] ) ;
1044
-
1045
1066
// Cycle through properties and merge them
1046
- for ( const propertyName of propertyNames ) {
1067
+ for ( const propertyName of mergedPropertyNames ) {
1047
1068
const propertyValue : TField [ keyof TField ] | undefined = currentObject [ propertyName ] ;
1048
1069
const parentPropertyValue : TField [ keyof TField ] | undefined = parentObject [ propertyName ] ;
1049
1070
0 commit comments