Skip to content

Commit 233e657

Browse files
committed
fixup config file
1 parent a3df9d7 commit 233e657

File tree

5 files changed

+74
-53
lines changed

5 files changed

+74
-53
lines changed

common/reviews/api/heft-config-file.api.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
2020
static _formatPathForLogging: (path: string) => string;
2121
getObjectSourceFilePath<TObject extends object>(obj: TObject): string | undefined;
2222
getPropertyOriginalValue<TParentProperty extends object, TValue>(options: IOriginalValueOptions<TParentProperty>): TValue | undefined;
23-
// Warning: (ae-forgotten-export) The symbol "IOnFileNotFoundCallback" needs to be exported by the entry point index.d.ts
23+
// Warning: (ae-forgotten-export) The symbol "IOnConfigurationFileNotFoundCallback" needs to be exported by the entry point index.d.ts
2424
//
2525
// (undocumented)
26-
protected _loadConfigurationFileInnerWithCache(terminal: ITerminal, resolvedConfigurationFilePath: string, projectFolderPath: string | undefined, onFileNotFound?: IOnFileNotFoundCallback): TConfigurationFile;
26+
protected _loadConfigurationFileInnerWithCache(terminal: ITerminal, resolvedConfigurationFilePath: string, projectFolderPath: string | undefined, onConfigurationFileNotFound?: IOnConfigurationFileNotFoundCallback): TConfigurationFile;
2727
// (undocumented)
28-
protected _loadConfigurationFileInnerWithCacheAsync(terminal: ITerminal, resolvedConfigurationFilePath: string, projectFolderPath: string | undefined, onFileNotFound?: IOnFileNotFoundCallback): Promise<TConfigurationFile>;
28+
protected _loadConfigurationFileInnerWithCacheAsync(terminal: ITerminal, resolvedConfigurationFilePath: string, projectFolderPath: string | undefined, onFileNotFound?: IOnConfigurationFileNotFoundCallback): Promise<TConfigurationFile>;
2929
}
3030

3131
// @beta

libraries/heft-config-file/src/ConfigurationFileBase.ts

Lines changed: 58 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -289,8 +289,10 @@ export interface IJsonPathsMetadata<TConfigurationFile> {
289289

290290
/**
291291
* 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.
294296
* @beta
295297
*/
296298
export type CustomValidationFunction<TConfigurationFile> = (
@@ -324,7 +326,8 @@ export interface IConfigurationFileOptionsBase<TConfigurationFile> {
324326
* Use this property if you need to validate the configuration file in ways beyond what JSON schema can handle.
325327
* This function will be invoked after JSON schema validation.
326328
*
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.
328331
*/
329332
customValidationFunction?: CustomValidationFunction<TConfigurationFile>;
330333
}
@@ -388,7 +391,12 @@ interface IConfigurationFileCacheEntry<TConfigFile> {
388391
configurationFile: TConfigFile & IConfigurationJson;
389392
}
390393

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;
392400

393401
/**
394402
* @beta
@@ -469,15 +477,15 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
469477
terminal: ITerminal,
470478
resolvedConfigurationFilePath: string,
471479
projectFolderPath: string | undefined,
472-
onFileNotFound?: IOnFileNotFoundCallback
480+
onConfigurationFileNotFound?: IOnConfigurationFileNotFoundCallback
473481
): TConfigurationFile {
474482
const visitedConfigurationFilePaths: Set<string> = new Set<string>();
475483
const cacheEntry: IConfigurationFileCacheEntry<TConfigurationFile> =
476484
this._loadConfigurationFileEntryWithCache(
477485
terminal,
478486
resolvedConfigurationFilePath,
479487
visitedConfigurationFilePaths,
480-
onFileNotFound
488+
onConfigurationFileNotFound
481489
);
482490

483491
const result: TConfigurationFile = this._finalizeConfigurationFile(
@@ -493,7 +501,7 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
493501
terminal: ITerminal,
494502
resolvedConfigurationFilePath: string,
495503
projectFolderPath: string | undefined,
496-
onFileNotFound?: IOnFileNotFoundCallback
504+
onFileNotFound?: IOnConfigurationFileNotFoundCallback
497505
): Promise<TConfigurationFile> {
498506
const visitedConfigurationFilePaths: Set<string> = new Set<string>();
499507
const cacheEntry: IConfigurationFileCacheEntry<TConfigurationFile> =
@@ -517,7 +525,7 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
517525
terminal: ITerminal,
518526
resolvedConfigurationFilePath: string,
519527
visitedConfigurationFilePaths: Set<string>,
520-
onFileNotFound?: IOnFileNotFoundCallback
528+
onFileNotFound?: IOnConfigurationFileNotFoundCallback
521529
): IConfigurationFileCacheEntry<TConfigurationFile> {
522530
if (visitedConfigurationFilePaths.has(resolvedConfigurationFilePath)) {
523531
const resolvedConfigurationFilePathForLogging: string = ConfigurationFileBase._formatPathForLogging(
@@ -550,7 +558,7 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
550558
terminal: ITerminal,
551559
resolvedConfigurationFilePath: string,
552560
visitedConfigurationFilePaths: Set<string>,
553-
onFileNotFound?: IOnFileNotFoundCallback
561+
onConfigurationFileNotFound?: IOnConfigurationFileNotFoundCallback
554562
): Promise<IConfigurationFileCacheEntry<TConfigurationFile>> {
555563
if (visitedConfigurationFilePaths.has(resolvedConfigurationFilePath)) {
556564
const resolvedConfigurationFilePathForLogging: string = ConfigurationFileBase._formatPathForLogging(
@@ -570,7 +578,7 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
570578
terminal,
571579
resolvedConfigurationFilePath,
572580
visitedConfigurationFilePaths,
573-
onFileNotFound
581+
onConfigurationFileNotFound
574582
).then((value: IConfigurationFileCacheEntry<TConfigurationFile>) => {
575583
this._configCache.set(resolvedConfigurationFilePath, value);
576584
return value;
@@ -595,7 +603,7 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
595603
try {
596604
configurationJson = JsonFile.parseString(fileText);
597605
} catch (e) {
598-
throw new Error(`In config file "${resolvedConfigurationFilePathForLogging}": ${e}`);
606+
throw new Error(`In configuration file "${resolvedConfigurationFilePathForLogging}": ${e}`);
599607
}
600608

601609
return configurationJson;
@@ -699,12 +707,15 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
699707
}
700708

701709
if (
702-
this._customValidationFunction?.(
710+
this._customValidationFunction &&
711+
!this._customValidationFunction(
703712
result as TConfigurationFile,
704713
resolvedConfigurationFilePathForLogging,
705714
terminal
706-
) === false
715+
)
707716
) {
717+
// To suppress this error, the function may throw its own error, such as an AlreadyReportedError if it already
718+
// logged to the terminal.
708719
throw new Error(
709720
`Resolved configuration file at "${resolvedConfigurationFilePathForLogging}" failed custom validation.`
710721
);
@@ -721,7 +732,7 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
721732
terminal: ITerminal,
722733
resolvedConfigurationFilePath: string,
723734
visitedConfigurationFilePaths: Set<string>,
724-
fileNotFoundFallback?: IOnFileNotFoundCallback
735+
fileNotFoundFallback?: IOnConfigurationFileNotFoundCallback
725736
): IConfigurationFileCacheEntry<TConfigurationFile> {
726737
const resolvedConfigurationFilePathForLogging: string = ConfigurationFileBase._formatPathForLogging(
727738
resolvedConfigurationFilePath
@@ -736,11 +747,18 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
736747
resolvedConfigurationFilePathForLogging
737748
);
738749
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+
}
744762
}
745763

746764
terminal.writeDebugLine(`Configuration file "${resolvedConfigurationFilePathForLogging}" not found.`);
@@ -794,26 +812,33 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
794812
terminal: ITerminal,
795813
resolvedConfigurationFilePath: string,
796814
visitedConfigurationFilePaths: Set<string>,
797-
fileNotFoundFallback?: IOnFileNotFoundCallback
815+
fileNotFoundFallback?: IOnConfigurationFileNotFoundCallback
798816
): Promise<IConfigurationFileCacheEntry<TConfigurationFile>> {
799817
const resolvedConfigurationFilePathForLogging: string = ConfigurationFileBase._formatPathForLogging(
800818
resolvedConfigurationFilePath
801819
);
802820

803821
let fileText: string;
804822
try {
805-
fileText = FileSystem.readFile(resolvedConfigurationFilePath);
823+
fileText = await FileSystem.readFileAsync(resolvedConfigurationFilePath);
806824
} catch (e) {
807825
if (FileSystem.isNotExistError(e as Error)) {
808826
const fallbackPath: string | undefined = fileNotFoundFallback?.(
809827
resolvedConfigurationFilePathForLogging
810828
);
811829
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+
}
817842
}
818843

819844
terminal.writeDebugLine(`Configuration file "${resolvedConfigurationFilePathForLogging}" not found.`);
@@ -830,7 +855,7 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
830855
let parentConfiguration: IConfigurationFileCacheEntry<TConfigurationFile> | undefined;
831856
if (configurationJson.extends) {
832857
try {
833-
const resolvedParentConfigPath: string = Import.resolveModule({
858+
const resolvedParentConfigPath: string = await Import.resolveModuleAsync({
834859
modulePath: configurationJson.extends,
835860
baseFolderPath: nodeJsPath.dirname(resolvedConfigurationFilePath)
836861
});
@@ -974,11 +999,13 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
974999
// contain inheritance type annotation keys, or other built-in properties that we ignore
9751000
// (eg. "extends", "$schema").
9761001
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)[] = [];
9791002
// A map of property names to their inheritance type.
9801003
const inheritanceTypeMap: Map<keyof TField, IPropertyInheritance<InheritanceType>> = new Map();
9811004

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+
9821009
// Do a first pass to gather and strip the inheritance type annotations from the merging object.
9831010
for (const propertyName of currentObjectPropertyNames) {
9841011
if (ignoreProperties && ignoreProperties.has(propertyName)) {
@@ -1032,18 +1059,12 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
10321059
);
10331060
}
10341061
} else {
1035-
filteredObjectPropertyNames.push(propertyName);
1062+
mergedPropertyNames.add(propertyName);
10361063
}
10371064
}
10381065

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-
10451066
// Cycle through properties and merge them
1046-
for (const propertyName of propertyNames) {
1067+
for (const propertyName of mergedPropertyNames) {
10471068
const propertyValue: TField[keyof TField] | undefined = currentObject[propertyName];
10481069
const parentPropertyValue: TField[keyof TField] | undefined = parentObject[propertyName];
10491070

libraries/heft-config-file/src/ProjectConfigurationFile.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { IRigConfig } from '@rushstack/rig-package';
88

99
import {
1010
ConfigurationFileBase,
11-
type IOnFileNotFoundCallback,
11+
type IOnConfigurationFileNotFoundCallback,
1212
type IConfigurationFileOptions
1313
} from './ConfigurationFileBase';
1414

@@ -129,13 +129,13 @@ export class ProjectConfigurationFile<TConfigurationFile> extends ConfigurationF
129129
private _getRigConfigFallback(
130130
terminal: ITerminal,
131131
rigConfig: IRigConfig | undefined
132-
): IOnFileNotFoundCallback | undefined {
132+
): IOnConfigurationFileNotFoundCallback | undefined {
133133
return rigConfig
134134
? (resolvedConfigurationFilePathForLogging: string) => {
135135
if (rigConfig.rigFound) {
136136
const rigProfileFolder: string = rigConfig.getResolvedProfileFolder();
137137
terminal.writeDebugLine(
138-
`Config file "${resolvedConfigurationFilePathForLogging}" does not exist. Attempting to load via rig ("${ConfigurationFileBase._formatPathForLogging(rigProfileFolder)}").`
138+
`Configuration file "${resolvedConfigurationFilePathForLogging}" does not exist. Attempting to load via rig ("${ConfigurationFileBase._formatPathForLogging(rigProfileFolder)}").`
139139
);
140140
return nodeJsPath.resolve(rigProfileFolder, this.projectRelativeFilePath);
141141
} else {

libraries/heft-config-file/src/test/ConfigurationFile.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1826,7 +1826,7 @@ describe('ConfigurationFile', () => {
18261826
// a newline on Windows, and a curly brace on other platforms, even though the location is
18271827
// accurate in both cases. Use a regex to match either.
18281828
expect(() => configFileLoader.loadConfigurationFileForProject(terminal, __dirname)).toThrowError(
1829-
/In config file "<project root>\/lib\/test\/errorCases\/invalidJson\/config.json": SyntaxError: Unexpected token '(}|\\n)' at 2:19/
1829+
/In configuration file "<project root>\/lib\/test\/errorCases\/invalidJson\/config.json": SyntaxError: Unexpected token '(}|\\n)' at 2:19/
18301830
);
18311831

18321832
jest.restoreAllMocks();
@@ -1861,7 +1861,7 @@ describe('ConfigurationFile', () => {
18611861
await expect(
18621862
configFileLoader.loadConfigurationFileForProjectAsync(terminal, __dirname)
18631863
).rejects.toThrowError(
1864-
/In config file "<project root>\/lib\/test\/errorCases\/invalidJson\/config.json": SyntaxError: Unexpected token '(}|\\n)' at 2:19/
1864+
/In configuration file "<project root>\/lib\/test\/errorCases\/invalidJson\/config.json": SyntaxError: Unexpected token '(}|\\n)' at 2:19/
18651865
);
18661866

18671867
jest.restoreAllMocks();

0 commit comments

Comments
 (0)