diff --git a/apps/heft/src/configuration/HeftConfiguration.ts b/apps/heft/src/configuration/HeftConfiguration.ts index cca2e66391..6177278e7c 100644 --- a/apps/heft/src/configuration/HeftConfiguration.ts +++ b/apps/heft/src/configuration/HeftConfiguration.ts @@ -4,6 +4,10 @@ import * as path from 'path'; import { type IPackageJson, PackageJsonLookup, InternalError, Path } from '@rushstack/node-core-library'; import { Terminal, type ITerminalProvider, type ITerminal } from '@rushstack/terminal'; +import { + type IProjectConfigurationFileSpecification, + ProjectConfigurationFile +} from '@rushstack/heft-config-file'; import { type IRigConfig, RigConfig } from '@rushstack/rig-package'; import { Constants } from '../utilities/Constants'; @@ -33,6 +37,11 @@ interface IHeftConfigurationOptions extends IHeftConfigurationInitializationOpti buildFolderPath: string; } +interface IProjectConfigurationFileEntry { + options: IProjectConfigurationFileSpecification; + loader: ProjectConfigurationFile; +} + /** * @public */ @@ -43,6 +52,8 @@ export class HeftConfiguration { private _rigConfig: IRigConfig | undefined; private _rigPackageResolver: RigPackageResolver | undefined; + private readonly _knownConfigurationFiles: Map> = new Map(); + /** * Project build folder path. This is the folder containing the project's package.json file. */ @@ -161,6 +172,34 @@ export class HeftConfiguration { } } + /** + * Attempts to load a riggable project configuration file using blocking, synchronous I/O. + * @param options - The options for the configuration file loader from `@rushstack/heft-config-file`. If invoking this function multiple times for the same file, reuse the same object. + * @param terminal - The terminal to log messages during configuration file loading. + * @returns The configuration file, or undefined if it could not be loaded. + */ + public tryLoadProjectConfigurationFile( + options: IProjectConfigurationFileSpecification, + terminal: ITerminal + ): TConfigFile | undefined { + const loader: ProjectConfigurationFile = this._getConfigFileLoader(options); + return loader.tryLoadConfigurationFileForProject(terminal, this.buildFolderPath, this._rigConfig); + } + + /** + * Attempts to load a riggable project configuration file using asynchronous I/O. + * @param options - The options for the configuration file loader from `@rushstack/heft-config-file`. If invoking this function multiple times for the same file, reuse the same object. + * @param terminal - The terminal to log messages during configuration file loading. + * @returns A promise that resolves to the configuration file, or undefined if it could not be loaded. + */ + public async tryLoadProjectConfigurationFileAsync( + options: IProjectConfigurationFileSpecification, + terminal: ITerminal + ): Promise { + const loader: ProjectConfigurationFile = this._getConfigFileLoader(options); + return loader.tryLoadConfigurationFileForProjectAsync(terminal, this.buildFolderPath, this._rigConfig); + } + /** * @internal */ @@ -187,4 +226,25 @@ export class HeftConfiguration { }); return configuration; } + + private _getConfigFileLoader( + options: IProjectConfigurationFileSpecification + ): ProjectConfigurationFile { + let entry: IProjectConfigurationFileEntry | undefined = this._knownConfigurationFiles.get( + options.projectRelativeFilePath + ) as IProjectConfigurationFileEntry | undefined; + + if (!entry) { + entry = { + options: Object.freeze(options), + loader: new ProjectConfigurationFile(options) + }; + } else if (options !== entry.options) { + throw new Error( + `The project configuration file for ${options.projectRelativeFilePath} has already been loaded with different options. Please ensure that options object used to load the configuration file is the same referenced object in all calls.` + ); + } + + return entry.loader; + } } diff --git a/apps/heft/src/configuration/types.ts b/apps/heft/src/configuration/types.ts new file mode 100644 index 0000000000..46d9e68f41 --- /dev/null +++ b/apps/heft/src/configuration/types.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +export type { + CustomValidationFunction, + ICustomJsonPathMetadata, + ICustomPropertyInheritance, + IJsonPathMetadata, + IJsonPathMetadataResolverOptions, + IJsonPathsMetadata, + INonCustomJsonPathMetadata, + IOriginalValueOptions, + IProjectConfigurationFileSpecification, + IPropertiesInheritance, + IPropertyInheritance, + IPropertyInheritanceDefaults, + InheritanceType, + PathResolutionMethod, + PropertyInheritanceCustomFunction +} from '@rushstack/heft-config-file'; diff --git a/apps/heft/src/index.ts b/apps/heft/src/index.ts index 957f721711..68d77c23d1 100644 --- a/apps/heft/src/index.ts +++ b/apps/heft/src/index.ts @@ -11,6 +11,9 @@ * @packageDocumentation */ +import type * as ConfigurationFile from './configuration/types'; +export type { ConfigurationFile }; + export { HeftConfiguration, type IHeftConfigurationInitializationOptions as _IHeftConfigurationInitializationOptions diff --git a/common/changes/@rushstack/heft-api-extractor-plugin/heft-config-api_2025-03-04-23-15.json b/common/changes/@rushstack/heft-api-extractor-plugin/heft-config-api_2025-03-04-23-15.json new file mode 100644 index 0000000000..586c20d93a --- /dev/null +++ b/common/changes/@rushstack/heft-api-extractor-plugin/heft-config-api_2025-03-04-23-15.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-api-extractor-plugin", + "comment": "Use `tryLoadProjectConfigurationFileAsync` Heft API to remove direct dependency on `@rushstack/heft-config-file`.", + "type": "minor" + } + ], + "packageName": "@rushstack/heft-api-extractor-plugin" +} \ No newline at end of file diff --git a/common/changes/@rushstack/heft-config-file/heft-config-api_2025-03-04-23-13.json b/common/changes/@rushstack/heft-config-file/heft-config-api_2025-03-04-23-13.json new file mode 100644 index 0000000000..8ad3c3e137 --- /dev/null +++ b/common/changes/@rushstack/heft-config-file/heft-config-api_2025-03-04-23-13.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-config-file", + "comment": "Fix an issue with `PathResolutionMethod.resolvePathRelativeToProjectRoot` when extending files across packages.", + "type": "minor" + } + ], + "packageName": "@rushstack/heft-config-file" +} \ No newline at end of file diff --git a/common/changes/@rushstack/heft-config-file/multi-emit-plugin_2025-03-04-01-02.json b/common/changes/@rushstack/heft-config-file/multi-emit-plugin_2025-03-04-01-02.json new file mode 100644 index 0000000000..3817d990a2 --- /dev/null +++ b/common/changes/@rushstack/heft-config-file/multi-emit-plugin_2025-03-04-01-02.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-config-file", + "comment": "Add a new `customValidationFunction` option for custom validation logic on loaded configuration files.", + "type": "minor" + } + ], + "packageName": "@rushstack/heft-config-file" +} \ No newline at end of file diff --git a/common/changes/@rushstack/heft-sass-plugin/heft-config-api_2025-03-05-00-29.json b/common/changes/@rushstack/heft-sass-plugin/heft-config-api_2025-03-05-00-29.json new file mode 100644 index 0000000000..82e868ab42 --- /dev/null +++ b/common/changes/@rushstack/heft-sass-plugin/heft-config-api_2025-03-05-00-29.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-sass-plugin", + "comment": "Use `tryLoadProjectConfigurationFileAsync` API to remove direct dependency on `@rushstack/heft-config-file`.", + "type": "minor" + } + ], + "packageName": "@rushstack/heft-sass-plugin" +} \ No newline at end of file diff --git a/common/changes/@rushstack/heft/multi-emit-plugin_2025-03-01-03-11.json b/common/changes/@rushstack/heft/multi-emit-plugin_2025-03-01-03-11.json new file mode 100644 index 0000000000..37c80695ff --- /dev/null +++ b/common/changes/@rushstack/heft/multi-emit-plugin_2025-03-01-03-11.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft", + "comment": "Add a method `tryLoadProjectConfigurationFileAsync(options, terminal)` to `HeftConfiguration`.", + "type": "minor" + } + ], + "packageName": "@rushstack/heft" +} \ No newline at end of file diff --git a/common/changes/@rushstack/rig-package/multi-emit-plugin_2025-03-01-03-11.json b/common/changes/@rushstack/rig-package/multi-emit-plugin_2025-03-01-03-11.json new file mode 100644 index 0000000000..7a237cfcc0 --- /dev/null +++ b/common/changes/@rushstack/rig-package/multi-emit-plugin_2025-03-01-03-11.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-typescript-plugin", + "comment": "Leverage Heft's new `tryLoadProjectConfigurationFileAsync` method.", + "type": "minor" + } + ], + "packageName": "@rushstack/heft-typescript-plugin" +} \ No newline at end of file diff --git a/common/config/subspaces/build-tests-subspace/repo-state.json b/common/config/subspaces/build-tests-subspace/repo-state.json index 6a7b311b31..74f84055b6 100644 --- a/common/config/subspaces/build-tests-subspace/repo-state.json +++ b/common/config/subspaces/build-tests-subspace/repo-state.json @@ -2,5 +2,5 @@ { "pnpmShrinkwrapHash": "d9e805ea30f80e290b5d5ea83856c8b5d7302941", "preferredVersionsHash": "54149ea3f01558a859c96dee2052b797d4defe68", - "packageJsonInjectedDependenciesHash": "8982ce433cee12f925dfe4d72638fad18cbf641c" + "packageJsonInjectedDependenciesHash": "949bb6038c34cb0580b82a6f728b26a66fff3177" } diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index 38c873dcb9..91ecee53d3 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -2496,9 +2496,6 @@ importers: ../../../heft-plugins/heft-api-extractor-plugin: dependencies: - '@rushstack/heft-config-file': - specifier: workspace:* - version: link:../../libraries/heft-config-file '@rushstack/node-core-library': specifier: workspace:* version: link:../../libraries/node-core-library @@ -2685,9 +2682,6 @@ importers: ../../../heft-plugins/heft-sass-plugin: dependencies: - '@rushstack/heft-config-file': - specifier: workspace:* - version: link:../../libraries/heft-config-file '@rushstack/node-core-library': specifier: workspace:* version: link:../../libraries/node-core-library diff --git a/common/reviews/api/heft-config-file.api.md b/common/reviews/api/heft-config-file.api.md index d508fb1419..bfad3503db 100644 --- a/common/reviews/api/heft-config-file.api.md +++ b/common/reviews/api/heft-config-file.api.md @@ -21,20 +21,20 @@ export abstract class ConfigurationFileBase(obj: TObject): string | undefined; getPropertyOriginalValue(options: IOriginalValueOptions): TValue | undefined; // (undocumented) - protected _loadConfigurationFileInnerWithCache(terminal: ITerminal, resolvedConfigurationFilePath: string, visitedConfigurationFilePaths: Set, rigConfig: IRigConfig | undefined): TConfigurationFile; + protected _loadConfigurationFileInnerWithCache(terminal: ITerminal, resolvedConfigurationFilePath: string, projectFolderPath: string | undefined, onConfigurationFileNotFound?: IOnConfigurationFileNotFoundCallback): TConfigurationFile; // (undocumented) - protected _loadConfigurationFileInnerWithCacheAsync(terminal: ITerminal, resolvedConfigurationFilePath: string, visitedConfigurationFilePaths: Set, rigConfig: IRigConfig | undefined): Promise; - // (undocumented) - protected abstract _tryLoadConfigurationFileInRig(terminal: ITerminal, rigConfig: IRigConfig, visitedConfigurationFilePaths: Set): TConfigurationFile | undefined; - // (undocumented) - protected abstract _tryLoadConfigurationFileInRigAsync(terminal: ITerminal, rigConfig: IRigConfig, visitedConfigurationFilePaths: Set): Promise; + protected _loadConfigurationFileInnerWithCacheAsync(terminal: ITerminal, resolvedConfigurationFilePath: string, projectFolderPath: string | undefined, onFileNotFound?: IOnConfigurationFileNotFoundCallback): Promise; } +// @beta +export type CustomValidationFunction = (configurationFile: TConfigurationFile, resolvedConfigurationFilePathForLogging: string, terminal: ITerminal) => boolean; + // @beta (undocumented) export type IConfigurationFileOptions = IConfigurationFileOptionsWithJsonSchemaFilePath | IConfigurationFileOptionsWithJsonSchemaObject; // @beta (undocumented) export interface IConfigurationFileOptionsBase { + customValidationFunction?: CustomValidationFunction; jsonPathMetadata?: IJsonPathsMetadata; propertyInheritance?: IPropertiesInheritance; propertyInheritanceDefaults?: IPropertyInheritanceDefaults; @@ -70,6 +70,7 @@ export type IJsonPathMetadata = ICustomJsonPathMetadata | INonCustomJsonPa export interface IJsonPathMetadataResolverOptions { configurationFile: Partial; configurationFilePath: string; + projectFolderPath?: string; propertyName: string; propertyValue: string; } @@ -80,12 +81,23 @@ export interface IJsonPathsMetadata { [jsonPath: string]: IJsonPathMetadata; } +// @beta +export const InheritanceType: { + readonly append: "append"; + readonly merge: "merge"; + readonly replace: "replace"; + readonly custom: "custom"; +}; + +// @beta (undocumented) +export type InheritanceType = (typeof InheritanceType)[keyof typeof InheritanceType]; + // @beta (undocumented) -export enum InheritanceType { - append = "append", - custom = "custom", - merge = "merge", - replace = "replace" +export namespace InheritanceType { + export type append = typeof InheritanceType.append; + export type custom = typeof InheritanceType.custom; + export type merge = typeof InheritanceType.merge; + export type replace = typeof InheritanceType.replace; } // @beta @@ -93,6 +105,9 @@ export interface INonCustomJsonPathMetadata { pathResolutionMethod?: PathResolutionMethod.NodeResolve | PathResolutionMethod.nodeResolve | PathResolutionMethod.resolvePathRelativeToConfigurationFile | PathResolutionMethod.resolvePathRelativeToProjectRoot; } +// @beta +export type IOnConfigurationFileNotFoundCallback = (resolvedConfigurationFilePathForLogging: string) => string | undefined; + // @beta (undocumented) export interface IOriginalValueOptions { // (undocumented) @@ -106,6 +121,9 @@ export interface IProjectConfigurationFileOptions { projectRelativeFilePath: string; } +// @beta +export type IProjectConfigurationFileSpecification = IConfigurationFileOptions; + // @beta (undocumented) export type IPropertiesInheritance = { [propertyName in keyof TConfigurationFile]?: IPropertyInheritance | ICustomPropertyInheritance; @@ -131,34 +149,38 @@ export class NonProjectConfigurationFile extends Configurati loadConfigurationFileAsync(terminal: ITerminal, filePath: string): Promise; tryLoadConfigurationFile(terminal: ITerminal, filePath: string): TConfigurationFile | undefined; tryLoadConfigurationFileAsync(terminal: ITerminal, filePath: string): Promise; - // (undocumented) - protected _tryLoadConfigurationFileInRig(terminal: ITerminal, rigConfig: IRigConfig, visitedConfigurationFilePaths: Set): TConfigurationFile | undefined; - // (undocumented) - protected _tryLoadConfigurationFileInRigAsync(terminal: ITerminal, rigConfig: IRigConfig, visitedConfigurationFilePaths: Set): Promise; } +// @beta +export const PathResolutionMethod: { + readonly resolvePathRelativeToConfigurationFile: "resolvePathRelativeToConfigurationFile"; + readonly resolvePathRelativeToProjectRoot: "resolvePathRelativeToProjectRoot"; + readonly NodeResolve: "NodeResolve"; + readonly nodeResolve: "nodeResolve"; + readonly custom: "custom"; +}; + +// @beta (undocumented) +export type PathResolutionMethod = (typeof PathResolutionMethod)[keyof typeof PathResolutionMethod]; + // @beta (undocumented) -export enum PathResolutionMethod { - custom = "custom", +export namespace PathResolutionMethod { + export type custom = typeof PathResolutionMethod.custom; // @deprecated - NodeResolve = "NodeResolve", - nodeResolve = "nodeResolve", - resolvePathRelativeToConfigurationFile = "resolvePathRelativeToConfigurationFile", - resolvePathRelativeToProjectRoot = "resolvePathRelativeToProjectRoot" + export type NodeResolve = typeof PathResolutionMethod.NodeResolve; + export type nodeResolve = typeof PathResolutionMethod.nodeResolve; + export type resolvePathRelativeToConfigurationFile = typeof PathResolutionMethod.resolvePathRelativeToConfigurationFile; + export type resolvePathRelativeToProjectRoot = typeof PathResolutionMethod.resolvePathRelativeToProjectRoot; } // @beta (undocumented) export class ProjectConfigurationFile extends ConfigurationFileBase { - constructor(options: IConfigurationFileOptions); + constructor(options: IProjectConfigurationFileSpecification); loadConfigurationFileForProject(terminal: ITerminal, projectPath: string, rigConfig?: IRigConfig): TConfigurationFile; loadConfigurationFileForProjectAsync(terminal: ITerminal, projectPath: string, rigConfig?: IRigConfig): Promise; readonly projectRelativeFilePath: string; tryLoadConfigurationFileForProject(terminal: ITerminal, projectPath: string, rigConfig?: IRigConfig): TConfigurationFile | undefined; tryLoadConfigurationFileForProjectAsync(terminal: ITerminal, projectPath: string, rigConfig?: IRigConfig): Promise; - // (undocumented) - protected _tryLoadConfigurationFileInRig(terminal: ITerminal, rigConfig: IRigConfig, visitedConfigurationFilePaths: Set): TConfigurationFile | undefined; - // (undocumented) - protected _tryLoadConfigurationFileInRigAsync(terminal: ITerminal, rigConfig: IRigConfig, visitedConfigurationFilePaths: Set): Promise; } // @beta (undocumented) diff --git a/common/reviews/api/heft.api.md b/common/reviews/api/heft.api.md index 3ce9a603be..714a0a17ad 100644 --- a/common/reviews/api/heft.api.md +++ b/common/reviews/api/heft.api.md @@ -16,11 +16,26 @@ import { CommandLineIntegerParameter } from '@rushstack/ts-command-line'; import { CommandLineParameter } from '@rushstack/ts-command-line'; import { CommandLineStringListParameter } from '@rushstack/ts-command-line'; import { CommandLineStringParameter } from '@rushstack/ts-command-line'; +import { CustomValidationFunction } from '@rushstack/heft-config-file'; import * as fs from 'fs'; +import { ICustomJsonPathMetadata } from '@rushstack/heft-config-file'; +import { ICustomPropertyInheritance } from '@rushstack/heft-config-file'; +import { IJsonPathMetadata } from '@rushstack/heft-config-file'; +import { IJsonPathMetadataResolverOptions } from '@rushstack/heft-config-file'; +import { IJsonPathsMetadata } from '@rushstack/heft-config-file'; +import { InheritanceType } from '@rushstack/heft-config-file'; +import { INonCustomJsonPathMetadata } from '@rushstack/heft-config-file'; +import { IOriginalValueOptions } from '@rushstack/heft-config-file'; import { IPackageJson } from '@rushstack/node-core-library'; +import { IProjectConfigurationFileSpecification } from '@rushstack/heft-config-file'; +import { IPropertiesInheritance } from '@rushstack/heft-config-file'; +import { IPropertyInheritance } from '@rushstack/heft-config-file'; +import { IPropertyInheritanceDefaults } from '@rushstack/heft-config-file'; import { IRigConfig } from '@rushstack/rig-package'; import { ITerminal } from '@rushstack/terminal'; import { ITerminalProvider } from '@rushstack/terminal'; +import { PathResolutionMethod } from '@rushstack/heft-config-file'; +import { PropertyInheritanceCustomFunction } from '@rushstack/heft-config-file'; export { CommandLineChoiceListParameter } @@ -38,6 +53,27 @@ export { CommandLineStringListParameter } export { CommandLineStringParameter } +declare namespace ConfigurationFile { + export { + CustomValidationFunction, + ICustomJsonPathMetadata, + ICustomPropertyInheritance, + IJsonPathMetadata, + IJsonPathMetadataResolverOptions, + IJsonPathsMetadata, + INonCustomJsonPathMetadata, + IOriginalValueOptions, + IProjectConfigurationFileSpecification, + IPropertiesInheritance, + IPropertyInheritance, + IPropertyInheritanceDefaults, + InheritanceType, + PathResolutionMethod, + PropertyInheritanceCustomFunction + } +} +export { ConfigurationFile } + // @public export type GlobFn = (pattern: string | string[], options?: IGlobOptions | undefined) => Promise; @@ -58,6 +94,8 @@ export class HeftConfiguration { get slashNormalizedBuildFolderPath(): string; get tempFolderPath(): string; readonly terminalProvider: ITerminalProvider; + tryLoadProjectConfigurationFile(options: IProjectConfigurationFileSpecification, terminal: ITerminal): TConfigFile | undefined; + tryLoadProjectConfigurationFileAsync(options: IProjectConfigurationFileSpecification, terminal: ITerminal): Promise; } // @public diff --git a/heft-plugins/heft-api-extractor-plugin/package.json b/heft-plugins/heft-api-extractor-plugin/package.json index 345b9bc6f3..10cc2d002b 100644 --- a/heft-plugins/heft-api-extractor-plugin/package.json +++ b/heft-plugins/heft-api-extractor-plugin/package.json @@ -18,7 +18,6 @@ "@rushstack/heft": "0.71.2" }, "dependencies": { - "@rushstack/heft-config-file": "workspace:*", "@rushstack/node-core-library": "workspace:*", "semver": "~7.5.4" }, diff --git a/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts b/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts index 58bcb4e731..a88b78ba76 100644 --- a/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts +++ b/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts @@ -7,9 +7,9 @@ import type { IHeftTaskRunHookOptions, IHeftTaskSession, HeftConfiguration, - IHeftTaskRunIncrementalHookOptions + IHeftTaskRunIncrementalHookOptions, + ConfigurationFile } from '@rushstack/heft'; -import { ProjectConfigurationFile } from '@rushstack/heft-config-file'; import { ApiExtractorRunner } from './ApiExtractorRunner'; import apiExtractorConfigSchema from './schemas/api-extractor-task.schema.json'; @@ -23,6 +23,12 @@ const EXTRACTOR_CONFIG_FILENAME: typeof TApiExtractor.ExtractorConfig.FILENAME = const LEGACY_EXTRACTOR_CONFIG_RELATIVE_PATH: string = `./${EXTRACTOR_CONFIG_FILENAME}`; const EXTRACTOR_CONFIG_RELATIVE_PATH: string = `./config/${EXTRACTOR_CONFIG_FILENAME}`; +const API_EXTRACTOR_CONFIG_SPECIFICATION: ConfigurationFile.IProjectConfigurationFileSpecification = + { + projectRelativeFilePath: TASK_CONFIG_RELATIVE_PATH, + jsonSchemaObject: apiExtractorConfigSchema + }; + export interface IApiExtractorConfigurationResult { apiExtractorPackage: typeof TApiExtractor; apiExtractorConfiguration: TApiExtractor.ExtractorConfig; @@ -50,9 +56,6 @@ export interface IApiExtractorTaskConfiguration { export default class ApiExtractorPlugin implements IHeftTaskPlugin { private _apiExtractor: typeof TApiExtractor | undefined; private _apiExtractorConfigurationFilePath: string | undefined | typeof UNINITIALIZED = UNINITIALIZED; - private _apiExtractorTaskConfigurationFileLoader: - | ProjectConfigurationFile - | undefined; private _printedWatchWarning: boolean = false; public apply(taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration): void { @@ -151,25 +154,6 @@ export default class ApiExtractorPlugin implements IHeftTaskPlugin { return this._apiExtractor; } - private async _getApiExtractorTaskConfigurationAsync( - taskSession: IHeftTaskSession, - heftConfiguration: HeftConfiguration - ): Promise { - if (!this._apiExtractorTaskConfigurationFileLoader) { - this._apiExtractorTaskConfigurationFileLoader = - new ProjectConfigurationFile({ - projectRelativeFilePath: TASK_CONFIG_RELATIVE_PATH, - jsonSchemaObject: apiExtractorConfigSchema - }); - } - - return await this._apiExtractorTaskConfigurationFileLoader.tryLoadConfigurationFileForProjectAsync( - taskSession.logger.terminal, - heftConfiguration.buildFolderPath, - heftConfiguration.rigConfig - ); - } - private async _runApiExtractorAsync( taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration, @@ -178,7 +162,10 @@ export default class ApiExtractorPlugin implements IHeftTaskPlugin { apiExtractorConfiguration: TApiExtractor.ExtractorConfig ): Promise { const apiExtractorTaskConfiguration: IApiExtractorTaskConfiguration | undefined = - await this._getApiExtractorTaskConfigurationAsync(taskSession, heftConfiguration); + await heftConfiguration.tryLoadProjectConfigurationFileAsync( + API_EXTRACTOR_CONFIG_SPECIFICATION, + taskSession.logger.terminal + ); if (runOptions.requestRun) { if (!apiExtractorTaskConfiguration?.runInWatchMode) { diff --git a/heft-plugins/heft-sass-plugin/package.json b/heft-plugins/heft-sass-plugin/package.json index c04811c81a..aed34a443f 100644 --- a/heft-plugins/heft-sass-plugin/package.json +++ b/heft-plugins/heft-sass-plugin/package.json @@ -19,7 +19,6 @@ "@rushstack/heft": "^0.71.2" }, "dependencies": { - "@rushstack/heft-config-file": "workspace:*", "@rushstack/node-core-library": "workspace:*", "@rushstack/typings-generator": "workspace:*", "sass-embedded": "~1.77.8", diff --git a/heft-plugins/heft-sass-plugin/src/SassPlugin.ts b/heft-plugins/heft-sass-plugin/src/SassPlugin.ts index 82018975f1..93333907a9 100644 --- a/heft-plugins/heft-sass-plugin/src/SassPlugin.ts +++ b/heft-plugins/heft-sass-plugin/src/SassPlugin.ts @@ -8,9 +8,9 @@ import type { IScopedLogger, IHeftTaskRunHookOptions, IHeftTaskRunIncrementalHookOptions, - IWatchedFileState + IWatchedFileState, + ConfigurationFile } from '@rushstack/heft'; -import { ProjectConfigurationFile } from '@rushstack/heft-config-file'; import { type ISassConfiguration, SassProcessor } from './SassProcessor'; import sassConfigSchema from './schemas/heft-sass-plugin.schema.json'; @@ -20,8 +20,13 @@ export interface ISassConfigurationJson extends Partial {} const PLUGIN_NAME: 'sass-plugin' = 'sass-plugin'; const SASS_CONFIGURATION_LOCATION: string = 'config/sass.json'; +const SASS_CONFIGURATION_FILE_SPECIFICATION: ConfigurationFile.IProjectConfigurationFileSpecification = + { + projectRelativeFilePath: SASS_CONFIGURATION_LOCATION, + jsonSchemaObject: sassConfigSchema + }; + export default class SassPlugin implements IHeftPlugin { - private static _sassConfigurationLoader: ProjectConfigurationFile | undefined; private _sassConfiguration: ISassConfiguration | undefined; private _sassProcessor: SassProcessor | undefined; @@ -100,23 +105,17 @@ export default class SassPlugin implements IHeftPlugin { } private async _loadSassConfigurationAsync( - { rigConfig, slashNormalizedBuildFolderPath }: HeftConfiguration, + heftConfiguration: HeftConfiguration, logger: IScopedLogger ): Promise { if (!this._sassConfiguration) { - if (!SassPlugin._sassConfigurationLoader) { - SassPlugin._sassConfigurationLoader = new ProjectConfigurationFile({ - projectRelativeFilePath: SASS_CONFIGURATION_LOCATION, - jsonSchemaObject: sassConfigSchema - }); - } - const sassConfigurationJson: ISassConfigurationJson | undefined = - await SassPlugin._sassConfigurationLoader.tryLoadConfigurationFileForProjectAsync( - logger.terminal, - slashNormalizedBuildFolderPath, - rigConfig + await heftConfiguration.tryLoadProjectConfigurationFileAsync( + SASS_CONFIGURATION_FILE_SPECIFICATION, + logger.terminal ); + + const { slashNormalizedBuildFolderPath } = heftConfiguration; if (sassConfigurationJson) { if (sassConfigurationJson.srcFolder) { sassConfigurationJson.srcFolder = `${slashNormalizedBuildFolderPath}/${sassConfigurationJson.srcFolder}`; diff --git a/heft-plugins/heft-typescript-plugin/src/TypeScriptPlugin.ts b/heft-plugins/heft-typescript-plugin/src/TypeScriptPlugin.ts index f3a4fcebc5..3d8f3edc5c 100644 --- a/heft-plugins/heft-typescript-plugin/src/TypeScriptPlugin.ts +++ b/heft-plugins/heft-typescript-plugin/src/TypeScriptPlugin.ts @@ -15,7 +15,8 @@ import type { IHeftTaskRunHookOptions, IHeftTaskRunIncrementalHookOptions, ICopyOperation, - IHeftTaskFileOperations + IHeftTaskFileOperations, + ConfigurationFile } from '@rushstack/heft'; import { TypeScriptBuilder, type ITypeScriptBuilderConfiguration } from './TypeScriptBuilder'; @@ -128,11 +129,22 @@ export interface ITypeScriptPluginAccessor { readonly onChangedFilesHook: SyncHook; } -let _typeScriptConfigurationFileLoader: ProjectConfigurationFile | undefined; -const _typeScriptConfigurationFilePromiseCache: Map< - string, - Promise -> = new Map(); +const TYPESCRIPT_LOADER_CONFIG: ConfigurationFile.IProjectConfigurationFileSpecification = + { + projectRelativeFilePath: 'config/typescript.json', + jsonSchemaObject: typescriptConfigSchema, + propertyInheritance: { + staticAssetsToCopy: { + // When merging objects, arrays will be automatically appended + inheritanceType: InheritanceType.merge + } + }, + jsonPathMetadata: { + '$.additionalModuleKindsToEmit.*.outFolderName': { + pathResolutionMethod: PathResolutionMethod.resolvePathRelativeToProjectRoot + } + } + }; /** * @beta @@ -141,37 +153,10 @@ export async function loadTypeScriptConfigurationFileAsync( heftConfiguration: HeftConfiguration, terminal: ITerminal ): Promise { - const buildFolderPath: string = heftConfiguration.buildFolderPath; - - // Check the cache first - let typescriptConfigurationFilePromise: Promise | undefined = - _typeScriptConfigurationFilePromiseCache.get(buildFolderPath); - - if (!typescriptConfigurationFilePromise) { - // Ensure that the file loader has been initialized. - if (!_typeScriptConfigurationFileLoader) { - _typeScriptConfigurationFileLoader = new ProjectConfigurationFile({ - projectRelativeFilePath: 'config/typescript.json', - jsonSchemaObject: typescriptConfigSchema, - propertyInheritance: { - staticAssetsToCopy: { - // When merging objects, arrays will be automatically appended - inheritanceType: InheritanceType.merge - } - } - }); - } - - typescriptConfigurationFilePromise = - _typeScriptConfigurationFileLoader.tryLoadConfigurationFileForProjectAsync( - terminal, - buildFolderPath, - heftConfiguration.rigConfig - ); - _typeScriptConfigurationFilePromiseCache.set(buildFolderPath, typescriptConfigurationFilePromise); - } - - return await typescriptConfigurationFilePromise; + return await heftConfiguration.tryLoadProjectConfigurationFileAsync( + TYPESCRIPT_LOADER_CONFIG, + terminal + ); } let _partialTsconfigFileLoader: ProjectConfigurationFile | undefined; @@ -235,6 +220,11 @@ export async function loadPartialTsconfigFileAsync( return await partialTsconfigFilePromise; } +interface ITypeScriptConfigurationJsonAndPartialTsconfigFile { + typeScriptConfigurationJson: ITypeScriptConfigurationJson | undefined; + partialTsconfigFile: IPartialTsconfig | undefined; +} + export default class TypeScriptPlugin implements IHeftTaskPlugin { public accessor: ITypeScriptPluginAccessor = { onChangedFilesHook: new SyncHook(['changedFilesHookOptions']) @@ -289,33 +279,35 @@ export default class TypeScriptPlugin implements IHeftTaskPlugin { taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration ): Promise { - const typeScriptConfiguration: ITypeScriptConfigurationJson | undefined = - await loadTypeScriptConfigurationFileAsync(heftConfiguration, taskSession.logger.terminal); + const { typeScriptConfigurationJson, partialTsconfigFile } = await this._loadConfigAsync( + taskSession, + heftConfiguration + ); // We only care about the copy if static assets were specified. const copyOperations: ICopyOperation[] = []; + const staticAssetsConfig: IStaticAssetsCopyConfiguration | undefined = + typeScriptConfigurationJson?.staticAssetsToCopy; if ( - typeScriptConfiguration?.staticAssetsToCopy?.fileExtensions?.length || - typeScriptConfiguration?.staticAssetsToCopy?.includeGlobs?.length || - typeScriptConfiguration?.staticAssetsToCopy?.excludeGlobs?.length + staticAssetsConfig && + (staticAssetsConfig.fileExtensions?.length || + staticAssetsConfig.includeGlobs?.length || + staticAssetsConfig.excludeGlobs?.length) ) { const destinationFolderPaths: Set = new Set(); // Add the output folder and all additional module kind output folders as destinations - const tsconfigOutDir: string | undefined = await this._getTsconfigOutDirAsync( - taskSession, - heftConfiguration, - typeScriptConfiguration - ); + const tsconfigOutDir: string | undefined = partialTsconfigFile?.compilerOptions?.outDir; if (tsconfigOutDir) { destinationFolderPaths.add(tsconfigOutDir); } - for (const emitModule of typeScriptConfiguration?.additionalModuleKindsToEmit || []) { - destinationFolderPaths.add(`${heftConfiguration.buildFolderPath}/${emitModule.outFolderName}`); + + for (const emitModule of typeScriptConfigurationJson?.additionalModuleKindsToEmit || []) { + destinationFolderPaths.add(emitModule.outFolderName); } copyOperations.push({ - ...typeScriptConfiguration?.staticAssetsToCopy, + ...staticAssetsConfig, // For now - these may need to be revised later sourcePath: path.resolve(heftConfiguration.buildFolderPath, 'src'), @@ -324,6 +316,7 @@ export default class TypeScriptPlugin implements IHeftTaskPlugin { hardlink: false }); } + return copyOperations; } @@ -331,15 +324,9 @@ export default class TypeScriptPlugin implements IHeftTaskPlugin { taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration ): Promise { - const terminal: ITerminal = taskSession.logger.terminal; - - const typeScriptConfigurationJson: ITypeScriptConfigurationJson | undefined = - await loadTypeScriptConfigurationFileAsync(heftConfiguration, terminal); - - const partialTsconfigFile: IPartialTsconfig | undefined = await loadPartialTsconfigFileAsync( - heftConfiguration, - terminal, - typeScriptConfigurationJson + const { typeScriptConfigurationJson, partialTsconfigFile } = await this._loadConfigAsync( + taskSession, + heftConfiguration ); if (!partialTsconfigFile) { @@ -383,16 +370,24 @@ export default class TypeScriptPlugin implements IHeftTaskPlugin { return typeScriptBuilder; } - private async _getTsconfigOutDirAsync( + private async _loadConfigAsync( taskSession: IHeftTaskSession, - heftConfiguration: HeftConfiguration, - typeScriptConfiguration: ITypeScriptConfigurationJson | undefined - ): Promise { + heftConfiguration: HeftConfiguration + ): Promise { + const terminal: ITerminal = taskSession.logger.terminal; + + const typeScriptConfigurationJson: ITypeScriptConfigurationJson | undefined = + await loadTypeScriptConfigurationFileAsync(heftConfiguration, terminal); + const partialTsconfigFile: IPartialTsconfig | undefined = await loadPartialTsconfigFileAsync( heftConfiguration, - taskSession.logger.terminal, - typeScriptConfiguration + terminal, + typeScriptConfigurationJson ); - return partialTsconfigFile?.compilerOptions?.outDir; + + return { + typeScriptConfigurationJson, + partialTsconfigFile + }; } } diff --git a/libraries/heft-config-file/src/ConfigurationFileBase.ts b/libraries/heft-config-file/src/ConfigurationFileBase.ts index 448e2a5a38..bd17fb686c 100644 --- a/libraries/heft-config-file/src/ConfigurationFileBase.ts +++ b/libraries/heft-config-file/src/ConfigurationFileBase.ts @@ -3,75 +3,152 @@ import * as nodeJsPath from 'path'; import { JSONPath } from 'jsonpath-plus'; -import { JsonSchema, JsonFile, PackageJsonLookup, Import, FileSystem } from '@rushstack/node-core-library'; +import { JsonSchema, JsonFile, Import, FileSystem } from '@rushstack/node-core-library'; import type { ITerminal } from '@rushstack/terminal'; -import type { IRigConfig } from '@rushstack/rig-package'; interface IConfigurationJson { extends?: string; } +/* eslint-disable @typescript-eslint/typedef,@typescript-eslint/no-redeclare,@typescript-eslint/no-namespace,@typescript-eslint/naming-convention */ +// This structure is used so that consumers can pass raw string literals and have it typecheck, without breaking existing callers. /** * @beta + * + * The set of possible mechanisms for merging properties from parent configuration files. */ -export enum InheritanceType { +const InheritanceType = { /** * Append additional elements after elements from the parent file's property. Only applicable * for arrays. */ - append = 'append', + append: 'append', /** * Perform a shallow merge of additional elements after elements from the parent file's property. * Only applicable for objects. */ - merge = 'merge', + merge: 'merge', /** * Discard elements from the parent file's property */ - replace = 'replace', + replace: 'replace', /** * Custom inheritance functionality */ - custom = 'custom' + custom: 'custom' +} as const; +/** + * @beta + */ +type InheritanceType = (typeof InheritanceType)[keyof typeof InheritanceType]; +/** + * @beta + */ +declare namespace InheritanceType { + /** + * Append additional elements after elements from the parent file's property. Only applicable + * for arrays. + */ + export type append = typeof InheritanceType.append; + + /** + * Perform a shallow merge of additional elements after elements from the parent file's property. + * Only applicable for objects. + */ + export type merge = typeof InheritanceType.merge; + + /** + * Discard elements from the parent file's property + */ + export type replace = typeof InheritanceType.replace; + + /** + * Custom inheritance functionality + */ + export type custom = typeof InheritanceType.custom; } +export { InheritanceType }; + +/** + * @beta + * + * The set of possible resolution methods for fields that refer to paths. + */ +const PathResolutionMethod = { + /** + * Resolve a path relative to the configuration file + */ + resolvePathRelativeToConfigurationFile: 'resolvePathRelativeToConfigurationFile', + + /** + * Resolve a path relative to the root of the project containing the configuration file + */ + resolvePathRelativeToProjectRoot: 'resolvePathRelativeToProjectRoot', + /** + * Treat the property as a NodeJS-style require/import reference and resolve using standard + * NodeJS filesystem resolution + * + * @deprecated + * Use {@link (PathResolutionMethod:variable).nodeResolve} instead + */ + NodeResolve: 'NodeResolve', + + /** + * Treat the property as a NodeJS-style require/import reference and resolve using standard + * NodeJS filesystem resolution + */ + nodeResolve: 'nodeResolve', + + /** + * Resolve the property using a custom resolver. + */ + custom: 'custom' +} as const; +/** + * @beta + */ +type PathResolutionMethod = (typeof PathResolutionMethod)[keyof typeof PathResolutionMethod]; /** * @beta */ -export enum PathResolutionMethod { +declare namespace PathResolutionMethod { /** * Resolve a path relative to the configuration file */ - resolvePathRelativeToConfigurationFile = 'resolvePathRelativeToConfigurationFile', + export type resolvePathRelativeToConfigurationFile = + typeof PathResolutionMethod.resolvePathRelativeToConfigurationFile; /** * Resolve a path relative to the root of the project containing the configuration file */ - resolvePathRelativeToProjectRoot = 'resolvePathRelativeToProjectRoot', + export type resolvePathRelativeToProjectRoot = typeof PathResolutionMethod.resolvePathRelativeToProjectRoot; /** * Treat the property as a NodeJS-style require/import reference and resolve using standard * NodeJS filesystem resolution * * @deprecated - * Use {@link PathResolutionMethod.nodeResolve} instead + * Use {@link (PathResolutionMethod:namespace).nodeResolve} instead */ - NodeResolve = 'NodeResolve', + export type NodeResolve = typeof PathResolutionMethod.NodeResolve; /** * Treat the property as a NodeJS-style require/import reference and resolve using standard * NodeJS filesystem resolution */ - nodeResolve = 'nodeResolve', + export type nodeResolve = typeof PathResolutionMethod.nodeResolve; /** * Resolve the property using a custom resolver. */ - custom = 'custom' + export type custom = typeof PathResolutionMethod.custom; } +export { PathResolutionMethod }; +/* eslint-enable @typescript-eslint/typedef,@typescript-eslint/no-redeclare,@typescript-eslint/no-namespace,@typescript-eslint/naming-convention */ const CONFIGURATION_FILE_MERGE_BEHAVIOR_FIELD_REGEX: RegExp = /^\$([^\.]+)\.inheritanceType$/; export const CONFIGURATION_FILE_FIELD_ANNOTATION: unique symbol = Symbol( @@ -109,6 +186,10 @@ export interface IJsonPathMetadataResolverOptions { * The configuration file the property was obtained from. */ configurationFile: Partial; + /** + * If this is a project configuration file, the root folder of the project. + */ + projectFolderPath?: string; } /** @@ -206,6 +287,20 @@ export interface IJsonPathsMetadata { [jsonPath: string]: IJsonPathMetadata; } +/** + * A function to invoke after schema validation to validate the configuration file. + * If this function returns any value other than `true`, the configuration file API + * will throw an error indicating that custom validation failed. If the function wishes + * to provide its own error message, it may use any combination of the terminal and throwing + * its own error. + * @beta + */ +export type CustomValidationFunction = ( + configurationFile: TConfigurationFile, + resolvedConfigurationFilePathForLogging: string, + terminal: ITerminal +) => boolean; + /** * @beta */ @@ -226,6 +321,15 @@ export interface IConfigurationFileOptionsBase { * configuration files. */ propertyInheritanceDefaults?: IPropertyInheritanceDefaults; + + /** + * Use this property if you need to validate the configuration file in ways beyond what JSON schema can handle. + * This function will be invoked after JSON schema validation. + * + * If the file is valid, this function should return `true`, otherwise `ConfigurationFile` will throw an error + * indicating that custom validation failed. To suppress this error, the function may itself choose to throw. + */ + customValidationFunction?: CustomValidationFunction; } /** @@ -280,15 +384,31 @@ export interface IOriginalValueOptions { propertyName: keyof TParentProperty; } +interface IConfigurationFileCacheEntry { + resolvedConfigurationFilePath: string; + resolvedConfigurationFilePathForLogging: string; + parent?: IConfigurationFileCacheEntry; + configurationFile: TConfigFile & IConfigurationJson; +} + +/** + * Callback that returns a fallback configuration file path if the original configuration file was not found. + * @beta + */ +export type IOnConfigurationFileNotFoundCallback = ( + resolvedConfigurationFilePathForLogging: string +) => string | undefined; + /** * @beta */ export abstract class ConfigurationFileBase { private readonly _getSchema: () => JsonSchema; - private readonly _jsonPathMetadata: IJsonPathsMetadata; + private readonly _jsonPathMetadata: readonly [string, IJsonPathMetadata][]; private readonly _propertyInheritanceTypes: IPropertiesInheritance; private readonly _defaultPropertyInheritance: IPropertyInheritanceDefaults; + private readonly _customValidationFunction: CustomValidationFunction | undefined; private __schema: JsonSchema | undefined; private get _schema(): JsonSchema { if (!this.__schema) { @@ -298,9 +418,11 @@ export abstract class ConfigurationFileBase = new Map(); - private readonly _configPromiseCache: Map> = new Map(); - private readonly _packageJsonLookup: PackageJsonLookup = new PackageJsonLookup(); + private readonly _configCache: Map> = new Map(); + private readonly _configPromiseCache: Map< + string, + Promise> + > = new Map(); public constructor(options: IConfigurationFileOptions) { if (options.jsonSchemaObject) { @@ -309,9 +431,10 @@ export abstract class ConfigurationFileBase JsonSchema.fromFile(options.jsonSchemaPath); } - this._jsonPathMetadata = options.jsonPathMetadata || {}; + this._jsonPathMetadata = Object.entries(options.jsonPathMetadata || {}); this._propertyInheritanceTypes = options.propertyInheritance || {}; this._defaultPropertyInheritance = options.propertyInheritanceDefaults || {}; + this._customValidationFunction = options.customValidationFunction; } /** @@ -342,22 +465,69 @@ export abstract class ConfigurationFileBase( options: IOriginalValueOptions ): TValue | undefined { - const annotation: IConfigurationFileFieldAnnotation | undefined = - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (options.parentObject as any)[CONFIGURATION_FILE_FIELD_ANNOTATION]; - if (annotation && annotation.originalValues.hasOwnProperty(options.propertyName)) { + const { + [CONFIGURATION_FILE_FIELD_ANNOTATION]: annotation + }: { [CONFIGURATION_FILE_FIELD_ANNOTATION]?: IConfigurationFileFieldAnnotation } = + options.parentObject; + if (annotation?.originalValues.hasOwnProperty(options.propertyName)) { return annotation.originalValues[options.propertyName] as TValue; - } else { - return undefined; } } protected _loadConfigurationFileInnerWithCache( terminal: ITerminal, resolvedConfigurationFilePath: string, - visitedConfigurationFilePaths: Set, - rigConfig: IRigConfig | undefined + projectFolderPath: string | undefined, + onConfigurationFileNotFound?: IOnConfigurationFileNotFoundCallback ): TConfigurationFile { + const visitedConfigurationFilePaths: Set = new Set(); + const cacheEntry: IConfigurationFileCacheEntry = + this._loadConfigurationFileEntryWithCache( + terminal, + resolvedConfigurationFilePath, + visitedConfigurationFilePaths, + onConfigurationFileNotFound + ); + + const result: TConfigurationFile = this._finalizeConfigurationFile( + cacheEntry, + projectFolderPath, + terminal + ); + + return result; + } + + protected async _loadConfigurationFileInnerWithCacheAsync( + terminal: ITerminal, + resolvedConfigurationFilePath: string, + projectFolderPath: string | undefined, + onFileNotFound?: IOnConfigurationFileNotFoundCallback + ): Promise { + const visitedConfigurationFilePaths: Set = new Set(); + const cacheEntry: IConfigurationFileCacheEntry = + await this._loadConfigurationFileEntryWithCacheAsync( + terminal, + resolvedConfigurationFilePath, + visitedConfigurationFilePaths, + onFileNotFound + ); + + const result: TConfigurationFile = this._finalizeConfigurationFile( + cacheEntry, + projectFolderPath, + terminal + ); + + return result; + } + + private _loadConfigurationFileEntryWithCache( + terminal: ITerminal, + resolvedConfigurationFilePath: string, + visitedConfigurationFilePaths: Set, + onFileNotFound?: IOnConfigurationFileNotFoundCallback + ): IConfigurationFileCacheEntry { if (visitedConfigurationFilePaths.has(resolvedConfigurationFilePath)) { const resolvedConfigurationFilePathForLogging: string = ConfigurationFileBase._formatPathForLogging( resolvedConfigurationFilePath @@ -369,13 +539,15 @@ export abstract class ConfigurationFileBase | undefined = this._configCache.get( + resolvedConfigurationFilePath + ); if (!cacheEntry) { - cacheEntry = this._loadConfigurationFileInner( + cacheEntry = this._loadConfigurationFileEntry( terminal, resolvedConfigurationFilePath, visitedConfigurationFilePaths, - rigConfig + onFileNotFound ); this._configCache.set(resolvedConfigurationFilePath, cacheEntry); } @@ -383,12 +555,12 @@ export abstract class ConfigurationFileBase, - rigConfig: IRigConfig | undefined - ): Promise { + onConfigurationFileNotFound?: IOnConfigurationFileNotFoundCallback + ): Promise> { if (visitedConfigurationFilePaths.has(resolvedConfigurationFilePath)) { const resolvedConfigurationFilePathForLogging: string = ConfigurationFileBase._formatPathForLogging( resolvedConfigurationFilePath @@ -400,16 +572,15 @@ export abstract class ConfigurationFileBase | undefined = this._configPromiseCache.get( - resolvedConfigurationFilePath - ); + let cacheEntryPromise: Promise> | undefined = + this._configPromiseCache.get(resolvedConfigurationFilePath); if (!cacheEntryPromise) { - cacheEntryPromise = this._loadConfigurationFileInnerAsync( + cacheEntryPromise = this._loadConfigurationFileEntryAsync( terminal, resolvedConfigurationFilePath, visitedConfigurationFilePaths, - rigConfig - ).then((value: TConfigurationFile) => { + onConfigurationFileNotFound + ).then((value: IConfigurationFileCacheEntry) => { this._configCache.set(resolvedConfigurationFilePath, value); return value; }); @@ -419,48 +590,59 @@ export abstract class ConfigurationFileBase - ): TConfigurationFile | undefined; - - protected abstract _tryLoadConfigurationFileInRigAsync( - terminal: ITerminal, - rigConfig: IRigConfig, - visitedConfigurationFilePaths: Set - ): Promise; - - private _parseAndResolveConfigurationFile( + /** + * Parses the raw JSON-with-comments text of the configuration file. + * @param fileText - The text of the configuration file + * @param resolvedConfigurationFilePathForLogging - The path to the configuration file, formatted for logs + * @returns The parsed configuration file + */ + private _parseConfigurationFile( fileText: string, - resolvedConfigurationFilePath: string, resolvedConfigurationFilePathForLogging: string ): IConfigurationJson & TConfigurationFile { let configurationJson: IConfigurationJson & TConfigurationFile; try { configurationJson = JsonFile.parseString(fileText); } catch (e) { - throw new Error(`In config file "${resolvedConfigurationFilePathForLogging}": ${e}`); + throw new Error(`In configuration file "${resolvedConfigurationFilePathForLogging}": ${e}`); } - this._annotateProperties(resolvedConfigurationFilePath, configurationJson); + return configurationJson; + } + + /** + * Resolves all path properties and annotates properties with their original values. + * @param entry - The cache entry for the loaded configuration file + * @param projectFolderPath - The project folder path, if applicable + * @returns The configuration file with all path properties resolved + */ + private _contextualizeConfigurationFile( + entry: IConfigurationFileCacheEntry, + projectFolderPath: string | undefined + ): IConfigurationJson & TConfigurationFile { + // Deep copy the configuration file because different callers might contextualize properties differently. + const result: IConfigurationJson & TConfigurationFile = structuredClone(entry.configurationFile); - for (const [jsonPath, metadata] of Object.entries(this._jsonPathMetadata)) { + const { resolvedConfigurationFilePath } = entry; + + this._annotateProperties(resolvedConfigurationFilePath, result); + + for (const [jsonPath, metadata] of this._jsonPathMetadata) { JSONPath({ path: jsonPath, - json: configurationJson, + json: result, callback: (payload: unknown, payloadType: string, fullPayload: IJsonPathCallbackObject) => { const resolvedPath: string = this._resolvePathProperty( { propertyName: fullPayload.path, propertyValue: fullPayload.value, configurationFilePath: resolvedConfigurationFilePath, - configurationFile: configurationJson + configurationFile: result, + projectFolderPath }, metadata ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (fullPayload.parent as any)[fullPayload.parentProperty] = resolvedPath; + (fullPayload.parent as Record)[fullPayload.parentProperty] = resolvedPath; }, otherTypeCallback: () => { throw new Error('@other() tags are not supported'); @@ -468,18 +650,91 @@ export abstract class ConfigurationFileBase, + projectFolderPath: string | undefined + ): Partial { + const { parent, resolvedConfigurationFilePath } = entry; + const parentConfig: TConfigurationFile | {} = parent + ? this._contextualizeAndFlattenConfigurationFile(parent, projectFolderPath) + : {}; + + const currentConfig: IConfigurationJson & TConfigurationFile = this._contextualizeConfigurationFile( + entry, + projectFolderPath + ); + + const result: Partial = this._mergeConfigurationFiles( + parentConfig, + currentConfig, + resolvedConfigurationFilePath + ); + + return result; + } + + /** + * Resolves all path properties, merges parent properties, and validates the configuration file. + * @param entry - The cache entry for the loaded configuration file + * @param projectFolderPath - The project folder path, if applicable + * @param terminal - The terminal to log validation messages to + * @returns The finalized configuration file + */ + private _finalizeConfigurationFile( + entry: IConfigurationFileCacheEntry, + projectFolderPath: string | undefined, + terminal: ITerminal + ): TConfigurationFile { + const { resolvedConfigurationFilePathForLogging } = entry; + + const result: Partial = this._contextualizeAndFlattenConfigurationFile( + entry, + projectFolderPath + ); + + try { + this._schema.validateObject(result, resolvedConfigurationFilePathForLogging); + } catch (e) { + throw new Error(`Resolved configuration object does not match schema: ${e}`); + } + + if ( + this._customValidationFunction && + !this._customValidationFunction( + result as TConfigurationFile, + resolvedConfigurationFilePathForLogging, + terminal + ) + ) { + // To suppress this error, the function may throw its own error, such as an AlreadyReportedError if it already + // logged to the terminal. + throw new Error( + `Resolved configuration file at "${resolvedConfigurationFilePathForLogging}" failed custom validation.` + ); + } + + // If the schema validates, we can assume that the configuration file is complete. + return result as TConfigurationFile; } // NOTE: Internal calls to load a configuration file should use `_loadConfigurationFileInnerWithCache`. // Don't call this function directly, as it does not provide config file loop detection, // and you won't get the advantage of queueing up for a config file that is already loading. - private _loadConfigurationFileInner( + private _loadConfigurationFileEntry( terminal: ITerminal, resolvedConfigurationFilePath: string, visitedConfigurationFilePaths: Set, - rigConfig: IRigConfig | undefined - ): TConfigurationFile { + fileNotFoundFallback?: IOnConfigurationFileNotFoundCallback + ): IConfigurationFileCacheEntry { const resolvedConfigurationFilePathForLogging: string = ConfigurationFileBase._formatPathForLogging( resolvedConfigurationFilePath ); @@ -489,48 +744,46 @@ export abstract class ConfigurationFileBase | undefined; if (configurationJson.extends) { try { const resolvedParentConfigPath: string = Import.resolveModule({ modulePath: configurationJson.extends, baseFolderPath: nodeJsPath.dirname(resolvedConfigurationFilePath) }); - parentConfiguration = this._loadConfigurationFileInnerWithCache( + parentConfiguration = this._loadConfigurationFileEntryWithCache( terminal, resolvedParentConfigPath, - visitedConfigurationFilePaths, - undefined + visitedConfigurationFilePaths ); } catch (e) { if (FileSystem.isNotExistError(e as Error)) { @@ -544,30 +797,24 @@ export abstract class ConfigurationFileBase = this._mergeConfigurationFiles( - parentConfiguration || {}, - configurationJson, - resolvedConfigurationFilePath - ); - try { - this._schema.validateObject(result, resolvedConfigurationFilePathForLogging); - } catch (e) { - throw new Error(`Resolved configuration object does not match schema: ${e}`); - } - - // If the schema validates, we can assume that the configuration file is complete. - return result as TConfigurationFile; + const result: IConfigurationFileCacheEntry = { + configurationFile: configurationJson, + resolvedConfigurationFilePath, + resolvedConfigurationFilePathForLogging, + parent: parentConfiguration + }; + return result; } // NOTE: Internal calls to load a configuration file should use `_loadConfigurationFileInnerWithCacheAsync`. // Don't call this function directly, as it does not provide config file loop detection, // and you won't get the advantage of queueing up for a config file that is already loading. - private async _loadConfigurationFileInnerAsync( + private async _loadConfigurationFileEntryAsync( terminal: ITerminal, resolvedConfigurationFilePath: string, visitedConfigurationFilePaths: Set, - rigConfig: IRigConfig | undefined - ): Promise { + fileNotFoundFallback?: IOnConfigurationFileNotFoundCallback + ): Promise> { const resolvedConfigurationFilePathForLogging: string = ConfigurationFileBase._formatPathForLogging( resolvedConfigurationFilePath ); @@ -577,48 +824,46 @@ export abstract class ConfigurationFileBase | undefined; if (configurationJson.extends) { try { - const resolvedParentConfigPath: string = Import.resolveModule({ + const resolvedParentConfigPath: string = await Import.resolveModuleAsync({ modulePath: configurationJson.extends, baseFolderPath: nodeJsPath.dirname(resolvedConfigurationFilePath) }); - parentConfiguration = await this._loadConfigurationFileInnerWithCacheAsync( + parentConfiguration = await this._loadConfigurationFileEntryWithCacheAsync( terminal, resolvedParentConfigPath, - visitedConfigurationFilePaths, - undefined + visitedConfigurationFilePaths ); } catch (e) { if (FileSystem.isNotExistError(e as Error)) { @@ -632,53 +877,40 @@ export abstract class ConfigurationFileBase = this._mergeConfigurationFiles( - parentConfiguration || {}, - configurationJson, - resolvedConfigurationFilePath - ); - try { - this._schema.validateObject(result, resolvedConfigurationFilePathForLogging); - } catch (e) { - throw new Error(`Resolved configuration object does not match schema: ${e}`); - } - - // If the schema validates, we can assume that the configuration file is complete. - return result as TConfigurationFile; + const result: IConfigurationFileCacheEntry = { + configurationFile: configurationJson, + resolvedConfigurationFilePath, + resolvedConfigurationFilePathForLogging, + parent: parentConfiguration + }; + return result; } - private _annotateProperties(resolvedConfigurationFilePath: string, obj: TObject): void { - if (!obj) { + private _annotateProperties(resolvedConfigurationFilePath: string, root: TObject): void { + if (!root) { return; } - if (typeof obj === 'object') { - this._annotateProperty(resolvedConfigurationFilePath, obj); + const queue: Set = new Set([root]); + for (const obj of queue) { + if (obj && typeof obj === 'object') { + (obj as unknown as IAnnotatedField)[CONFIGURATION_FILE_FIELD_ANNOTATION] = { + configurationFilePath: resolvedConfigurationFilePath, + originalValues: { ...obj } + }; - for (const objValue of Object.values(obj)) { - this._annotateProperties(resolvedConfigurationFilePath, objValue); + for (const objValue of Object.values(obj)) { + queue.add(objValue as TObject); + } } } } - private _annotateProperty(resolvedConfigurationFilePath: string, obj: TObject): void { - if (!obj) { - return; - } - - if (typeof obj === 'object') { - (obj as unknown as IAnnotatedField)[CONFIGURATION_FILE_FIELD_ANNOTATION] = { - configurationFilePath: resolvedConfigurationFilePath, - originalValues: { ...obj } - }; - } - } - private _resolvePathProperty( resolverOptions: IJsonPathMetadataResolverOptions, metadata: IJsonPathMetadata ): string { - const { propertyValue, configurationFilePath } = resolverOptions; + const { propertyValue, configurationFilePath, projectFolderPath } = resolverOptions; const resolutionMethod: PathResolutionMethod | undefined = metadata.pathResolutionMethod; if (resolutionMethod === undefined) { return propertyValue; @@ -690,13 +922,12 @@ export abstract class ConfigurationFileBase = new Set(Object.keys(currentObject)); - // An array of property names that should be included in the resulting object. - const filteredObjectPropertyNames: (keyof TField)[] = []; // A map of property names to their inheritance type. const inheritanceTypeMap: Map> = new Map(); + // The set of property names that should be included in the resulting object + // All names from the parent are assumed to already be filtered. + const mergedPropertyNames: Set = new Set(Object.keys(parentObject)); + // Do a first pass to gather and strip the inheritance type annotations from the merging object. for (const propertyName of currentObjectPropertyNames) { if (ignoreProperties && ignoreProperties.has(propertyName)) { @@ -827,18 +1060,12 @@ export abstract class ConfigurationFileBase = new Set([ - ...Object.keys(parentObject), - ...filteredObjectPropertyNames - ]); - // Cycle through properties and merge them - for (const propertyName of propertyNames) { + for (const propertyName of mergedPropertyNames) { const propertyValue: TField[keyof TField] | undefined = currentObject[propertyName]; const parentPropertyValue: TField[keyof TField] | undefined = parentObject[propertyName]; diff --git a/libraries/heft-config-file/src/NonProjectConfigurationFile.ts b/libraries/heft-config-file/src/NonProjectConfigurationFile.ts index d3a7bd23dc..20929de4d0 100644 --- a/libraries/heft-config-file/src/NonProjectConfigurationFile.ts +++ b/libraries/heft-config-file/src/NonProjectConfigurationFile.ts @@ -1,9 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { FileSystem } from '@rushstack/node-core-library'; +import { FileSystem, PackageJsonLookup } from '@rushstack/node-core-library'; import type { ITerminal } from '@rushstack/terminal'; -import type { IRigConfig } from '@rushstack/rig-package'; import { ConfigurationFileBase } from './ConfigurationFileBase'; @@ -19,7 +18,11 @@ export class NonProjectConfigurationFile extends Configurati * `extends` properties. Will throw an error if the file cannot be found. */ public loadConfigurationFile(terminal: ITerminal, filePath: string): TConfigurationFile { - return this._loadConfigurationFileInnerWithCache(terminal, filePath, new Set(), undefined); + return this._loadConfigurationFileInnerWithCache( + terminal, + filePath, + PackageJsonLookup.instance.tryGetPackageFolderFor(filePath) + ); } /** @@ -33,8 +36,7 @@ export class NonProjectConfigurationFile extends Configurati return await this._loadConfigurationFileInnerWithCacheAsync( terminal, filePath, - new Set(), - undefined + PackageJsonLookup.instance.tryGetPackageFolderFor(filePath) ); } @@ -70,22 +72,4 @@ export class NonProjectConfigurationFile extends Configurati throw e; } } - - protected _tryLoadConfigurationFileInRig( - terminal: ITerminal, - rigConfig: IRigConfig, - visitedConfigurationFilePaths: Set - ): TConfigurationFile | undefined { - // This is a no-op because we don't support rigging for non-project configuration files - return undefined; - } - - protected async _tryLoadConfigurationFileInRigAsync( - terminal: ITerminal, - rigConfig: IRigConfig, - visitedConfigurationFilePaths: Set - ): Promise { - // This is a no-op because we don't support rigging for non-project configuration files - return undefined; - } } diff --git a/libraries/heft-config-file/src/ProjectConfigurationFile.ts b/libraries/heft-config-file/src/ProjectConfigurationFile.ts index 066ed7a532..6b93d26272 100644 --- a/libraries/heft-config-file/src/ProjectConfigurationFile.ts +++ b/libraries/heft-config-file/src/ProjectConfigurationFile.ts @@ -2,11 +2,15 @@ // See LICENSE in the project root for license information. import * as nodeJsPath from 'path'; -import { FileSystem } from '@rushstack/node-core-library'; +import { FileSystem, PackageJsonLookup } from '@rushstack/node-core-library'; import type { ITerminal } from '@rushstack/terminal'; import type { IRigConfig } from '@rushstack/rig-package'; -import { ConfigurationFileBase, type IConfigurationFileOptions } from './ConfigurationFileBase'; +import { + ConfigurationFileBase, + type IOnConfigurationFileNotFoundCallback, + type IConfigurationFileOptions +} from './ConfigurationFileBase'; /** * @beta @@ -18,6 +22,15 @@ export interface IProjectConfigurationFileOptions { projectRelativeFilePath: string; } +/** + * Alias for the constructor type for {@link ProjectConfigurationFile}. + * @beta + */ +export type IProjectConfigurationFileSpecification = IConfigurationFileOptions< + TConfigFile, + IProjectConfigurationFileOptions +>; + /** * @beta */ @@ -28,9 +41,7 @@ export class ProjectConfigurationFile extends ConfigurationF /** {@inheritDoc IProjectConfigurationFileOptions.projectRelativeFilePath} */ public readonly projectRelativeFilePath: string; - public constructor( - options: IConfigurationFileOptions - ) { + public constructor(options: IProjectConfigurationFileSpecification) { super(options); this.projectRelativeFilePath = options.projectRelativeFilePath; } @@ -49,8 +60,8 @@ export class ProjectConfigurationFile extends ConfigurationF return this._loadConfigurationFileInnerWithCache( terminal, projectConfigurationFilePath, - new Set(), - rigConfig + PackageJsonLookup.instance.tryGetPackageFolderFor(projectPath), + this._getRigConfigFallback(terminal, rigConfig) ); } @@ -68,8 +79,8 @@ export class ProjectConfigurationFile extends ConfigurationF return await this._loadConfigurationFileInnerWithCacheAsync( terminal, projectConfigurationFilePath, - new Set(), - rigConfig + PackageJsonLookup.instance.tryGetPackageFolderFor(projectPath), + this._getRigConfigFallback(terminal, rigConfig) ); } @@ -111,77 +122,28 @@ export class ProjectConfigurationFile extends ConfigurationF } } - protected _tryLoadConfigurationFileInRig( - terminal: ITerminal, - rigConfig: IRigConfig, - visitedConfigurationFilePaths: Set - ): TConfigurationFile | undefined { - if (rigConfig.rigFound) { - const rigProfileFolder: string = rigConfig.getResolvedProfileFolder(); - try { - return this._loadConfigurationFileInnerWithCache( - terminal, - nodeJsPath.resolve(rigProfileFolder, this.projectRelativeFilePath), - visitedConfigurationFilePaths, - undefined - ); - } catch (e) { - // Ignore cases where a configuration file doesn't exist in a rig - if (!FileSystem.isNotExistError(e as Error)) { - throw e; - } else { - terminal.writeDebugLine( - `Configuration file "${ - this.projectRelativeFilePath - }" not found in rig ("${ConfigurationFileBase._formatPathForLogging(rigProfileFolder)}")` - ); - } - } - } else { - terminal.writeDebugLine( - `No rig found for "${ConfigurationFileBase._formatPathForLogging(rigConfig.projectFolderPath)}"` - ); - } - - return undefined; + private _getConfigurationFilePathForProject(projectPath: string): string { + return nodeJsPath.resolve(projectPath, this.projectRelativeFilePath); } - protected async _tryLoadConfigurationFileInRigAsync( + private _getRigConfigFallback( terminal: ITerminal, - rigConfig: IRigConfig, - visitedConfigurationFilePaths: Set - ): Promise { - if (rigConfig.rigFound) { - const rigProfileFolder: string = await rigConfig.getResolvedProfileFolderAsync(); - try { - return await this._loadConfigurationFileInnerWithCacheAsync( - terminal, - nodeJsPath.resolve(rigProfileFolder, this.projectRelativeFilePath), - visitedConfigurationFilePaths, - undefined - ); - } catch (e) { - // Ignore cases where a configuration file doesn't exist in a rig - if (!FileSystem.isNotExistError(e as Error)) { - throw e; - } else { - terminal.writeDebugLine( - `Configuration file "${ - this.projectRelativeFilePath - }" not found in rig ("${ConfigurationFileBase._formatPathForLogging(rigProfileFolder)}")` - ); + rigConfig: IRigConfig | undefined + ): IOnConfigurationFileNotFoundCallback | undefined { + return rigConfig + ? (resolvedConfigurationFilePathForLogging: string) => { + if (rigConfig.rigFound) { + const rigProfileFolder: string = rigConfig.getResolvedProfileFolder(); + terminal.writeDebugLine( + `Configuration file "${resolvedConfigurationFilePathForLogging}" does not exist. Attempting to load via rig ("${ConfigurationFileBase._formatPathForLogging(rigProfileFolder)}").` + ); + return nodeJsPath.resolve(rigProfileFolder, this.projectRelativeFilePath); + } else { + terminal.writeDebugLine( + `No rig found for "${ConfigurationFileBase._formatPathForLogging(rigConfig.projectFolderPath)}"` + ); + } } - } - } else { - terminal.writeDebugLine( - `No rig found for "${ConfigurationFileBase._formatPathForLogging(rigConfig.projectFolderPath)}"` - ); - } - - return undefined; - } - - private _getConfigurationFilePathForProject(projectPath: string): string { - return nodeJsPath.resolve(projectPath, this.projectRelativeFilePath); + : undefined; } } diff --git a/libraries/heft-config-file/src/index.ts b/libraries/heft-config-file/src/index.ts index 4ae1fa97b9..bd912a1ea1 100644 --- a/libraries/heft-config-file/src/index.ts +++ b/libraries/heft-config-file/src/index.ts @@ -10,6 +10,7 @@ export { ConfigurationFileBase, + type CustomValidationFunction, type IConfigurationFileOptionsBase, type IConfigurationFileOptionsWithJsonSchemaFilePath, type IConfigurationFileOptionsWithJsonSchemaObject, @@ -21,6 +22,7 @@ export { type IJsonPathsMetadata, InheritanceType, type INonCustomJsonPathMetadata, + type IOnConfigurationFileNotFoundCallback, type IOriginalValueOptions, type IPropertiesInheritance, type IPropertyInheritance, @@ -44,7 +46,11 @@ export const ConfigurationFile: typeof ProjectConfigurationFile = ProjectConfigu // eslint-disable-next-line @typescript-eslint/no-redeclare export type ConfigurationFile = ProjectConfigurationFile; -export { ProjectConfigurationFile, type IProjectConfigurationFileOptions } from './ProjectConfigurationFile'; +export { + ProjectConfigurationFile, + type IProjectConfigurationFileOptions, + type IProjectConfigurationFileSpecification +} from './ProjectConfigurationFile'; export { NonProjectConfigurationFile } from './NonProjectConfigurationFile'; export * as TestUtilities from './TestUtilities'; diff --git a/libraries/heft-config-file/src/test/ConfigurationFile.test.ts b/libraries/heft-config-file/src/test/ConfigurationFile.test.ts index a47516a796..117be99701 100644 --- a/libraries/heft-config-file/src/test/ConfigurationFile.test.ts +++ b/libraries/heft-config-file/src/test/ConfigurationFile.test.ts @@ -1826,7 +1826,7 @@ describe('ConfigurationFile', () => { // a newline on Windows, and a curly brace on other platforms, even though the location is // accurate in both cases. Use a regex to match either. expect(() => configFileLoader.loadConfigurationFileForProject(terminal, __dirname)).toThrowError( - /In config file "\/lib\/test\/errorCases\/invalidJson\/config.json": SyntaxError: Unexpected token '(}|\\n)' at 2:19/ + /In configuration file "\/lib\/test\/errorCases\/invalidJson\/config.json": SyntaxError: Unexpected token '(}|\\n)' at 2:19/ ); jest.restoreAllMocks(); @@ -1861,7 +1861,7 @@ describe('ConfigurationFile', () => { await expect( configFileLoader.loadConfigurationFileForProjectAsync(terminal, __dirname) ).rejects.toThrowError( - /In config file "\/lib\/test\/errorCases\/invalidJson\/config.json": SyntaxError: Unexpected token '(}|\\n)' at 2:19/ + /In configuration file "\/lib\/test\/errorCases\/invalidJson\/config.json": SyntaxError: Unexpected token '(}|\\n)' at 2:19/ ); jest.restoreAllMocks(); diff --git a/libraries/heft-config-file/src/test/__snapshots__/ConfigurationFile.test.ts.snap b/libraries/heft-config-file/src/test/__snapshots__/ConfigurationFile.test.ts.snap index 2dc578c2fa..5748f708a9 100644 --- a/libraries/heft-config-file/src/test/__snapshots__/ConfigurationFile.test.ts.snap +++ b/libraries/heft-config-file/src/test/__snapshots__/ConfigurationFile.test.ts.snap @@ -748,7 +748,7 @@ Object { exports[`ConfigurationFile loading a rig correctly loads a config file inside a rig 1`] = ` Object { - "debug": "Config file \\"/lib/test/project-referencing-rig/config/simplestConfigFile.json\\" does not exist. Attempting to load via rig.[n]", + "debug": "Configuration file \\"/lib/test/project-referencing-rig/config/simplestConfigFile.json\\" does not exist. Attempting to load via rig (\\"/lib/test/project-referencing-rig/node_modules/test-rig/profiles/default\\").[n]", "error": "", "log": "", "verbose": "", @@ -758,7 +758,7 @@ Object { exports[`ConfigurationFile loading a rig correctly loads a config file inside a rig async 1`] = ` Object { - "debug": "Config file \\"/lib/test/project-referencing-rig/config/simplestConfigFile.json\\" does not exist. Attempting to load via rig.[n]", + "debug": "Configuration file \\"/lib/test/project-referencing-rig/config/simplestConfigFile.json\\" does not exist. Attempting to load via rig (\\"/lib/test/project-referencing-rig/node_modules/test-rig/profiles/default\\").[n]", "error": "", "log": "", "verbose": "", @@ -768,7 +768,7 @@ Object { exports[`ConfigurationFile loading a rig correctly loads a config file inside a rig via tryLoadConfigurationFileForProject 1`] = ` Object { - "debug": "Config file \\"/lib/test/project-referencing-rig/config/simplestConfigFile.json\\" does not exist. Attempting to load via rig.[n]", + "debug": "Configuration file \\"/lib/test/project-referencing-rig/config/simplestConfigFile.json\\" does not exist. Attempting to load via rig (\\"/lib/test/project-referencing-rig/node_modules/test-rig/profiles/default\\").[n]", "error": "", "log": "", "verbose": "", @@ -778,7 +778,7 @@ Object { exports[`ConfigurationFile loading a rig correctly loads a config file inside a rig via tryLoadConfigurationFileForProjectAsync 1`] = ` Object { - "debug": "Config file \\"/lib/test/project-referencing-rig/config/simplestConfigFile.json\\" does not exist. Attempting to load via rig.[n]", + "debug": "Configuration file \\"/lib/test/project-referencing-rig/config/simplestConfigFile.json\\" does not exist. Attempting to load via rig (\\"/lib/test/project-referencing-rig/node_modules/test-rig/profiles/default\\").[n]", "error": "", "log": "", "verbose": "", @@ -790,7 +790,7 @@ exports[`ConfigurationFile loading a rig throws an error when a config file does exports[`ConfigurationFile loading a rig throws an error when a config file doesn't exist in a project referencing a rig, which also doesn't have the file 2`] = ` Object { - "debug": "Config file \\"/lib/test/project-referencing-rig/config/notExist.json\\" does not exist. Attempting to load via rig.[n]Configuration file \\"/lib/test/project-referencing-rig/node_modules/test-rig/profiles/default/config/notExist.json\\" not found.[n]Configuration file \\"config/notExist.json\\" not found in rig (\\"/lib/test/project-referencing-rig/node_modules/test-rig/profiles/default\\")[n]", + "debug": "Configuration file \\"/lib/test/project-referencing-rig/config/notExist.json\\" does not exist. Attempting to load via rig (\\"/lib/test/project-referencing-rig/node_modules/test-rig/profiles/default\\").[n]Configuration file \\"/lib/test/project-referencing-rig/node_modules/test-rig/profiles/default/config/notExist.json\\" not found.[n]Configuration file \\"/lib/test/project-referencing-rig/config/notExist.json\\" not found.[n]", "error": "", "log": "", "verbose": "", @@ -802,7 +802,7 @@ exports[`ConfigurationFile loading a rig throws an error when a config file does exports[`ConfigurationFile loading a rig throws an error when a config file doesn't exist in a project referencing a rig, which also doesn't have the file async 2`] = ` Object { - "debug": "Config file \\"/lib/test/project-referencing-rig/config/notExist.json\\" does not exist. Attempting to load via rig.[n]Configuration file \\"/lib/test/project-referencing-rig/node_modules/test-rig/profiles/default/config/notExist.json\\" not found.[n]Configuration file \\"config/notExist.json\\" not found in rig (\\"/lib/test/project-referencing-rig/node_modules/test-rig/profiles/default\\")[n]", + "debug": "Configuration file \\"/lib/test/project-referencing-rig/config/notExist.json\\" does not exist. Attempting to load via rig (\\"/lib/test/project-referencing-rig/node_modules/test-rig/profiles/default\\").[n]Configuration file \\"/lib/test/project-referencing-rig/node_modules/test-rig/profiles/default/config/notExist.json\\" not found.[n]Configuration file \\"/lib/test/project-referencing-rig/config/notExist.json\\" not found.[n]", "error": "", "log": "", "verbose": "",