From 74e3deee1acc2892ca9b31881a9cc241424a30a3 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 2 Dec 2024 18:48:05 -0500 Subject: [PATCH 01/12] Create a non-project version of ConfigurationFile. --- common/reviews/api/heft-config-file.api.md | 63 +++- ...rationFile.ts => ConfigurationFileBase.ts} | 234 +++--------- .../src/NonProjectConfigurationFile.ts | 91 +++++ .../src/ProjectConfigurationFile.ts | 187 ++++++++++ libraries/heft-config-file/src/index.ts | 11 +- .../src/test/ConfigurationFile.test.ts | 334 ++++++++++-------- 6 files changed, 569 insertions(+), 351 deletions(-) rename libraries/heft-config-file/src/{ConfigurationFile.ts => ConfigurationFileBase.ts} (84%) create mode 100644 libraries/heft-config-file/src/NonProjectConfigurationFile.ts create mode 100644 libraries/heft-config-file/src/ProjectConfigurationFile.ts diff --git a/common/reviews/api/heft-config-file.api.md b/common/reviews/api/heft-config-file.api.md index 24dd86b64a8..ad66b7854ec 100644 --- a/common/reviews/api/heft-config-file.api.md +++ b/common/reviews/api/heft-config-file.api.md @@ -4,47 +4,61 @@ ```ts -import type { IRigConfig } from '@rushstack/rig-package'; +import { IRigConfig } from '@rushstack/rig-package'; import type { ITerminal } from '@rushstack/terminal'; // @beta (undocumented) -export class ConfigurationFile { - constructor(options: IConfigurationFileOptions); - // @internal (undocumented) - static _formatPathForLogging: (path: string) => string; - getObjectSourceFilePath(obj: TObject): string | undefined; - getPropertyOriginalValue(options: IOriginalValueOptions): TValue | undefined; +export class ConfigurationFile extends ConfigurationFileBase { + constructor(options: IConfigurationFileOptions); 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) +export abstract class ConfigurationFileBase { + constructor(options: IConfigurationFileOptions); + // @internal (undocumented) + static _formatPathForLogging: (path: string) => string; + getObjectSourceFilePath(obj: TObject): string | undefined; + getPropertyOriginalValue(options: IOriginalValueOptions): TValue | undefined; + // (undocumented) + protected _loadConfigurationFileInnerWithCache(terminal: ITerminal, resolvedConfigurationFilePath: string, visitedConfigurationFilePaths: Set, rigConfig: IRigConfig | undefined): 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; } // @beta (undocumented) -export type IConfigurationFileOptions = IConfigurationFileOptionsWithJsonSchemaFilePath | IConfigurationFileOptionsWithJsonSchemaObject; +export type IConfigurationFileOptions = IConfigurationFileOptionsWithJsonSchemaFilePath | IConfigurationFileOptionsWithJsonSchemaObject; // @beta (undocumented) export interface IConfigurationFileOptionsBase { jsonPathMetadata?: IJsonPathsMetadata; - projectRelativeFilePath: string; propertyInheritance?: IPropertiesInheritance; propertyInheritanceDefaults?: IPropertyInheritanceDefaults; } // @beta (undocumented) -export interface IConfigurationFileOptionsWithJsonSchemaFilePath extends IConfigurationFileOptionsBase { - // (undocumented) - jsonSchemaObject?: never; +export type IConfigurationFileOptionsWithJsonSchemaFilePath = IConfigurationFileOptionsBase & TExtraOptions & { jsonSchemaPath: string; -} + jsonSchemaObject?: never; +}; // @beta (undocumented) -export interface IConfigurationFileOptionsWithJsonSchemaObject extends IConfigurationFileOptionsBase { +export type IConfigurationFileOptionsWithJsonSchemaObject = IConfigurationFileOptionsBase & TExtraOptions & { jsonSchemaObject: object; - // (undocumented) jsonSchemaPath?: never; -} +}; // @beta export interface ICustomJsonPathMetadata { @@ -95,6 +109,11 @@ export interface IOriginalValueOptions { propertyName: keyof TParentProperty; } +// @beta (undocumented) +export interface IProjectConfigurationFileOptions { + projectRelativeFilePath: string; +} + // @beta (undocumented) export type IPropertiesInheritance = { [propertyName in keyof TConfigurationFile]?: IPropertyInheritance | ICustomPropertyInheritance; @@ -114,6 +133,18 @@ export interface IPropertyInheritanceDefaults { object?: IPropertyInheritance; } +// @beta (undocumented) +export class NonProjectConfigurationFile extends ConfigurationFileBase { + loadConfigurationFile(terminal: ITerminal, filePath: string): TConfigurationFile; + 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 (undocumented) export enum PathResolutionMethod { custom = "custom", diff --git a/libraries/heft-config-file/src/ConfigurationFile.ts b/libraries/heft-config-file/src/ConfigurationFileBase.ts similarity index 84% rename from libraries/heft-config-file/src/ConfigurationFile.ts rename to libraries/heft-config-file/src/ConfigurationFileBase.ts index 5f509e56306..434f047cebc 100644 --- a/libraries/heft-config-file/src/ConfigurationFile.ts +++ b/libraries/heft-config-file/src/ConfigurationFileBase.ts @@ -208,11 +208,6 @@ export interface IJsonPathsMetadata { * @beta */ export interface IConfigurationFileOptionsBase { - /** - * A project root-relative path to the configuration file that should be loaded. - */ - projectRelativeFilePath: string; - /** * Use this property to specify how JSON nodes are postprocessed. */ @@ -234,33 +229,39 @@ export interface IConfigurationFileOptionsBase { /** * @beta */ -export interface IConfigurationFileOptionsWithJsonSchemaFilePath - extends IConfigurationFileOptionsBase { - /** - * The path to the schema for the configuration file. - */ - jsonSchemaPath: string; - jsonSchemaObject?: never; -} +export type IConfigurationFileOptionsWithJsonSchemaFilePath< + TConfigurationFile, + TExtraOptions extends {} +> = IConfigurationFileOptionsBase & + TExtraOptions & { + /** + * The path to the schema for the configuration file. + */ + jsonSchemaPath: string; + jsonSchemaObject?: never; + }; /** * @beta */ -export interface IConfigurationFileOptionsWithJsonSchemaObject - extends IConfigurationFileOptionsBase { - /** - * The schema for the configuration file. - */ - jsonSchemaObject: object; - jsonSchemaPath?: never; -} +export type IConfigurationFileOptionsWithJsonSchemaObject< + TConfigurationFile, + TExtraOptions extends {} +> = IConfigurationFileOptionsBase & + TExtraOptions & { + /** + * The schema for the configuration file. + */ + jsonSchemaObject: object; + jsonSchemaPath?: never; + }; /** * @beta */ -export type IConfigurationFileOptions = - | IConfigurationFileOptionsWithJsonSchemaFilePath - | IConfigurationFileOptionsWithJsonSchemaObject; +export type IConfigurationFileOptions = + | IConfigurationFileOptionsWithJsonSchemaFilePath + | IConfigurationFileOptionsWithJsonSchemaObject; interface IJsonPathCallbackObject { path: string; @@ -280,12 +281,9 @@ export interface IOriginalValueOptions { /** * @beta */ -export class ConfigurationFile { +export abstract class ConfigurationFileBase { private readonly _getSchema: () => JsonSchema; - /** {@inheritDoc IConfigurationFileOptionsBase.projectRelativeFilePath} */ - public readonly projectRelativeFilePath: string; - private readonly _jsonPathMetadata: IJsonPathsMetadata; private readonly _propertyInheritanceTypes: IPropertiesInheritance; private readonly _defaultPropertyInheritance: IPropertyInheritanceDefaults; @@ -302,9 +300,7 @@ export class ConfigurationFile { private readonly _configPromiseCache: Map> = new Map(); private readonly _packageJsonLookup: PackageJsonLookup = new PackageJsonLookup(); - public constructor(options: IConfigurationFileOptions) { - this.projectRelativeFilePath = options.projectRelativeFilePath; - + public constructor(options: IConfigurationFileOptions) { if (options.jsonSchemaObject) { this._getSchema = () => JsonSchema.fromLoadedObject(options.jsonSchemaObject); } else { @@ -316,82 +312,6 @@ export class ConfigurationFile { this._defaultPropertyInheritance = options.propertyInheritanceDefaults || {}; } - /** - * Find and return a configuration file for the specified project, automatically resolving - * `extends` properties and handling rigged configuration files. Will throw an error if a configuration - * file cannot be found in the rig or project config folder. - */ - public loadConfigurationFileForProject( - terminal: ITerminal, - projectPath: string, - rigConfig?: IRigConfig - ): TConfigurationFile { - const projectConfigurationFilePath: string = this._getConfigurationFilePathForProject(projectPath); - return this._loadConfigurationFileInnerWithCache( - terminal, - projectConfigurationFilePath, - new Set(), - rigConfig - ); - } - - /** - * Find and return a configuration file for the specified project, automatically resolving - * `extends` properties and handling rigged configuration files. Will throw an error if a configuration - * file cannot be found in the rig or project config folder. - */ - public async loadConfigurationFileForProjectAsync( - terminal: ITerminal, - projectPath: string, - rigConfig?: IRigConfig - ): Promise { - const projectConfigurationFilePath: string = this._getConfigurationFilePathForProject(projectPath); - return await this._loadConfigurationFileInnerWithCacheAsync( - terminal, - projectConfigurationFilePath, - new Set(), - rigConfig - ); - } - - /** - * This function is identical to {@link ConfigurationFile.loadConfigurationFileForProject}, except - * that it returns `undefined` instead of throwing an error if the configuration file cannot be found. - */ - public tryLoadConfigurationFileForProject( - terminal: ITerminal, - projectPath: string, - rigConfig?: IRigConfig - ): TConfigurationFile | undefined { - try { - return this.loadConfigurationFileForProject(terminal, projectPath, rigConfig); - } catch (e) { - if (FileSystem.isNotExistError(e as Error)) { - return undefined; - } - throw e; - } - } - - /** - * This function is identical to {@link ConfigurationFile.loadConfigurationFileForProjectAsync}, except - * that it returns `undefined` instead of throwing an error if the configuration file cannot be found. - */ - public async tryLoadConfigurationFileForProjectAsync( - terminal: ITerminal, - projectPath: string, - rigConfig?: IRigConfig - ): Promise { - try { - return await this.loadConfigurationFileForProjectAsync(terminal, projectPath, rigConfig); - } catch (e) { - if (FileSystem.isNotExistError(e as Error)) { - return undefined; - } - throw e; - } - } - /** * @internal */ @@ -430,14 +350,14 @@ export class ConfigurationFile { } } - private _loadConfigurationFileInnerWithCache( + protected _loadConfigurationFileInnerWithCache( terminal: ITerminal, resolvedConfigurationFilePath: string, visitedConfigurationFilePaths: Set, rigConfig: IRigConfig | undefined ): TConfigurationFile { if (visitedConfigurationFilePaths.has(resolvedConfigurationFilePath)) { - const resolvedConfigurationFilePathForLogging: string = ConfigurationFile._formatPathForLogging( + const resolvedConfigurationFilePathForLogging: string = ConfigurationFileBase._formatPathForLogging( resolvedConfigurationFilePath ); throw new Error( @@ -461,14 +381,14 @@ export class ConfigurationFile { return cacheEntry; } - private async _loadConfigurationFileInnerWithCacheAsync( + protected async _loadConfigurationFileInnerWithCacheAsync( terminal: ITerminal, resolvedConfigurationFilePath: string, visitedConfigurationFilePaths: Set, rigConfig: IRigConfig | undefined ): Promise { if (visitedConfigurationFilePaths.has(resolvedConfigurationFilePath)) { - const resolvedConfigurationFilePathForLogging: string = ConfigurationFile._formatPathForLogging( + const resolvedConfigurationFilePathForLogging: string = ConfigurationFileBase._formatPathForLogging( resolvedConfigurationFilePath ); throw new Error( @@ -497,6 +417,18 @@ export class ConfigurationFile { return await cacheEntryPromise; } + protected abstract _tryLoadConfigurationFileInRig( + terminal: ITerminal, + rigConfig: IRigConfig, + visitedConfigurationFilePaths: Set + ): TConfigurationFile | undefined; + + protected abstract _tryLoadConfigurationFileInRigAsync( + terminal: ITerminal, + rigConfig: IRigConfig, + visitedConfigurationFilePaths: Set + ): Promise; + private _parseAndResolveConfigurationFile( fileText: string, resolvedConfigurationFilePath: string, @@ -546,7 +478,7 @@ export class ConfigurationFile { visitedConfigurationFilePaths: Set, rigConfig: IRigConfig | undefined ): TConfigurationFile { - const resolvedConfigurationFilePathForLogging: string = ConfigurationFile._formatPathForLogging( + const resolvedConfigurationFilePathForLogging: string = ConfigurationFileBase._formatPathForLogging( resolvedConfigurationFilePath ); @@ -634,7 +566,7 @@ export class ConfigurationFile { visitedConfigurationFilePaths: Set, rigConfig: IRigConfig | undefined ): Promise { - const resolvedConfigurationFilePathForLogging: string = ConfigurationFile._formatPathForLogging( + const resolvedConfigurationFilePathForLogging: string = ConfigurationFileBase._formatPathForLogging( resolvedConfigurationFilePath ); @@ -713,76 +645,6 @@ export class ConfigurationFile { return result as TConfigurationFile; } - private _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 ("${ConfigurationFile._formatPathForLogging(rigProfileFolder)}")` - ); - } - } - } else { - terminal.writeDebugLine( - `No rig found for "${ConfigurationFile._formatPathForLogging(rigConfig.projectFolderPath)}"` - ); - } - - return undefined; - } - - private async _tryLoadConfigurationFileInRigAsync( - 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 ("${ConfigurationFile._formatPathForLogging(rigProfileFolder)}")` - ); - } - } - } else { - terminal.writeDebugLine( - `No rig found for "${ConfigurationFile._formatPathForLogging(rigConfig.projectFolderPath)}"` - ); - } - - return undefined; - } - private _annotateProperties(resolvedConfigurationFilePath: string, obj: TObject): void { if (!obj) { return; @@ -830,7 +692,7 @@ export class ConfigurationFile { this._packageJsonLookup.tryGetPackageFolderFor(configurationFilePath); if (!packageRoot) { throw new Error( - `Could not find a package root for path "${ConfigurationFile._formatPathForLogging( + `Could not find a package root for path "${ConfigurationFileBase._formatPathForLogging( configurationFilePath )}"` ); @@ -1121,8 +983,4 @@ export class ConfigurationFile { return result; } - - private _getConfigurationFilePathForProject(projectPath: string): string { - return nodeJsPath.resolve(projectPath, this.projectRelativeFilePath); - } } diff --git a/libraries/heft-config-file/src/NonProjectConfigurationFile.ts b/libraries/heft-config-file/src/NonProjectConfigurationFile.ts new file mode 100644 index 00000000000..dbd66c89dd6 --- /dev/null +++ b/libraries/heft-config-file/src/NonProjectConfigurationFile.ts @@ -0,0 +1,91 @@ +// 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 type { ITerminal } from '@rushstack/terminal'; + +import { ConfigurationFileBase } from './ConfigurationFileBase'; +import { IRigConfig } from '@rushstack/rig-package'; + +/** + * @beta + */ +export class NonProjectConfigurationFile extends ConfigurationFileBase< + TConfigurationFile, + {} +> { + /** + * Load the configuration file at the specified absolute path, automatically resolving + * `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); + } + + /** + * Load the configuration file at the specified absolute path, automatically resolving + * `extends` properties. Will throw an error if the file cannot be found. + */ + public async loadConfigurationFileAsync( + terminal: ITerminal, + filePath: string + ): Promise { + return await this._loadConfigurationFileInnerWithCacheAsync( + terminal, + filePath, + new Set(), + undefined + ); + } + + /** + * This function is identical to {@link NonProjectConfigurationFile.loadConfigurationFile}, except + * that it returns `undefined` instead of throwing an error if the configuration file cannot be found. + */ + public tryLoadConfigurationFile(terminal: ITerminal, filePath: string): TConfigurationFile | undefined { + try { + return this.loadConfigurationFile(terminal, filePath); + } catch (e) { + if (FileSystem.isNotExistError(e as Error)) { + return undefined; + } + throw e; + } + } + + /** + * This function is identical to {@link NonProjectConfigurationFile.loadConfigurationFileAsync}, except + * that it returns `undefined` instead of throwing an error if the configuration file cannot be found. + */ + public async tryLoadConfigurationFileAsync( + terminal: ITerminal, + filePath: string + ): Promise { + try { + return await this.loadConfigurationFileAsync(terminal, filePath); + } catch (e) { + if (FileSystem.isNotExistError(e as Error)) { + return undefined; + } + 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 new file mode 100644 index 00000000000..65327975daa --- /dev/null +++ b/libraries/heft-config-file/src/ProjectConfigurationFile.ts @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as nodeJsPath from 'path'; +import { FileSystem } from '@rushstack/node-core-library'; +import type { ITerminal } from '@rushstack/terminal'; +import type { IRigConfig } from '@rushstack/rig-package'; + +import { ConfigurationFileBase, IConfigurationFileOptions } from './ConfigurationFileBase'; + +/** + * @beta + */ +export interface IProjectConfigurationFileOptions { + /** + * A project root-relative path to the configuration file that should be loaded. + */ + projectRelativeFilePath: string; +} + +/** + * @beta + */ +export class ProjectConfigurationFile extends ConfigurationFileBase< + TConfigurationFile, + IProjectConfigurationFileOptions +> { + /** {@inheritDoc IProjectConfigurationFileOptions.projectRelativeFilePath} */ + public readonly projectRelativeFilePath: string; + + public constructor( + options: IConfigurationFileOptions + ) { + super(options); + this.projectRelativeFilePath = options.projectRelativeFilePath; + } + + /** + * Find and return a configuration file for the specified project, automatically resolving + * `extends` properties and handling rigged configuration files. Will throw an error if a configuration + * file cannot be found in the rig or project config folder. + */ + public loadConfigurationFileForProject( + terminal: ITerminal, + projectPath: string, + rigConfig?: IRigConfig + ): TConfigurationFile { + const projectConfigurationFilePath: string = this._getConfigurationFilePathForProject(projectPath); + return this._loadConfigurationFileInnerWithCache( + terminal, + projectConfigurationFilePath, + new Set(), + rigConfig + ); + } + + /** + * Find and return a configuration file for the specified project, automatically resolving + * `extends` properties and handling rigged configuration files. Will throw an error if a configuration + * file cannot be found in the rig or project config folder. + */ + public async loadConfigurationFileForProjectAsync( + terminal: ITerminal, + projectPath: string, + rigConfig?: IRigConfig + ): Promise { + const projectConfigurationFilePath: string = this._getConfigurationFilePathForProject(projectPath); + return await this._loadConfigurationFileInnerWithCacheAsync( + terminal, + projectConfigurationFilePath, + new Set(), + rigConfig + ); + } + + /** + * This function is identical to {@link ConfigurationFile.loadConfigurationFileForProject}, except + * that it returns `undefined` instead of throwing an error if the configuration file cannot be found. + */ + public tryLoadConfigurationFileForProject( + terminal: ITerminal, + projectPath: string, + rigConfig?: IRigConfig + ): TConfigurationFile | undefined { + try { + return this.loadConfigurationFileForProject(terminal, projectPath, rigConfig); + } catch (e) { + if (FileSystem.isNotExistError(e as Error)) { + return undefined; + } + throw e; + } + } + + /** + * This function is identical to {@link ConfigurationFile.loadConfigurationFileForProjectAsync}, except + * that it returns `undefined` instead of throwing an error if the configuration file cannot be found. + */ + public async tryLoadConfigurationFileForProjectAsync( + terminal: ITerminal, + projectPath: string, + rigConfig?: IRigConfig + ): Promise { + try { + return await this.loadConfigurationFileForProjectAsync(terminal, projectPath, rigConfig); + } catch (e) { + if (FileSystem.isNotExistError(e as Error)) { + return undefined; + } + throw e; + } + } + + 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; + } + + protected async _tryLoadConfigurationFileInRigAsync( + 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)}")` + ); + } + } + } else { + terminal.writeDebugLine( + `No rig found for "${ConfigurationFileBase._formatPathForLogging(rigConfig.projectFolderPath)}"` + ); + } + + return undefined; + } + + private _getConfigurationFilePathForProject(projectPath: string): string { + return nodeJsPath.resolve(projectPath, this.projectRelativeFilePath); + } +} diff --git a/libraries/heft-config-file/src/index.ts b/libraries/heft-config-file/src/index.ts index 896fc44308a..c809c9e10b3 100644 --- a/libraries/heft-config-file/src/index.ts +++ b/libraries/heft-config-file/src/index.ts @@ -9,7 +9,7 @@ */ export { - ConfigurationFile, + ConfigurationFileBase, type IConfigurationFileOptionsBase, type IConfigurationFileOptionsWithJsonSchemaFilePath, type IConfigurationFileOptionsWithJsonSchemaObject, @@ -27,4 +27,11 @@ export { type IPropertyInheritanceDefaults, PathResolutionMethod, type PropertyInheritanceCustomFunction -} from './ConfigurationFile'; +} from './ConfigurationFileBase'; + +export { + // TODO: REname this export to `ProjectConfigurationFile` in the next major version bump + ProjectConfigurationFile as ConfigurationFile, + type IProjectConfigurationFileOptions +} from './ProjectConfigurationFile'; +export { NonProjectConfigurationFile } from './NonProjectConfigurationFile'; diff --git a/libraries/heft-config-file/src/test/ConfigurationFile.test.ts b/libraries/heft-config-file/src/test/ConfigurationFile.test.ts index 34790bca53b..5e10006b856 100644 --- a/libraries/heft-config-file/src/test/ConfigurationFile.test.ts +++ b/libraries/heft-config-file/src/test/ConfigurationFile.test.ts @@ -6,9 +6,11 @@ import { FileSystem, JsonFile, Path, Text } from '@rushstack/node-core-library'; import { StringBufferTerminalProvider, Terminal } from '@rushstack/terminal'; import { RigConfig } from '@rushstack/rig-package'; -import { ConfigurationFile, PathResolutionMethod, InheritanceType } from '../ConfigurationFile'; +import { ProjectConfigurationFile } from '../ProjectConfigurationFile'; +import { PathResolutionMethod, InheritanceType } from '../ConfigurationFileBase'; +import { NonProjectConfigurationFile } from '../NonProjectConfigurationFile'; -describe(ConfigurationFile.name, () => { +describe(ProjectConfigurationFile.name, () => { const projectRoot: string = nodeJsPath.resolve(__dirname, '..', '..'); let terminalProvider: StringBufferTerminalProvider; let terminal: Terminal; @@ -16,7 +18,7 @@ describe(ConfigurationFile.name, () => { beforeEach(() => { const formatPathForLogging: (path: string) => string = (path: string) => `/${Path.convertToSlashes(nodeJsPath.relative(projectRoot, path))}`; - jest.spyOn(ConfigurationFile, '_formatPathForLogging').mockImplementation(formatPathForLogging); + jest.spyOn(ProjectConfigurationFile, '_formatPathForLogging').mockImplementation(formatPathForLogging); jest.spyOn(JsonFile, '_formatPathForError').mockImplementation(formatPathForLogging); terminalProvider = new StringBufferTerminalProvider(false); @@ -43,8 +45,8 @@ describe(ConfigurationFile.name, () => { } it('Correctly loads the config file', () => { - const configFileLoader: ConfigurationFile = - new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: projectRelativeFilePath, ...partialOptions }); @@ -64,8 +66,8 @@ describe(ConfigurationFile.name, () => { }); it('Correctly loads the config file async', async () => { - const configFileLoader: ConfigurationFile = - new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: projectRelativeFilePath, ...partialOptions }); @@ -83,8 +85,8 @@ describe(ConfigurationFile.name, () => { }); it('Correctly resolves paths relative to the config file', () => { - const configFileLoader: ConfigurationFile = - new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: projectRelativeFilePath, ...partialOptions, jsonPathMetadata: { @@ -110,8 +112,8 @@ describe(ConfigurationFile.name, () => { }); it('Correctly resolves paths relative to the config file async', async () => { - const configFileLoader: ConfigurationFile = - new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: projectRelativeFilePath, ...partialOptions, jsonPathMetadata: { @@ -135,8 +137,8 @@ describe(ConfigurationFile.name, () => { }); it('Correctly resolves paths relative to the project root', () => { - const configFileLoader: ConfigurationFile = - new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: projectRelativeFilePath, ...partialOptions, jsonPathMetadata: { @@ -162,8 +164,8 @@ describe(ConfigurationFile.name, () => { }); it('Correctly resolves paths relative to the project root async', async () => { - const configFileLoader: ConfigurationFile = - new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: projectRelativeFilePath, ...partialOptions, jsonPathMetadata: { @@ -185,6 +187,42 @@ describe(ConfigurationFile.name, () => { configFileLoader.getPropertyOriginalValue({ parentObject: loadedConfigFile, propertyName: 'thing' }) ).toEqual('A'); }); + + it(`The ${NonProjectConfigurationFile.name} version works correctly`, () => { + const configFileLoader: NonProjectConfigurationFile = + new NonProjectConfigurationFile(partialOptions); + const loadedConfigFile: ISimplestConfigFile = configFileLoader.loadConfigurationFile( + terminal, + `${__dirname}/${projectRelativeFilePath}` + ); + const expectedConfigFile: ISimplestConfigFile = { thing: 'A' }; + + expect(JSON.stringify(loadedConfigFile)).toEqual(JSON.stringify(expectedConfigFile)); + expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile)).toEqual( + nodeJsPath.resolve(__dirname, projectRelativeFilePath) + ); + expect( + configFileLoader.getPropertyOriginalValue({ parentObject: loadedConfigFile, propertyName: 'thing' }) + ).toEqual('A'); + }); + + it(`The ${NonProjectConfigurationFile.name} version works correctly async`, async () => { + const configFileLoader: NonProjectConfigurationFile = + new NonProjectConfigurationFile(partialOptions); + const loadedConfigFile: ISimplestConfigFile = await configFileLoader.loadConfigurationFileAsync( + terminal, + `${__dirname}/${projectRelativeFilePath}` + ); + const expectedConfigFile: ISimplestConfigFile = { thing: 'A' }; + + expect(JSON.stringify(loadedConfigFile)).toEqual(JSON.stringify(expectedConfigFile)); + expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile)).toEqual( + nodeJsPath.resolve(__dirname, projectRelativeFilePath) + ); + expect( + configFileLoader.getPropertyOriginalValue({ parentObject: loadedConfigFile, propertyName: 'thing' }) + ).toEqual('A'); + }); } describe('with a JSON schema path', () => { @@ -214,12 +252,11 @@ describe(ConfigurationFile.name, () => { } it('Correctly loads the config file', () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile( - { + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath, jsonSchemaPath: schemaPath - } - ); + }); const loadedConfigFile: ISimpleConfigFile = configFileLoader.loadConfigurationFileForProject( terminal, __dirname @@ -233,12 +270,11 @@ describe(ConfigurationFile.name, () => { }); it('Correctly loads the config file async', async () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile( - { + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath, jsonSchemaPath: schemaPath - } - ); + }); const loadedConfigFile: ISimpleConfigFile = await configFileLoader.loadConfigurationFileForProjectAsync( terminal, __dirname @@ -252,8 +288,8 @@ describe(ConfigurationFile.name, () => { }); it('Correctly resolves paths relative to the config file', () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile( - { + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath, jsonSchemaPath: schemaPath, jsonPathMetadata: { @@ -264,8 +300,7 @@ describe(ConfigurationFile.name, () => { pathResolutionMethod: PathResolutionMethod.resolvePathRelativeToConfigurationFile } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = configFileLoader.loadConfigurationFileForProject( terminal, __dirname @@ -286,8 +321,8 @@ describe(ConfigurationFile.name, () => { }); it('Correctly resolves paths relative to the config file async', async () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile( - { + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath, jsonSchemaPath: schemaPath, jsonPathMetadata: { @@ -298,8 +333,7 @@ describe(ConfigurationFile.name, () => { pathResolutionMethod: PathResolutionMethod.resolvePathRelativeToConfigurationFile } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = await configFileLoader.loadConfigurationFileForProjectAsync( terminal, __dirname @@ -320,8 +354,8 @@ describe(ConfigurationFile.name, () => { }); it('Correctly resolves paths relative to the project root', () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile( - { + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath, jsonSchemaPath: schemaPath, jsonPathMetadata: { @@ -332,8 +366,7 @@ describe(ConfigurationFile.name, () => { pathResolutionMethod: PathResolutionMethod.resolvePathRelativeToProjectRoot } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = configFileLoader.loadConfigurationFileForProject( terminal, __dirname @@ -354,8 +387,8 @@ describe(ConfigurationFile.name, () => { }); it('Correctly resolves paths relative to the project root async', async () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile( - { + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath, jsonSchemaPath: schemaPath, jsonPathMetadata: { @@ -366,8 +399,7 @@ describe(ConfigurationFile.name, () => { pathResolutionMethod: PathResolutionMethod.resolvePathRelativeToProjectRoot } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = await configFileLoader.loadConfigurationFileForProjectAsync( terminal, __dirname @@ -404,46 +436,68 @@ describe(ConfigurationFile.name, () => { } it('Correctly loads the config file with default config meta', () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile( - { + const expectedConfigFile: ISimpleConfigFile = { + things: ['A', 'B', 'C', 'D', 'E'], + thingsObj: { A: { D: 'E' }, F: { G: 'H' } }, + booleanProp: false + }; + + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath, jsonSchemaPath: schemaPath - } - ); + }); const loadedConfigFile: ISimpleConfigFile = configFileLoader.loadConfigurationFileForProject( terminal, __dirname ); + expect(JSON.stringify(loadedConfigFile)).toEqual(JSON.stringify(expectedConfigFile)); + + const nonProjectConfigFileLoader: NonProjectConfigurationFile = + new NonProjectConfigurationFile({ + jsonSchemaPath: schemaPath + }); + const nonProjectLoadedConfigFile: ISimpleConfigFile = nonProjectConfigFileLoader.loadConfigurationFile( + terminal, + `${__dirname}/${projectRelativeFilePath}` + ); + expect(JSON.stringify(nonProjectLoadedConfigFile)).toEqual(JSON.stringify(expectedConfigFile)); + }); + + it('Correctly loads the config file with default config meta async', async () => { const expectedConfigFile: ISimpleConfigFile = { things: ['A', 'B', 'C', 'D', 'E'], thingsObj: { A: { D: 'E' }, F: { G: 'H' } }, booleanProp: false }; - expect(JSON.stringify(loadedConfigFile)).toEqual(JSON.stringify(expectedConfigFile)); - }); - it('Correctly loads the config file with default config meta async', async () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile( - { + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath, jsonSchemaPath: schemaPath - } - ); + }); const loadedConfigFile: ISimpleConfigFile = await configFileLoader.loadConfigurationFileForProjectAsync( terminal, __dirname ); - const expectedConfigFile: ISimpleConfigFile = { - things: ['A', 'B', 'C', 'D', 'E'], - thingsObj: { A: { D: 'E' }, F: { G: 'H' } }, - booleanProp: false - }; + expect(JSON.stringify(loadedConfigFile)).toEqual(JSON.stringify(expectedConfigFile)); + + const nonProjectConfigFileLoader: NonProjectConfigurationFile = + new NonProjectConfigurationFile({ + jsonSchemaPath: schemaPath + }); + const nonProjectLoadedConfigFile: ISimpleConfigFile = + await nonProjectConfigFileLoader.loadConfigurationFileAsync( + terminal, + `${__dirname}/${projectRelativeFilePath}` + ); + expect(JSON.stringify(nonProjectLoadedConfigFile)).toEqual(JSON.stringify(expectedConfigFile)); }); it('Correctly loads the config file with "append" and "merge" in config meta', () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile( - { + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath, jsonSchemaPath: schemaPath, propertyInheritance: { @@ -454,8 +508,7 @@ describe(ConfigurationFile.name, () => { inheritanceType: InheritanceType.merge } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = configFileLoader.loadConfigurationFileForProject( terminal, __dirname @@ -469,8 +522,8 @@ describe(ConfigurationFile.name, () => { }); it('Correctly loads the config file with "append" and "merge" in config meta async', async () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile( - { + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath, jsonSchemaPath: schemaPath, propertyInheritance: { @@ -481,8 +534,7 @@ describe(ConfigurationFile.name, () => { inheritanceType: InheritanceType.merge } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = await configFileLoader.loadConfigurationFileForProjectAsync( terminal, __dirname @@ -496,8 +548,8 @@ describe(ConfigurationFile.name, () => { }); it('Correctly loads the config file with "replace" in config meta', () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile( - { + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath, jsonSchemaPath: schemaPath, propertyInheritance: { @@ -508,8 +560,7 @@ describe(ConfigurationFile.name, () => { inheritanceType: InheritanceType.replace } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = configFileLoader.loadConfigurationFileForProject( terminal, __dirname @@ -523,8 +574,8 @@ describe(ConfigurationFile.name, () => { }); it('Correctly loads the config file with "replace" in config meta async', async () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile( - { + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath, jsonSchemaPath: schemaPath, propertyInheritance: { @@ -535,8 +586,7 @@ describe(ConfigurationFile.name, () => { inheritanceType: InheritanceType.replace } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = await configFileLoader.loadConfigurationFileForProjectAsync( terminal, __dirname @@ -550,16 +600,15 @@ describe(ConfigurationFile.name, () => { }); it('Correctly loads the config file with modified merge behaviors for arrays and objects', () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile( - { + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath, jsonSchemaPath: schemaPath, propertyInheritanceDefaults: { array: { inheritanceType: InheritanceType.replace }, object: { inheritanceType: InheritanceType.merge } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = configFileLoader.loadConfigurationFileForProject( terminal, __dirname @@ -573,16 +622,15 @@ describe(ConfigurationFile.name, () => { }); it('Correctly loads the config file with modified merge behaviors for arrays and objects async', async () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile( - { + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath, jsonSchemaPath: schemaPath, propertyInheritanceDefaults: { array: { inheritanceType: InheritanceType.replace }, object: { inheritanceType: InheritanceType.merge } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = await configFileLoader.loadConfigurationFileForProjectAsync( terminal, __dirname @@ -596,8 +644,8 @@ describe(ConfigurationFile.name, () => { }); it('Correctly loads the config file with "custom" in config meta', () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile( - { + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath, jsonSchemaPath: schemaPath, propertyInheritance: { @@ -617,8 +665,7 @@ describe(ConfigurationFile.name, () => { } } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = configFileLoader.loadConfigurationFileForProject( terminal, __dirname @@ -632,8 +679,8 @@ describe(ConfigurationFile.name, () => { }); it('Correctly loads the config file with "custom" in config meta async', async () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile( - { + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath, jsonSchemaPath: schemaPath, propertyInheritance: { @@ -653,8 +700,7 @@ describe(ConfigurationFile.name, () => { } } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = await configFileLoader.loadConfigurationFileForProjectAsync( terminal, __dirname @@ -668,8 +714,8 @@ describe(ConfigurationFile.name, () => { }); it('Correctly resolves paths relative to the config file', () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile( - { + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath, jsonSchemaPath: schemaPath, jsonPathMetadata: { @@ -680,8 +726,7 @@ describe(ConfigurationFile.name, () => { pathResolutionMethod: PathResolutionMethod.resolvePathRelativeToConfigurationFile } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = configFileLoader.loadConfigurationFileForProject( terminal, __dirname @@ -711,8 +756,8 @@ describe(ConfigurationFile.name, () => { }); it('Correctly resolves paths relative to the config file async', async () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile( - { + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath, jsonSchemaPath: schemaPath, jsonPathMetadata: { @@ -723,8 +768,7 @@ describe(ConfigurationFile.name, () => { pathResolutionMethod: PathResolutionMethod.resolvePathRelativeToConfigurationFile } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = await configFileLoader.loadConfigurationFileForProjectAsync( terminal, __dirname @@ -769,8 +813,8 @@ describe(ConfigurationFile.name, () => { ); const schemaPath: string = nodeJsPath.resolve(__dirname, 'complexConfigFile', 'plugins.schema.json'); - const configFileLoader: ConfigurationFile = - new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: projectRelativeFilePath, jsonSchemaPath: schemaPath, jsonPathMetadata: { @@ -852,8 +896,8 @@ describe(ConfigurationFile.name, () => { ); const schemaPath: string = nodeJsPath.resolve(__dirname, 'complexConfigFile', 'plugins.schema.json'); - const configFileLoader: ConfigurationFile = - new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: projectRelativeFilePath, jsonSchemaPath: schemaPath, jsonPathMetadata: { @@ -933,8 +977,8 @@ describe(ConfigurationFile.name, () => { ); const schemaPath: string = nodeJsPath.resolve(__dirname, 'complexConfigFile', 'plugins.schema.json'); - const configFileLoader: ConfigurationFile = - new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: projectRelativeFilePath, jsonSchemaPath: schemaPath, jsonPathMetadata: { @@ -1016,8 +1060,8 @@ describe(ConfigurationFile.name, () => { ); const schemaPath: string = nodeJsPath.resolve(__dirname, 'complexConfigFile', 'plugins.schema.json'); - const configFileLoader: ConfigurationFile = - new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: projectRelativeFilePath, jsonSchemaPath: schemaPath, jsonPathMetadata: { @@ -1149,8 +1193,8 @@ describe(ConfigurationFile.name, () => { 'inheritanceTypeConfigFile.schema.json' ); - const configFileLoader: ConfigurationFile = - new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: projectRelativeFilePath, jsonSchemaPath: schemaPath }); @@ -1241,8 +1285,8 @@ describe(ConfigurationFile.name, () => { 'inheritanceTypeConfigFile.schema.json' ); - const configFileLoader: ConfigurationFile = - new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: projectRelativeFilePath, jsonSchemaPath: schemaPath }); @@ -1332,8 +1376,8 @@ describe(ConfigurationFile.name, () => { 'simpleInheritanceTypeConfigFile.schema.json' ); - const configFileLoader: ConfigurationFile = - new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: projectRelativeFilePath, jsonSchemaPath: schemaPath }); @@ -1378,7 +1422,7 @@ describe(ConfigurationFile.name, () => { 'simpleInheritanceTypeConfigFile', 'simpleInheritanceTypeConfigFile.schema.json' ); - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: 'simpleInheritanceTypeConfigFile/badInheritanceTypeConfigFileA.json', jsonSchemaPath: schemaPath }); @@ -1394,7 +1438,7 @@ describe(ConfigurationFile.name, () => { 'simpleInheritanceTypeConfigFile', 'simpleInheritanceTypeConfigFile.schema.json' ); - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: 'simpleInheritanceTypeConfigFile/badInheritanceTypeConfigFileA.json', jsonSchemaPath: schemaPath }); @@ -1410,7 +1454,7 @@ describe(ConfigurationFile.name, () => { 'simpleInheritanceTypeConfigFile', 'simpleInheritanceTypeConfigFile.schema.json' ); - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: 'simpleInheritanceTypeConfigFile/badInheritanceTypeConfigFileB.json', jsonSchemaPath: schemaPath }); @@ -1426,7 +1470,7 @@ describe(ConfigurationFile.name, () => { 'simpleInheritanceTypeConfigFile', 'simpleInheritanceTypeConfigFile.schema.json' ); - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: 'simpleInheritanceTypeConfigFile/badInheritanceTypeConfigFileB.json', jsonSchemaPath: schemaPath }); @@ -1442,7 +1486,7 @@ describe(ConfigurationFile.name, () => { 'simpleInheritanceTypeConfigFile', 'simpleInheritanceTypeConfigFile.schema.json' ); - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: 'simpleInheritanceTypeConfigFile/badInheritanceTypeConfigFileC.json', jsonSchemaPath: schemaPath }); @@ -1458,7 +1502,7 @@ describe(ConfigurationFile.name, () => { 'simpleInheritanceTypeConfigFile', 'simpleInheritanceTypeConfigFile.schema.json' ); - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: 'simpleInheritanceTypeConfigFile/badInheritanceTypeConfigFileC.json', jsonSchemaPath: schemaPath }); @@ -1474,7 +1518,7 @@ describe(ConfigurationFile.name, () => { 'simpleInheritanceTypeConfigFile', 'simpleInheritanceTypeConfigFile.schema.json' ); - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: 'simpleInheritanceTypeConfigFile/badInheritanceTypeConfigFileD.json', jsonSchemaPath: schemaPath }); @@ -1490,7 +1534,7 @@ describe(ConfigurationFile.name, () => { 'simpleInheritanceTypeConfigFile', 'simpleInheritanceTypeConfigFile.schema.json' ); - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: 'simpleInheritanceTypeConfigFile/badInheritanceTypeConfigFileD.json', jsonSchemaPath: schemaPath }); @@ -1506,7 +1550,7 @@ describe(ConfigurationFile.name, () => { 'simpleInheritanceTypeConfigFile', 'simpleInheritanceTypeConfigFile.schema.json' ); - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: 'simpleInheritanceTypeConfigFile/badInheritanceTypeConfigFileE.json', jsonSchemaPath: schemaPath }); @@ -1522,7 +1566,7 @@ describe(ConfigurationFile.name, () => { 'simpleInheritanceTypeConfigFile', 'simpleInheritanceTypeConfigFile.schema.json' ); - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: 'simpleInheritanceTypeConfigFile/badInheritanceTypeConfigFileE.json', jsonSchemaPath: schemaPath }); @@ -1549,8 +1593,8 @@ describe(ConfigurationFile.name, () => { it('correctly loads a config file inside a rig', () => { const projectRelativeFilePath: string = 'config/simplestConfigFile.json'; - const configFileLoader: ConfigurationFile = - new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: projectRelativeFilePath, jsonSchemaPath: schemaPath }); @@ -1579,8 +1623,8 @@ describe(ConfigurationFile.name, () => { it('correctly loads a config file inside a rig async', async () => { const projectRelativeFilePath: string = 'config/simplestConfigFile.json'; - const configFileLoader: ConfigurationFile = - new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: projectRelativeFilePath, jsonSchemaPath: schemaPath }); @@ -1606,8 +1650,8 @@ describe(ConfigurationFile.name, () => { it('correctly loads a config file inside a rig via tryLoadConfigurationFileForProject', () => { const projectRelativeFilePath: string = 'config/simplestConfigFile.json'; - const configFileLoader: ConfigurationFile = - new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: projectRelativeFilePath, jsonSchemaPath: schemaPath }); @@ -1634,8 +1678,8 @@ describe(ConfigurationFile.name, () => { it('correctly loads a config file inside a rig via tryLoadConfigurationFileForProjectAsync', async () => { const projectRelativeFilePath: string = 'config/simplestConfigFile.json'; - const configFileLoader: ConfigurationFile = - new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: projectRelativeFilePath, jsonSchemaPath: schemaPath }); @@ -1661,7 +1705,7 @@ describe(ConfigurationFile.name, () => { }); it("throws an error when a config file doesn't exist in a project referencing a rig, which also doesn't have the file", () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: 'config/notExist.json', jsonSchemaPath: schemaPath }); @@ -1672,7 +1716,7 @@ describe(ConfigurationFile.name, () => { }); it("throws an error when a config file doesn't exist in a project referencing a rig, which also doesn't have the file async", async () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: 'config/notExist.json', jsonSchemaPath: schemaPath }); @@ -1688,7 +1732,7 @@ describe(ConfigurationFile.name, () => { it("throws an error when the file doesn't exist", () => { const errorCaseFolderName: string = 'invalidType'; - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: `${errorCasesFolderName}/${errorCaseFolderName}/notExist.json`, jsonSchemaPath: nodeJsPath.resolve( __dirname, @@ -1705,7 +1749,7 @@ describe(ConfigurationFile.name, () => { it("throws an error when the file doesn't exist async", async () => { const errorCaseFolderName: string = 'invalidType'; - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: `${errorCasesFolderName}/${errorCaseFolderName}/notExist.json`, jsonSchemaPath: nodeJsPath.resolve( __dirname, @@ -1722,7 +1766,7 @@ describe(ConfigurationFile.name, () => { it("returns undefined when the file doesn't exist for tryLoadConfigurationFileForProject", () => { const errorCaseFolderName: string = 'invalidType'; - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: `${errorCasesFolderName}/${errorCaseFolderName}/notExist.json`, jsonSchemaPath: nodeJsPath.resolve( __dirname, @@ -1737,7 +1781,7 @@ describe(ConfigurationFile.name, () => { it("returns undefined when the file doesn't exist for tryLoadConfigurationFileForProjectAsync", async () => { const errorCaseFolderName: string = 'invalidType'; - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: `${errorCasesFolderName}/${errorCaseFolderName}/notExist.json`, jsonSchemaPath: nodeJsPath.resolve( __dirname, @@ -1766,7 +1810,7 @@ describe(ConfigurationFile.name, () => { : Promise.reject(new Error('File not found')) ); - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: configFilePath, jsonSchemaPath: nodeJsPath.resolve( __dirname, @@ -1802,7 +1846,7 @@ describe(ConfigurationFile.name, () => { : Promise.reject(new Error('File not found')) ); - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: configFilePath, jsonSchemaPath: nodeJsPath.resolve( __dirname, @@ -1823,7 +1867,7 @@ describe(ConfigurationFile.name, () => { it("Throws an error for a file that doesn't match its schema", () => { const errorCaseFolderName: string = 'invalidType'; - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: `${errorCasesFolderName}/${errorCaseFolderName}/config.json`, jsonSchemaPath: nodeJsPath.resolve( __dirname, @@ -1840,7 +1884,7 @@ describe(ConfigurationFile.name, () => { it("Throws an error for a file that doesn't match its schema async", async () => { const errorCaseFolderName: string = 'invalidType'; - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: `${errorCasesFolderName}/${errorCaseFolderName}/config.json`, jsonSchemaPath: nodeJsPath.resolve( __dirname, @@ -1857,7 +1901,7 @@ describe(ConfigurationFile.name, () => { it('Throws an error when there is a circular reference in "extends" properties', () => { const errorCaseFolderName: string = 'circularReference'; - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: `${errorCasesFolderName}/${errorCaseFolderName}/config1.json`, jsonSchemaPath: nodeJsPath.resolve( __dirname, @@ -1874,7 +1918,7 @@ describe(ConfigurationFile.name, () => { it('Throws an error when there is a circular reference in "extends" properties async', async () => { const errorCaseFolderName: string = 'circularReference'; - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: `${errorCasesFolderName}/${errorCaseFolderName}/config1.json`, jsonSchemaPath: nodeJsPath.resolve( __dirname, @@ -1891,7 +1935,7 @@ describe(ConfigurationFile.name, () => { it('Throws an error when an "extends" property points to a file that cannot be resolved', () => { const errorCaseFolderName: string = 'extendsNotExist'; - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: `${errorCasesFolderName}/${errorCaseFolderName}/config.json`, jsonSchemaPath: nodeJsPath.resolve( __dirname, @@ -1908,7 +1952,7 @@ describe(ConfigurationFile.name, () => { it('Throws an error when an "extends" property points to a file that cannot be resolved async', async () => { const errorCaseFolderName: string = 'extendsNotExist'; - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: `${errorCasesFolderName}/${errorCaseFolderName}/config.json`, jsonSchemaPath: nodeJsPath.resolve( __dirname, @@ -1925,7 +1969,7 @@ describe(ConfigurationFile.name, () => { it("Throws an error when a combined config file doesn't match the schema", () => { const errorCaseFolderName: string = 'invalidCombinedFile'; - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: `${errorCasesFolderName}/${errorCaseFolderName}/config1.json`, jsonSchemaPath: nodeJsPath.resolve( __dirname, @@ -1942,7 +1986,7 @@ describe(ConfigurationFile.name, () => { it("Throws an error when a combined config file doesn't match the schema async", async () => { const errorCaseFolderName: string = 'invalidCombinedFile'; - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: `${errorCasesFolderName}/${errorCaseFolderName}/config1.json`, jsonSchemaPath: nodeJsPath.resolve( __dirname, @@ -1958,7 +2002,7 @@ describe(ConfigurationFile.name, () => { }); it("Throws an error when a requested file doesn't exist", () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: `${errorCasesFolderName}/folderThatDoesntExist/config.json`, jsonSchemaPath: nodeJsPath.resolve( __dirname, @@ -1974,7 +2018,7 @@ describe(ConfigurationFile.name, () => { }); it("Throws an error when a requested file doesn't exist async", async () => { - const configFileLoader: ConfigurationFile = new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = new ProjectConfigurationFile({ projectRelativeFilePath: `${errorCasesFolderName}/folderThatDoesntExist/config.json`, jsonSchemaPath: nodeJsPath.resolve( __dirname, From dc42344289dffc10f12daa56b65fd579127ba298 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 2 Dec 2024 18:55:34 -0500 Subject: [PATCH 02/12] fixup! Create a non-project version of ConfigurationFile. --- .../src/NonProjectConfigurationFile.ts | 2 +- .../src/ProjectConfigurationFile.ts | 2 +- .../src/test/ConfigurationFile.test.ts | 8 ++-- .../ConfigurationFile.test.ts.snap | 40 +++++++++++++++++++ 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/libraries/heft-config-file/src/NonProjectConfigurationFile.ts b/libraries/heft-config-file/src/NonProjectConfigurationFile.ts index dbd66c89dd6..d3a7bd23dcd 100644 --- a/libraries/heft-config-file/src/NonProjectConfigurationFile.ts +++ b/libraries/heft-config-file/src/NonProjectConfigurationFile.ts @@ -3,9 +3,9 @@ import { FileSystem } from '@rushstack/node-core-library'; import type { ITerminal } from '@rushstack/terminal'; +import type { IRigConfig } from '@rushstack/rig-package'; import { ConfigurationFileBase } from './ConfigurationFileBase'; -import { IRigConfig } from '@rushstack/rig-package'; /** * @beta diff --git a/libraries/heft-config-file/src/ProjectConfigurationFile.ts b/libraries/heft-config-file/src/ProjectConfigurationFile.ts index 65327975daa..ac4788df0e5 100644 --- a/libraries/heft-config-file/src/ProjectConfigurationFile.ts +++ b/libraries/heft-config-file/src/ProjectConfigurationFile.ts @@ -6,7 +6,7 @@ import { FileSystem } from '@rushstack/node-core-library'; import type { ITerminal } from '@rushstack/terminal'; import type { IRigConfig } from '@rushstack/rig-package'; -import { ConfigurationFileBase, IConfigurationFileOptions } from './ConfigurationFileBase'; +import { ConfigurationFileBase, type IConfigurationFileOptions } from './ConfigurationFileBase'; /** * @beta diff --git a/libraries/heft-config-file/src/test/ConfigurationFile.test.ts b/libraries/heft-config-file/src/test/ConfigurationFile.test.ts index 5e10006b856..88d11c3a03a 100644 --- a/libraries/heft-config-file/src/test/ConfigurationFile.test.ts +++ b/libraries/heft-config-file/src/test/ConfigurationFile.test.ts @@ -1,16 +1,18 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +/* eslint-disable max-lines */ + import * as nodeJsPath from 'path'; import { FileSystem, JsonFile, Path, Text } from '@rushstack/node-core-library'; import { StringBufferTerminalProvider, Terminal } from '@rushstack/terminal'; import { RigConfig } from '@rushstack/rig-package'; import { ProjectConfigurationFile } from '../ProjectConfigurationFile'; -import { PathResolutionMethod, InheritanceType } from '../ConfigurationFileBase'; +import { PathResolutionMethod, InheritanceType, ConfigurationFileBase } from '../ConfigurationFileBase'; import { NonProjectConfigurationFile } from '../NonProjectConfigurationFile'; -describe(ProjectConfigurationFile.name, () => { +describe('ConfigurationFile', () => { const projectRoot: string = nodeJsPath.resolve(__dirname, '..', '..'); let terminalProvider: StringBufferTerminalProvider; let terminal: Terminal; @@ -18,7 +20,7 @@ describe(ProjectConfigurationFile.name, () => { beforeEach(() => { const formatPathForLogging: (path: string) => string = (path: string) => `/${Path.convertToSlashes(nodeJsPath.relative(projectRoot, path))}`; - jest.spyOn(ProjectConfigurationFile, '_formatPathForLogging').mockImplementation(formatPathForLogging); + jest.spyOn(ConfigurationFileBase, '_formatPathForLogging').mockImplementation(formatPathForLogging); jest.spyOn(JsonFile, '_formatPathForError').mockImplementation(formatPathForLogging); terminalProvider = new StringBufferTerminalProvider(false); 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 a6028298487..2dc578c2fa2 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 @@ -280,6 +280,26 @@ Object { } `; +exports[`ConfigurationFile A simple config file with a JSON schema object The NonProjectConfigurationFile version works correctly 1`] = ` +Object { + "debug": "", + "error": "", + "log": "", + "verbose": "", + "warning": "", +} +`; + +exports[`ConfigurationFile A simple config file with a JSON schema object The NonProjectConfigurationFile version works correctly async 1`] = ` +Object { + "debug": "", + "error": "", + "log": "", + "verbose": "", + "warning": "", +} +`; + exports[`ConfigurationFile A simple config file with a JSON schema path Correctly loads the config file 1`] = ` Object { "debug": "", @@ -340,6 +360,26 @@ Object { } `; +exports[`ConfigurationFile A simple config file with a JSON schema path The NonProjectConfigurationFile version works correctly 1`] = ` +Object { + "debug": "", + "error": "", + "log": "", + "verbose": "", + "warning": "", +} +`; + +exports[`ConfigurationFile A simple config file with a JSON schema path The NonProjectConfigurationFile version works correctly async 1`] = ` +Object { + "debug": "", + "error": "", + "log": "", + "verbose": "", + "warning": "", +} +`; + exports[`ConfigurationFile a complex file with inheritance type annotations Correctly loads a complex config file with a single inheritance type annotation 1`] = ` Object { "debug": "", From c7033453fbb4cb83b999adc3a93192abacd84197 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 2 Dec 2024 18:55:41 -0500 Subject: [PATCH 03/12] fixup! Create a non-project version of ConfigurationFile. --- common/reviews/api/heft-config-file.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/reviews/api/heft-config-file.api.md b/common/reviews/api/heft-config-file.api.md index ad66b7854ec..99554d13a61 100644 --- a/common/reviews/api/heft-config-file.api.md +++ b/common/reviews/api/heft-config-file.api.md @@ -4,7 +4,7 @@ ```ts -import { IRigConfig } from '@rushstack/rig-package'; +import type { IRigConfig } from '@rushstack/rig-package'; import type { ITerminal } from '@rushstack/terminal'; // @beta (undocumented) From af91b04acf0e9971345cde31fffa2d10dac65586 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 2 Dec 2024 18:57:35 -0500 Subject: [PATCH 04/12] fixup! Create a non-project version of ConfigurationFile. --- ...port-pnpm-config.json-extends_2024-12-02-23-57.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@rushstack/heft-config-file/support-pnpm-config.json-extends_2024-12-02-23-57.json diff --git a/common/changes/@rushstack/heft-config-file/support-pnpm-config.json-extends_2024-12-02-23-57.json b/common/changes/@rushstack/heft-config-file/support-pnpm-config.json-extends_2024-12-02-23-57.json new file mode 100644 index 00000000000..db24bfc3ed7 --- /dev/null +++ b/common/changes/@rushstack/heft-config-file/support-pnpm-config.json-extends_2024-12-02-23-57.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-config-file", + "comment": "Add a new `NonProjectConfigurationFile` class that is designed to load absolute-pathed configuration files without rig support.", + "type": "minor" + } + ], + "packageName": "@rushstack/heft-config-file" +} \ No newline at end of file From 1e28904c7fff563ca1a9700101e13fac97441aad Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 2 Dec 2024 18:58:35 -0500 Subject: [PATCH 05/12] Add support for extends in pnpm-config.json --- ...pnpm-config.json-extends_2024-12-02-23-58.json | 10 ++++++++++ .../src/logic/pnpm/PnpmOptionsConfiguration.ts | 15 ++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 common/changes/@microsoft/rush/support-pnpm-config.json-extends_2024-12-02-23-58.json diff --git a/common/changes/@microsoft/rush/support-pnpm-config.json-extends_2024-12-02-23-58.json b/common/changes/@microsoft/rush/support-pnpm-config.json-extends_2024-12-02-23-58.json new file mode 100644 index 00000000000..239d4e1f8e2 --- /dev/null +++ b/common/changes/@microsoft/rush/support-pnpm-config.json-extends_2024-12-02-23-58.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Add support for an `\"extends\"` property in the `common/config/rush/pnpm-config.json` and `common/config/subspace/*/pnpm-config.json` files.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index adec7035a54..123b582c4a8 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -2,6 +2,8 @@ // See LICENSE in the project root for license information. import { JsonFile, type JsonObject, JsonSchema } from '@rushstack/node-core-library'; +import { NonProjectConfigurationFile } from '@rushstack/heft-config-file'; +import { ConsoleTerminalProvider, ITerminal, Terminal } from '@rushstack/terminal'; import { type IPackageManagerOptionsJsonBase, @@ -434,9 +436,16 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration jsonFilename: string, commonTempFolder: string ): PnpmOptionsConfiguration { - const pnpmOptionJson: IPnpmOptionsJson = JsonFile.loadAndValidate( - jsonFilename, - PnpmOptionsConfiguration._jsonSchema + // TODO: plumb through the terminal + const terminal: ITerminal = new Terminal(new ConsoleTerminalProvider()); + + const pnpmOptionsConfigFile: NonProjectConfigurationFile = + new NonProjectConfigurationFile({ + jsonSchemaObject: PnpmOptionsConfiguration._jsonSchema + }); + const pnpmOptionJson: IPnpmOptionsJson = pnpmOptionsConfigFile.loadConfigurationFile( + terminal, + jsonFilename ); return new PnpmOptionsConfiguration(pnpmOptionJson || {}, commonTempFolder, jsonFilename); } From 361aeba1e6d2b8214668c7a2d7039d9cc5821d49 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 2 Dec 2024 22:16:48 -0500 Subject: [PATCH 06/12] fixup! Add support for extends in pnpm-config.json --- libraries/rush-lib/src/schemas/pnpm-config.schema.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libraries/rush-lib/src/schemas/pnpm-config.schema.json b/libraries/rush-lib/src/schemas/pnpm-config.schema.json index 7347662e472..6ab70eb2667 100644 --- a/libraries/rush-lib/src/schemas/pnpm-config.schema.json +++ b/libraries/rush-lib/src/schemas/pnpm-config.schema.json @@ -10,6 +10,11 @@ "type": "string" }, + "extends": { + "description": "Optionally specifies another JSON config file that this file extends from. This provides a way for standard settings to be shared across multiple projects.", + "type": "string" + }, + "useWorkspaces": { "description": "If true, then `rush install` and `rush update` will use the PNPM workspaces feature to perform the install, instead of the old model where Rush generated the symlinks for each projects's node_modules folder. This option is strongly recommended. The default value is false.", "type": "boolean" From 463e38a963e877c4451f6b0a595caea72788e946 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 2 Dec 2024 22:23:16 -0500 Subject: [PATCH 07/12] fixup! Add support for extends in pnpm-config.json --- libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index 123b582c4a8..9082322d60f 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -3,7 +3,7 @@ import { JsonFile, type JsonObject, JsonSchema } from '@rushstack/node-core-library'; import { NonProjectConfigurationFile } from '@rushstack/heft-config-file'; -import { ConsoleTerminalProvider, ITerminal, Terminal } from '@rushstack/terminal'; +import { ConsoleTerminalProvider, Terminal } from '@rushstack/terminal'; import { type IPackageManagerOptionsJsonBase, @@ -437,7 +437,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration commonTempFolder: string ): PnpmOptionsConfiguration { // TODO: plumb through the terminal - const terminal: ITerminal = new Terminal(new ConsoleTerminalProvider()); + const terminal: Terminal = new Terminal(new ConsoleTerminalProvider()); const pnpmOptionsConfigFile: NonProjectConfigurationFile = new NonProjectConfigurationFile({ From 5b6652c8af60c02827c714ba79abd5dd3c13b16f Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 2 Dec 2024 22:28:08 -0500 Subject: [PATCH 08/12] Rename ConfigurationFile to ProjectConfigurationFile. --- apps/heft/src/utilities/CoreConfigFiles.ts | 21 ++++++------ ...-config.json-extends_2024-12-03-03-27.json | 10 ++++++ ...-config.json-extends_2024-12-03-03-27.json | 10 ++++++ ...-config.json-extends_2024-12-03-03-27.json | 10 ++++++ ...-config.json-extends_2024-12-03-03-27.json | 10 ++++++ ...-config.json-extends_2024-12-03-03-27.json | 10 ++++++ ...-config.json-extends_2024-12-03-03-27.json | 10 ++++++ common/reviews/api/heft-config-file.api.md | 32 +++++++++++-------- .../src/ApiExtractorPlugin.ts | 13 ++++---- .../heft-jest-plugin/src/JestPlugin.ts | 8 ++--- .../src/test/JestPlugin.test.ts | 6 ++-- .../heft-sass-plugin/src/SassPlugin.ts | 6 ++-- .../src/TypeScriptPlugin.ts | 10 +++--- .../src/ProjectConfigurationFile.ts | 4 +-- libraries/heft-config-file/src/index.ts | 21 +++++++++--- .../src/api/RushProjectConfiguration.ts | 10 +++--- .../src/RushProjectServeConfigFile.ts | 6 ++-- 17 files changed, 138 insertions(+), 59 deletions(-) create mode 100644 common/changes/@rushstack/heft-api-extractor-plugin/support-pnpm-config.json-extends_2024-12-03-03-27.json create mode 100644 common/changes/@rushstack/heft-config-file/support-pnpm-config.json-extends_2024-12-03-03-27.json create mode 100644 common/changes/@rushstack/heft-jest-plugin/support-pnpm-config.json-extends_2024-12-03-03-27.json create mode 100644 common/changes/@rushstack/heft-sass-plugin/support-pnpm-config.json-extends_2024-12-03-03-27.json create mode 100644 common/changes/@rushstack/heft-typescript-plugin/support-pnpm-config.json-extends_2024-12-03-03-27.json create mode 100644 common/changes/@rushstack/heft/support-pnpm-config.json-extends_2024-12-03-03-27.json diff --git a/apps/heft/src/utilities/CoreConfigFiles.ts b/apps/heft/src/utilities/CoreConfigFiles.ts index a593353bfce..8ca28874cae 100644 --- a/apps/heft/src/utilities/CoreConfigFiles.ts +++ b/apps/heft/src/utilities/CoreConfigFiles.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { - ConfigurationFile, + ProjectConfigurationFile, InheritanceType, PathResolutionMethod, type IJsonPathMetadataResolverOptions @@ -59,9 +59,9 @@ export interface IHeftConfigurationJson { } export class CoreConfigFiles { - private static _heftConfigFileLoader: ConfigurationFile | undefined; + private static _heftConfigFileLoader: ProjectConfigurationFile | undefined; private static _nodeServiceConfigurationLoader: - | ConfigurationFile + | ProjectConfigurationFile | undefined; public static heftConfigurationProjectRelativeFilePath: string = `${Constants.projectConfigFolderName}/${Constants.heftConfigurationFilename}`; @@ -110,7 +110,7 @@ export class CoreConfigFiles { }; const schemaObject: object = await import('../schemas/heft.schema.json'); - CoreConfigFiles._heftConfigFileLoader = new ConfigurationFile({ + CoreConfigFiles._heftConfigFileLoader = new ProjectConfigurationFile({ projectRelativeFilePath: CoreConfigFiles.heftConfigurationProjectRelativeFilePath, jsonSchemaObject: schemaObject, propertyInheritanceDefaults: { @@ -134,7 +134,7 @@ export class CoreConfigFiles { }); } - const heftConfigFileLoader: ConfigurationFile = + const heftConfigFileLoader: ProjectConfigurationFile = CoreConfigFiles._heftConfigFileLoader; let configurationFile: IHeftConfigurationJson; @@ -158,10 +158,11 @@ export class CoreConfigFiles { // want to see if it parses. We will use the ConfigurationFile class to load it to ensure // that we follow the "extends" chain for the entire config file. const legacySchemaObject: object = await import('../schemas/heft-legacy.schema.json'); - const legacyConfigFileLoader: ConfigurationFile = new ConfigurationFile({ - projectRelativeFilePath: CoreConfigFiles.heftConfigurationProjectRelativeFilePath, - jsonSchemaObject: legacySchemaObject - }); + const legacyConfigFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ + projectRelativeFilePath: CoreConfigFiles.heftConfigurationProjectRelativeFilePath, + jsonSchemaObject: legacySchemaObject + }); await legacyConfigFileLoader.loadConfigurationFileForProjectAsync(terminal, projectPath, rigConfig); } catch (e2) { // It doesn't match the legacy schema either. Throw the original error. @@ -232,7 +233,7 @@ export class CoreConfigFiles { if (!CoreConfigFiles._nodeServiceConfigurationLoader) { const schemaObject: object = await import('../schemas/node-service.schema.json'); CoreConfigFiles._nodeServiceConfigurationLoader = - new ConfigurationFile({ + new ProjectConfigurationFile({ projectRelativeFilePath: CoreConfigFiles.nodeServiceConfigurationProjectRelativeFilePath, jsonSchemaObject: schemaObject }); diff --git a/common/changes/@rushstack/heft-api-extractor-plugin/support-pnpm-config.json-extends_2024-12-03-03-27.json b/common/changes/@rushstack/heft-api-extractor-plugin/support-pnpm-config.json-extends_2024-12-03-03-27.json new file mode 100644 index 00000000000..95128aed453 --- /dev/null +++ b/common/changes/@rushstack/heft-api-extractor-plugin/support-pnpm-config.json-extends_2024-12-03-03-27.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-api-extractor-plugin", + "comment": "", + "type": "none" + } + ], + "packageName": "@rushstack/heft-api-extractor-plugin" +} \ No newline at end of file diff --git a/common/changes/@rushstack/heft-config-file/support-pnpm-config.json-extends_2024-12-03-03-27.json b/common/changes/@rushstack/heft-config-file/support-pnpm-config.json-extends_2024-12-03-03-27.json new file mode 100644 index 00000000000..1ec737f7902 --- /dev/null +++ b/common/changes/@rushstack/heft-config-file/support-pnpm-config.json-extends_2024-12-03-03-27.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-config-file", + "comment": "Rename `ConfigurationFile` to `ProjectConfigurationFile` and mark `ConfigurationFile` as `@deprecated`.", + "type": "minor" + } + ], + "packageName": "@rushstack/heft-config-file" +} \ No newline at end of file diff --git a/common/changes/@rushstack/heft-jest-plugin/support-pnpm-config.json-extends_2024-12-03-03-27.json b/common/changes/@rushstack/heft-jest-plugin/support-pnpm-config.json-extends_2024-12-03-03-27.json new file mode 100644 index 00000000000..8453696f39b --- /dev/null +++ b/common/changes/@rushstack/heft-jest-plugin/support-pnpm-config.json-extends_2024-12-03-03-27.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-jest-plugin", + "comment": "", + "type": "none" + } + ], + "packageName": "@rushstack/heft-jest-plugin" +} \ No newline at end of file diff --git a/common/changes/@rushstack/heft-sass-plugin/support-pnpm-config.json-extends_2024-12-03-03-27.json b/common/changes/@rushstack/heft-sass-plugin/support-pnpm-config.json-extends_2024-12-03-03-27.json new file mode 100644 index 00000000000..501a07dfec7 --- /dev/null +++ b/common/changes/@rushstack/heft-sass-plugin/support-pnpm-config.json-extends_2024-12-03-03-27.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-sass-plugin", + "comment": "", + "type": "none" + } + ], + "packageName": "@rushstack/heft-sass-plugin" +} \ No newline at end of file diff --git a/common/changes/@rushstack/heft-typescript-plugin/support-pnpm-config.json-extends_2024-12-03-03-27.json b/common/changes/@rushstack/heft-typescript-plugin/support-pnpm-config.json-extends_2024-12-03-03-27.json new file mode 100644 index 00000000000..cb6f98ca14f --- /dev/null +++ b/common/changes/@rushstack/heft-typescript-plugin/support-pnpm-config.json-extends_2024-12-03-03-27.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-typescript-plugin", + "comment": "", + "type": "none" + } + ], + "packageName": "@rushstack/heft-typescript-plugin" +} \ No newline at end of file diff --git a/common/changes/@rushstack/heft/support-pnpm-config.json-extends_2024-12-03-03-27.json b/common/changes/@rushstack/heft/support-pnpm-config.json-extends_2024-12-03-03-27.json new file mode 100644 index 00000000000..4da3f257a2d --- /dev/null +++ b/common/changes/@rushstack/heft/support-pnpm-config.json-extends_2024-12-03-03-27.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft", + "comment": "", + "type": "none" + } + ], + "packageName": "@rushstack/heft" +} \ No newline at end of file diff --git a/common/reviews/api/heft-config-file.api.md b/common/reviews/api/heft-config-file.api.md index 99554d13a61..eaa9ad5b28b 100644 --- a/common/reviews/api/heft-config-file.api.md +++ b/common/reviews/api/heft-config-file.api.md @@ -7,19 +7,11 @@ import type { IRigConfig } from '@rushstack/rig-package'; import type { ITerminal } from '@rushstack/terminal'; -// @beta (undocumented) -export class ConfigurationFile extends ConfigurationFileBase { - constructor(options: IConfigurationFileOptions); - 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 @deprecated (undocumented) +export const ConfigurationFile: typeof ProjectConfigurationFile; + +// @beta @deprecated (undocumented) +export type ConfigurationFile = ProjectConfigurationFile; // @beta (undocumented) export abstract class ConfigurationFileBase { @@ -155,6 +147,20 @@ export enum PathResolutionMethod { resolvePathRelativeToProjectRoot = "resolvePathRelativeToProjectRoot" } +// @beta (undocumented) +export class ProjectConfigurationFile extends ConfigurationFileBase { + constructor(options: IConfigurationFileOptions); + 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) export type PropertyInheritanceCustomFunction = (currentObject: TObject, parentObject: TObject) => TObject; diff --git a/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts b/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts index 18f6b2a2485..58bcb4e731f 100644 --- a/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts +++ b/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts @@ -9,7 +9,7 @@ import type { HeftConfiguration, IHeftTaskRunIncrementalHookOptions } from '@rushstack/heft'; -import { ConfigurationFile } from '@rushstack/heft-config-file'; +import { ProjectConfigurationFile } from '@rushstack/heft-config-file'; import { ApiExtractorRunner } from './ApiExtractorRunner'; import apiExtractorConfigSchema from './schemas/api-extractor-task.schema.json'; @@ -51,7 +51,7 @@ export default class ApiExtractorPlugin implements IHeftTaskPlugin { private _apiExtractor: typeof TApiExtractor | undefined; private _apiExtractorConfigurationFilePath: string | undefined | typeof UNINITIALIZED = UNINITIALIZED; private _apiExtractorTaskConfigurationFileLoader: - | ConfigurationFile + | ProjectConfigurationFile | undefined; private _printedWatchWarning: boolean = false; @@ -156,10 +156,11 @@ export default class ApiExtractorPlugin implements IHeftTaskPlugin { heftConfiguration: HeftConfiguration ): Promise { if (!this._apiExtractorTaskConfigurationFileLoader) { - this._apiExtractorTaskConfigurationFileLoader = new ConfigurationFile({ - projectRelativeFilePath: TASK_CONFIG_RELATIVE_PATH, - jsonSchemaObject: apiExtractorConfigSchema - }); + this._apiExtractorTaskConfigurationFileLoader = + new ProjectConfigurationFile({ + projectRelativeFilePath: TASK_CONFIG_RELATIVE_PATH, + jsonSchemaObject: apiExtractorConfigSchema + }); } return await this._apiExtractorTaskConfigurationFileLoader.tryLoadConfigurationFileForProjectAsync( diff --git a/heft-plugins/heft-jest-plugin/src/JestPlugin.ts b/heft-plugins/heft-jest-plugin/src/JestPlugin.ts index e8f7274b773..061d78e21e4 100644 --- a/heft-plugins/heft-jest-plugin/src/JestPlugin.ts +++ b/heft-plugins/heft-jest-plugin/src/JestPlugin.ts @@ -24,7 +24,7 @@ import type { CommandLineStringListParameter } from '@rushstack/heft'; import { - ConfigurationFile, + ProjectConfigurationFile, type ICustomJsonPathMetadata, type IJsonPathMetadataResolverOptions, InheritanceType, @@ -139,7 +139,7 @@ interface IPendingTestRun { * @internal */ export default class JestPlugin implements IHeftTaskPlugin { - private static _jestConfigurationFileLoader: ConfigurationFile | undefined; + private static _jestConfigurationFileLoader: ProjectConfigurationFile | undefined; private _jestPromise: Promise | undefined; private _pendingTestRuns: Set = new Set(); @@ -677,7 +677,7 @@ export default class JestPlugin implements IHeftTaskPlugin { public static _getJestConfigurationLoader( buildFolder: string, projectRelativeFilePath: string - ): ConfigurationFile { + ): ProjectConfigurationFile { if (!JestPlugin._jestConfigurationFileLoader) { // By default, ConfigurationFile will replace all objects, so we need to provide merge functions for these const shallowObjectInheritanceFunc: | undefined>( @@ -722,7 +722,7 @@ export default class JestPlugin implements IHeftTaskPlugin { resolveAsModule: true }); - JestPlugin._jestConfigurationFileLoader = new ConfigurationFile({ + JestPlugin._jestConfigurationFileLoader = new ProjectConfigurationFile({ projectRelativeFilePath: projectRelativeFilePath, // Bypass Jest configuration validation jsonSchemaObject: anythingSchema, diff --git a/heft-plugins/heft-jest-plugin/src/test/JestPlugin.test.ts b/heft-plugins/heft-jest-plugin/src/test/JestPlugin.test.ts index 33630934214..894dd742b2a 100644 --- a/heft-plugins/heft-jest-plugin/src/test/JestPlugin.test.ts +++ b/heft-plugins/heft-jest-plugin/src/test/JestPlugin.test.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import type { Config } from '@jest/types'; import type { IHeftTaskSession, HeftConfiguration, CommandLineParameter } from '@rushstack/heft'; -import type { ConfigurationFile } from '@rushstack/heft-config-file'; +import type { ProjectConfigurationFile } from '@rushstack/heft-config-file'; import { Import, JsonFile } from '@rushstack/node-core-library'; import { StringBufferTerminalProvider, Terminal } from '@rushstack/terminal'; @@ -74,7 +74,7 @@ describe('JestConfigLoader', () => { // Because we require the built modules, we need to set our rootDir to be in the 'lib' folder, since transpilation // means that we don't run on the built test assets directly const rootDir: string = path.resolve(__dirname, '..', '..', 'lib', 'test', 'project1'); - const loader: ConfigurationFile = JestPlugin._getJestConfigurationLoader( + const loader: ProjectConfigurationFile = JestPlugin._getJestConfigurationLoader( rootDir, 'config/jest.config.json' ); @@ -161,7 +161,7 @@ describe('JestConfigLoader', () => { // Because we require the built modules, we need to set our rootDir to be in the 'lib' folder, since transpilation // means that we don't run on the built test assets directly const rootDir: string = path.resolve(__dirname, '..', '..', 'lib', 'test', 'project2'); - const loader: ConfigurationFile = JestPlugin._getJestConfigurationLoader( + const loader: ProjectConfigurationFile = JestPlugin._getJestConfigurationLoader( rootDir, 'config/jest.config.json' ); diff --git a/heft-plugins/heft-sass-plugin/src/SassPlugin.ts b/heft-plugins/heft-sass-plugin/src/SassPlugin.ts index b819d276a1f..82018975f1d 100644 --- a/heft-plugins/heft-sass-plugin/src/SassPlugin.ts +++ b/heft-plugins/heft-sass-plugin/src/SassPlugin.ts @@ -10,7 +10,7 @@ import type { IHeftTaskRunIncrementalHookOptions, IWatchedFileState } from '@rushstack/heft'; -import { ConfigurationFile } from '@rushstack/heft-config-file'; +import { ProjectConfigurationFile } from '@rushstack/heft-config-file'; import { type ISassConfiguration, SassProcessor } from './SassProcessor'; import sassConfigSchema from './schemas/heft-sass-plugin.schema.json'; @@ -21,7 +21,7 @@ const PLUGIN_NAME: 'sass-plugin' = 'sass-plugin'; const SASS_CONFIGURATION_LOCATION: string = 'config/sass.json'; export default class SassPlugin implements IHeftPlugin { - private static _sassConfigurationLoader: ConfigurationFile | undefined; + private static _sassConfigurationLoader: ProjectConfigurationFile | undefined; private _sassConfiguration: ISassConfiguration | undefined; private _sassProcessor: SassProcessor | undefined; @@ -105,7 +105,7 @@ export default class SassPlugin implements IHeftPlugin { ): Promise { if (!this._sassConfiguration) { if (!SassPlugin._sassConfigurationLoader) { - SassPlugin._sassConfigurationLoader = new ConfigurationFile({ + SassPlugin._sassConfigurationLoader = new ProjectConfigurationFile({ projectRelativeFilePath: SASS_CONFIGURATION_LOCATION, jsonSchemaObject: sassConfigSchema }); diff --git a/heft-plugins/heft-typescript-plugin/src/TypeScriptPlugin.ts b/heft-plugins/heft-typescript-plugin/src/TypeScriptPlugin.ts index ef174b75e4d..a347be9adf4 100644 --- a/heft-plugins/heft-typescript-plugin/src/TypeScriptPlugin.ts +++ b/heft-plugins/heft-typescript-plugin/src/TypeScriptPlugin.ts @@ -7,7 +7,7 @@ import type * as TTypescript from 'typescript'; import { SyncHook } from 'tapable'; import { FileSystem, Path } from '@rushstack/node-core-library'; import type { ITerminal } from '@rushstack/terminal'; -import { ConfigurationFile, InheritanceType, PathResolutionMethod } from '@rushstack/heft-config-file'; +import { ProjectConfigurationFile, InheritanceType, PathResolutionMethod } from '@rushstack/heft-config-file'; import type { HeftConfiguration, IHeftTaskSession, @@ -127,7 +127,7 @@ export interface ITypeScriptPluginAccessor { readonly onChangedFilesHook: SyncHook; } -let _typeScriptConfigurationFileLoader: ConfigurationFile | undefined; +let _typeScriptConfigurationFileLoader: ProjectConfigurationFile | undefined; const _typeScriptConfigurationFilePromiseCache: Map< string, Promise @@ -149,7 +149,7 @@ export async function loadTypeScriptConfigurationFileAsync( if (!typescriptConfigurationFilePromise) { // Ensure that the file loader has been initialized. if (!_typeScriptConfigurationFileLoader) { - _typeScriptConfigurationFileLoader = new ConfigurationFile({ + _typeScriptConfigurationFileLoader = new ProjectConfigurationFile({ projectRelativeFilePath: 'config/typescript.json', jsonSchemaObject: typescriptConfigSchema, propertyInheritance: { @@ -173,7 +173,7 @@ export async function loadTypeScriptConfigurationFileAsync( return await typescriptConfigurationFilePromise; } -let _partialTsconfigFileLoader: ConfigurationFile | undefined; +let _partialTsconfigFileLoader: ProjectConfigurationFile | undefined; const _partialTsconfigFilePromiseCache: Map> = new Map(); function getTsconfigFilePath( @@ -213,7 +213,7 @@ export async function loadPartialTsconfigFileAsync( } else { // Ensure that the file loader has been initialized. if (!_partialTsconfigFileLoader) { - _partialTsconfigFileLoader = new ConfigurationFile({ + _partialTsconfigFileLoader = new ProjectConfigurationFile({ projectRelativeFilePath: typeScriptConfigurationJson?.project || 'tsconfig.json', jsonSchemaObject: anythingSchema, propertyInheritance: { diff --git a/libraries/heft-config-file/src/ProjectConfigurationFile.ts b/libraries/heft-config-file/src/ProjectConfigurationFile.ts index ac4788df0e5..066ed7a5327 100644 --- a/libraries/heft-config-file/src/ProjectConfigurationFile.ts +++ b/libraries/heft-config-file/src/ProjectConfigurationFile.ts @@ -74,7 +74,7 @@ export class ProjectConfigurationFile extends ConfigurationF } /** - * This function is identical to {@link ConfigurationFile.loadConfigurationFileForProject}, except + * This function is identical to {@link ProjectConfigurationFile.loadConfigurationFileForProject}, except * that it returns `undefined` instead of throwing an error if the configuration file cannot be found. */ public tryLoadConfigurationFileForProject( @@ -93,7 +93,7 @@ export class ProjectConfigurationFile extends ConfigurationF } /** - * This function is identical to {@link ConfigurationFile.loadConfigurationFileForProjectAsync}, except + * This function is identical to {@link ProjectConfigurationFile.loadConfigurationFileForProjectAsync}, except * that it returns `undefined` instead of throwing an error if the configuration file cannot be found. */ public async tryLoadConfigurationFileForProjectAsync( diff --git a/libraries/heft-config-file/src/index.ts b/libraries/heft-config-file/src/index.ts index c809c9e10b3..059d606f5d3 100644 --- a/libraries/heft-config-file/src/index.ts +++ b/libraries/heft-config-file/src/index.ts @@ -29,9 +29,20 @@ export { type PropertyInheritanceCustomFunction } from './ConfigurationFileBase'; -export { - // TODO: REname this export to `ProjectConfigurationFile` in the next major version bump - ProjectConfigurationFile as ConfigurationFile, - type IProjectConfigurationFileOptions -} from './ProjectConfigurationFile'; +import { ProjectConfigurationFile } from './ProjectConfigurationFile'; + +/** + * @deprecated Use {@link ProjectConfigurationFile} instead. + * @beta + */ +export const ConfigurationFile: typeof ProjectConfigurationFile = ProjectConfigurationFile; + +/** + * @deprecated Use {@link ProjectConfigurationFile} instead. + * @beta + */ +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type ConfigurationFile = ProjectConfigurationFile; + +export { ProjectConfigurationFile, type IProjectConfigurationFileOptions } from './ProjectConfigurationFile'; export { NonProjectConfigurationFile } from './NonProjectConfigurationFile'; diff --git a/libraries/rush-lib/src/api/RushProjectConfiguration.ts b/libraries/rush-lib/src/api/RushProjectConfiguration.ts index 5c6b60ff73b..be7af0597a4 100644 --- a/libraries/rush-lib/src/api/RushProjectConfiguration.ts +++ b/libraries/rush-lib/src/api/RushProjectConfiguration.ts @@ -3,7 +3,7 @@ import { AlreadyReportedError, Async, Path } from '@rushstack/node-core-library'; import type { ITerminal } from '@rushstack/terminal'; -import { ConfigurationFile, InheritanceType } from '@rushstack/heft-config-file'; +import { ProjectConfigurationFile, InheritanceType } from '@rushstack/heft-config-file'; import { RigConfig } from '@rushstack/rig-package'; import type { RushConfigurationProject } from './RushConfigurationProject'; @@ -147,8 +147,8 @@ interface IOldRushProjectJson { buildCacheOptions?: unknown; } -const RUSH_PROJECT_CONFIGURATION_FILE: ConfigurationFile = - new ConfigurationFile({ +const RUSH_PROJECT_CONFIGURATION_FILE: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: `config/${RushConstants.rushProjectConfigFilename}`, jsonSchemaObject: schemaJson, propertyInheritance: { @@ -230,8 +230,8 @@ const RUSH_PROJECT_CONFIGURATION_FILE: ConfigurationFile = } }); -const OLD_RUSH_PROJECT_CONFIGURATION_FILE: ConfigurationFile = - new ConfigurationFile({ +const OLD_RUSH_PROJECT_CONFIGURATION_FILE: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: RUSH_PROJECT_CONFIGURATION_FILE.projectRelativeFilePath, jsonSchemaObject: anythingSchemaJson }); diff --git a/rush-plugins/rush-serve-plugin/src/RushProjectServeConfigFile.ts b/rush-plugins/rush-serve-plugin/src/RushProjectServeConfigFile.ts index 0182025bc19..0126fcd5d30 100644 --- a/rush-plugins/rush-serve-plugin/src/RushProjectServeConfigFile.ts +++ b/rush-plugins/rush-serve-plugin/src/RushProjectServeConfigFile.ts @@ -3,7 +3,7 @@ import path from 'path'; -import { ConfigurationFile, InheritanceType } from '@rushstack/heft-config-file'; +import { ProjectConfigurationFile, InheritanceType } from '@rushstack/heft-config-file'; import { Async } from '@rushstack/node-core-library'; import type { ITerminal } from '@rushstack/terminal'; import { RigConfig } from '@rushstack/rig-package'; @@ -39,10 +39,10 @@ export interface IRoutingRule { } export class RushServeConfiguration { - private readonly _loader: ConfigurationFile; + private readonly _loader: ProjectConfigurationFile; public constructor() { - this._loader = new ConfigurationFile({ + this._loader = new ProjectConfigurationFile({ projectRelativeFilePath: 'config/rush-project-serve.json', jsonSchemaObject: rushProjectServeSchema, propertyInheritance: { From 434f3b9b685fdd34ee8b34a80c6b04428fac88a8 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 2 Dec 2024 22:35:07 -0500 Subject: [PATCH 09/12] fixup! Add support for extends in pnpm-config.json --- libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index 9082322d60f..ffc0f4b3b24 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -163,8 +163,6 @@ export interface IPnpmOptionsJson extends IPackageManagerOptionsJsonBase { * @public */ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfigurationBase { - private static _jsonSchema: JsonSchema = JsonSchema.fromLoadedObject(schemaJson); - private readonly _json: JsonObject; private _globalPatchedDependencies: Record | undefined; @@ -441,7 +439,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration const pnpmOptionsConfigFile: NonProjectConfigurationFile = new NonProjectConfigurationFile({ - jsonSchemaObject: PnpmOptionsConfiguration._jsonSchema + jsonSchemaObject: schemaJson }); const pnpmOptionJson: IPnpmOptionsJson = pnpmOptionsConfigFile.loadConfigurationFile( terminal, From 6cd2675e19ca26a499c56a561d6c442440963b21 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 2 Dec 2024 22:49:01 -0500 Subject: [PATCH 10/12] fixup! Add support for extends in pnpm-config.json --- libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index ffc0f4b3b24..92009e39c84 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { JsonFile, type JsonObject, JsonSchema } from '@rushstack/node-core-library'; +import { JsonFile, type JsonObject } from '@rushstack/node-core-library'; import { NonProjectConfigurationFile } from '@rushstack/heft-config-file'; import { ConsoleTerminalProvider, Terminal } from '@rushstack/terminal'; From 2f2d7baa9f7dca8712e453788fabed46eb92debb Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 2 Dec 2024 23:23:27 -0500 Subject: [PATCH 11/12] Strip investigative annotations during tests. --- common/reviews/api/heft-config-file.api.md | 9 ++++++ .../src/ConfigurationFileBase.ts | 6 ++-- .../heft-config-file/src/TestUtilities.ts | 32 +++++++++++++++++++ libraries/heft-config-file/src/index.ts | 2 ++ .../test/PnpmOptionsConfiguration.test.ts | 12 ++++--- .../src/logic/test/InstallHelpers.test.ts | 8 +++-- 6 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 libraries/heft-config-file/src/TestUtilities.ts diff --git a/common/reviews/api/heft-config-file.api.md b/common/reviews/api/heft-config-file.api.md index eaa9ad5b28b..d508fb1419a 100644 --- a/common/reviews/api/heft-config-file.api.md +++ b/common/reviews/api/heft-config-file.api.md @@ -164,4 +164,13 @@ export class ProjectConfigurationFile extends ConfigurationF // @beta (undocumented) export type PropertyInheritanceCustomFunction = (currentObject: TObject, parentObject: TObject) => TObject; +// @beta +function stripAnnotations(obj: TObject): TObject; + +declare namespace TestUtilities { + export { + stripAnnotations + } +} + ``` diff --git a/libraries/heft-config-file/src/ConfigurationFileBase.ts b/libraries/heft-config-file/src/ConfigurationFileBase.ts index 434f047cebc..448e2a5a38a 100644 --- a/libraries/heft-config-file/src/ConfigurationFileBase.ts +++ b/libraries/heft-config-file/src/ConfigurationFileBase.ts @@ -74,9 +74,11 @@ export enum PathResolutionMethod { } const CONFIGURATION_FILE_MERGE_BEHAVIOR_FIELD_REGEX: RegExp = /^\$([^\.]+)\.inheritanceType$/; -const CONFIGURATION_FILE_FIELD_ANNOTATION: unique symbol = Symbol('configuration-file-field-annotation'); +export const CONFIGURATION_FILE_FIELD_ANNOTATION: unique symbol = Symbol( + 'configuration-file-field-annotation' +); -interface IAnnotatedField { +export interface IAnnotatedField { [CONFIGURATION_FILE_FIELD_ANNOTATION]: IConfigurationFileFieldAnnotation; } diff --git a/libraries/heft-config-file/src/TestUtilities.ts b/libraries/heft-config-file/src/TestUtilities.ts new file mode 100644 index 00000000000..b51f3f455c0 --- /dev/null +++ b/libraries/heft-config-file/src/TestUtilities.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { CONFIGURATION_FILE_FIELD_ANNOTATION, type IAnnotatedField } from './ConfigurationFileBase'; + +/** + * Returns an object with investigative annotations stripped, useful for snapshot testing. + * + * @beta + */ +export function stripAnnotations(obj: TObject): TObject { + if (typeof obj !== 'object' || obj === null) { + return obj; + } else if (Array.isArray(obj)) { + const result: unknown[] = []; + for (const value of obj) { + result.push(stripAnnotations(value)); + } + + return result as TObject; + } else { + const clonedObj: TObject = { ...obj } as TObject; + delete (clonedObj as Partial>)[CONFIGURATION_FILE_FIELD_ANNOTATION]; + for (const [name, value] of Object.entries(clonedObj as object)) { + clonedObj[name as keyof TObject] = stripAnnotations( + value as TObject[keyof TObject] + ); + } + + return clonedObj; + } +} diff --git a/libraries/heft-config-file/src/index.ts b/libraries/heft-config-file/src/index.ts index 059d606f5d3..4ae1fa97b9b 100644 --- a/libraries/heft-config-file/src/index.ts +++ b/libraries/heft-config-file/src/index.ts @@ -46,3 +46,5 @@ export type ConfigurationFile = ProjectConfigurationFile { fakeCommonTempFolder ); - expect(pnpmConfiguration.globalOverrides).toEqual({ + expect(TestUtilities.stripAnnotations(pnpmConfiguration.globalOverrides)).toEqual({ foo: '^1.0.0', quux: 'npm:@myorg/quux@^1.0.0', 'bar@^2.1.0': '3.0.0', 'qar@1>zoo': '2' }); - expect(pnpmConfiguration.environmentVariables).toEqual({ + expect(TestUtilities.stripAnnotations(pnpmConfiguration.environmentVariables)).toEqual({ NODE_OPTIONS: { value: '--max-old-space-size=4096', override: false @@ -52,7 +53,7 @@ describe(PnpmOptionsConfiguration.name, () => { fakeCommonTempFolder ); - expect(pnpmConfiguration.globalPackageExtensions).toEqual({ + expect(TestUtilities.stripAnnotations(pnpmConfiguration.globalPackageExtensions)).toEqual({ 'react-redux': { peerDependencies: { 'react-dom': '*' @@ -67,6 +68,9 @@ describe(PnpmOptionsConfiguration.name, () => { fakeCommonTempFolder ); - expect(pnpmConfiguration.globalNeverBuiltDependencies).toEqual(['fsevents', 'level']); + expect(TestUtilities.stripAnnotations(pnpmConfiguration.globalNeverBuiltDependencies)).toEqual([ + 'fsevents', + 'level' + ]); }); }); diff --git a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts index 32be66eabd8..d161d5da632 100644 --- a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts +++ b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts @@ -1,10 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { InstallHelpers } from '../installManager/InstallHelpers'; -import { RushConfiguration } from '../../api/RushConfiguration'; import { type IPackageJson, JsonFile } from '@rushstack/node-core-library'; import { StringBufferTerminalProvider, Terminal } from '@rushstack/terminal'; +import { TestUtilities } from '@rushstack/heft-config-file'; + +import { InstallHelpers } from '../installManager/InstallHelpers'; +import { RushConfiguration } from '../../api/RushConfiguration'; describe('InstallHelpers', () => { describe('generateCommonPackageJson', () => { @@ -48,7 +50,7 @@ describe('InstallHelpers', () => { terminal ); const packageJson: IPackageJson = mockJsonFileSave.mock.calls[0][0]; - expect(packageJson).toEqual( + expect(TestUtilities.stripAnnotations(packageJson)).toEqual( expect.objectContaining({ pnpm: { overrides: { From b545c11eaffd6ee524ea4e7f26cfd86770e1b951 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 2 Dec 2024 23:49:57 -0500 Subject: [PATCH 12/12] fixup! Create a non-project version of ConfigurationFile. --- libraries/heft-config-file/src/test/ConfigurationFile.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/heft-config-file/src/test/ConfigurationFile.test.ts b/libraries/heft-config-file/src/test/ConfigurationFile.test.ts index 88d11c3a03a..a47516a7968 100644 --- a/libraries/heft-config-file/src/test/ConfigurationFile.test.ts +++ b/libraries/heft-config-file/src/test/ConfigurationFile.test.ts @@ -201,7 +201,7 @@ describe('ConfigurationFile', () => { expect(JSON.stringify(loadedConfigFile)).toEqual(JSON.stringify(expectedConfigFile)); expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile)).toEqual( - nodeJsPath.resolve(__dirname, projectRelativeFilePath) + `${__dirname}/${projectRelativeFilePath}` ); expect( configFileLoader.getPropertyOriginalValue({ parentObject: loadedConfigFile, propertyName: 'thing' }) @@ -219,7 +219,7 @@ describe('ConfigurationFile', () => { expect(JSON.stringify(loadedConfigFile)).toEqual(JSON.stringify(expectedConfigFile)); expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile)).toEqual( - nodeJsPath.resolve(__dirname, projectRelativeFilePath) + `${__dirname}/${projectRelativeFilePath}` ); expect( configFileLoader.getPropertyOriginalValue({ parentObject: loadedConfigFile, propertyName: 'thing' })