From b71f856e15c5df971b57c109b486a1c5d854c971 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 2 Dec 2024 21:09:44 -0800 Subject: [PATCH] [rush] Add support for an `"extends"` property in `pnpm-config.json` files. (#5023) * Create a non-project version of ConfigurationFile. * fixup! Create a non-project version of ConfigurationFile. * fixup! Create a non-project version of ConfigurationFile. * fixup! Create a non-project version of ConfigurationFile. * Add support for extends in pnpm-config.json * fixup! Add support for extends in pnpm-config.json * fixup! Add support for extends in pnpm-config.json * Rename ConfigurationFile to ProjectConfigurationFile. * fixup! Add support for extends in pnpm-config.json --- apps/heft/src/utilities/CoreConfigFiles.ts | 21 +- ...-config.json-extends_2024-12-02-23-58.json | 10 + ...-config.json-extends_2024-12-03-03-27.json | 10 + ...-config.json-extends_2024-12-02-23-57.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 | 78 +++- .../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 +- ...rationFile.ts => ConfigurationFileBase.ts} | 240 +++---------- .../src/NonProjectConfigurationFile.ts | 91 +++++ .../src/ProjectConfigurationFile.ts | 187 ++++++++++ .../heft-config-file/src/TestUtilities.ts | 32 ++ libraries/heft-config-file/src/index.ts | 24 +- .../src/test/ConfigurationFile.test.ts | 336 ++++++++++-------- .../ConfigurationFile.test.ts.snap | 40 +++ .../src/api/RushProjectConfiguration.ts | 10 +- .../logic/pnpm/PnpmOptionsConfiguration.ts | 19 +- .../test/PnpmOptionsConfiguration.test.ts | 12 +- .../src/logic/test/InstallHelpers.test.ts | 8 +- .../src/schemas/pnpm-config.schema.json | 5 + .../src/RushProjectServeConfigFile.ts | 6 +- 28 files changed, 827 insertions(+), 405 deletions(-) create mode 100644 common/changes/@microsoft/rush/support-pnpm-config.json-extends_2024-12-02-23-58.json 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-02-23-57.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 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 create mode 100644 libraries/heft-config-file/src/TestUtilities.ts 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/@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/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-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 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 24dd86b64a8..d508fb1419a 100644 --- a/common/reviews/api/heft-config-file.api.md +++ b/common/reviews/api/heft-config-file.api.md @@ -7,44 +7,50 @@ import type { IRigConfig } from '@rushstack/rig-package'; import type { ITerminal } from '@rushstack/terminal'; +// @beta @deprecated (undocumented) +export const ConfigurationFile: typeof ProjectConfigurationFile; + +// @beta @deprecated (undocumented) +export type ConfigurationFile = ProjectConfigurationFile; + // @beta (undocumented) -export class ConfigurationFile { - constructor(options: IConfigurationFileOptions); +export abstract class ConfigurationFileBase { + constructor(options: IConfigurationFileOptions); // @internal (undocumented) static _formatPathForLogging: (path: string) => string; getObjectSourceFilePath(obj: TObject): string | undefined; getPropertyOriginalValue(options: IOriginalValueOptions): TValue | undefined; - 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 _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 +101,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 +125,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", @@ -124,7 +147,30 @@ 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; +// @beta +function stripAnnotations(obj: TObject): TObject; + +declare namespace TestUtilities { + export { + stripAnnotations + } +} + ``` 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/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..448e2a5a38a 100644 --- a/libraries/heft-config-file/src/ConfigurationFile.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; } @@ -208,11 +210,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 +231,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 +283,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 +302,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 +314,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 +352,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 +383,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 +419,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 +480,7 @@ export class ConfigurationFile { visitedConfigurationFilePaths: Set, rigConfig: IRigConfig | undefined ): TConfigurationFile { - const resolvedConfigurationFilePathForLogging: string = ConfigurationFile._formatPathForLogging( + const resolvedConfigurationFilePathForLogging: string = ConfigurationFileBase._formatPathForLogging( resolvedConfigurationFilePath ); @@ -634,7 +568,7 @@ export class ConfigurationFile { visitedConfigurationFilePaths: Set, rigConfig: IRigConfig | undefined ): Promise { - const resolvedConfigurationFilePathForLogging: string = ConfigurationFile._formatPathForLogging( + const resolvedConfigurationFilePathForLogging: string = ConfigurationFileBase._formatPathForLogging( resolvedConfigurationFilePath ); @@ -713,76 +647,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 +694,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 +985,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..d3a7bd23dcd --- /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 type { IRigConfig } from '@rushstack/rig-package'; + +import { ConfigurationFileBase } from './ConfigurationFileBase'; + +/** + * @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..066ed7a5327 --- /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, type 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 ProjectConfigurationFile.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 ProjectConfigurationFile.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/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 896fc44308a..4ae1fa97b9b 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,24 @@ export { type IPropertyInheritanceDefaults, PathResolutionMethod, type PropertyInheritanceCustomFunction -} from './ConfigurationFile'; +} from './ConfigurationFileBase'; + +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'; + +export * as TestUtilities from './TestUtilities'; diff --git a/libraries/heft-config-file/src/test/ConfigurationFile.test.ts b/libraries/heft-config-file/src/test/ConfigurationFile.test.ts index 34790bca53b..a47516a7968 100644 --- a/libraries/heft-config-file/src/test/ConfigurationFile.test.ts +++ b/libraries/heft-config-file/src/test/ConfigurationFile.test.ts @@ -1,14 +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 { ConfigurationFile, PathResolutionMethod, InheritanceType } from '../ConfigurationFile'; +import { ProjectConfigurationFile } from '../ProjectConfigurationFile'; +import { PathResolutionMethod, InheritanceType, ConfigurationFileBase } from '../ConfigurationFileBase'; +import { NonProjectConfigurationFile } from '../NonProjectConfigurationFile'; -describe(ConfigurationFile.name, () => { +describe('ConfigurationFile', () => { const projectRoot: string = nodeJsPath.resolve(__dirname, '..', '..'); let terminalProvider: StringBufferTerminalProvider; let terminal: Terminal; @@ -16,7 +20,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(ConfigurationFileBase, '_formatPathForLogging').mockImplementation(formatPathForLogging); jest.spyOn(JsonFile, '_formatPathForError').mockImplementation(formatPathForLogging); terminalProvider = new StringBufferTerminalProvider(false); @@ -43,8 +47,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 +68,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 +87,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 +114,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 +139,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 +166,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 +189,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( + `${__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( + `${__dirname}/${projectRelativeFilePath}` + ); + expect( + configFileLoader.getPropertyOriginalValue({ parentObject: loadedConfigFile, propertyName: 'thing' }) + ).toEqual('A'); + }); } describe('with a JSON schema path', () => { @@ -214,12 +254,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 +272,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 +290,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 +302,7 @@ describe(ConfigurationFile.name, () => { pathResolutionMethod: PathResolutionMethod.resolvePathRelativeToConfigurationFile } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = configFileLoader.loadConfigurationFileForProject( terminal, __dirname @@ -286,8 +323,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 +335,7 @@ describe(ConfigurationFile.name, () => { pathResolutionMethod: PathResolutionMethod.resolvePathRelativeToConfigurationFile } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = await configFileLoader.loadConfigurationFileForProjectAsync( terminal, __dirname @@ -320,8 +356,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 +368,7 @@ describe(ConfigurationFile.name, () => { pathResolutionMethod: PathResolutionMethod.resolvePathRelativeToProjectRoot } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = configFileLoader.loadConfigurationFileForProject( terminal, __dirname @@ -354,8 +389,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 +401,7 @@ describe(ConfigurationFile.name, () => { pathResolutionMethod: PathResolutionMethod.resolvePathRelativeToProjectRoot } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = await configFileLoader.loadConfigurationFileForProjectAsync( terminal, __dirname @@ -404,46 +438,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 +510,7 @@ describe(ConfigurationFile.name, () => { inheritanceType: InheritanceType.merge } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = configFileLoader.loadConfigurationFileForProject( terminal, __dirname @@ -469,8 +524,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 +536,7 @@ describe(ConfigurationFile.name, () => { inheritanceType: InheritanceType.merge } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = await configFileLoader.loadConfigurationFileForProjectAsync( terminal, __dirname @@ -496,8 +550,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 +562,7 @@ describe(ConfigurationFile.name, () => { inheritanceType: InheritanceType.replace } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = configFileLoader.loadConfigurationFileForProject( terminal, __dirname @@ -523,8 +576,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 +588,7 @@ describe(ConfigurationFile.name, () => { inheritanceType: InheritanceType.replace } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = await configFileLoader.loadConfigurationFileForProjectAsync( terminal, __dirname @@ -550,16 +602,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 +624,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 +646,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 +667,7 @@ describe(ConfigurationFile.name, () => { } } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = configFileLoader.loadConfigurationFileForProject( terminal, __dirname @@ -632,8 +681,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 +702,7 @@ describe(ConfigurationFile.name, () => { } } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = await configFileLoader.loadConfigurationFileForProjectAsync( terminal, __dirname @@ -668,8 +716,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 +728,7 @@ describe(ConfigurationFile.name, () => { pathResolutionMethod: PathResolutionMethod.resolvePathRelativeToConfigurationFile } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = configFileLoader.loadConfigurationFileForProject( terminal, __dirname @@ -711,8 +758,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 +770,7 @@ describe(ConfigurationFile.name, () => { pathResolutionMethod: PathResolutionMethod.resolvePathRelativeToConfigurationFile } } - } - ); + }); const loadedConfigFile: ISimpleConfigFile = await configFileLoader.loadConfigurationFileForProjectAsync( terminal, __dirname @@ -769,8 +815,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 +898,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 +979,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 +1062,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 +1195,8 @@ describe(ConfigurationFile.name, () => { 'inheritanceTypeConfigFile.schema.json' ); - const configFileLoader: ConfigurationFile = - new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: projectRelativeFilePath, jsonSchemaPath: schemaPath }); @@ -1241,8 +1287,8 @@ describe(ConfigurationFile.name, () => { 'inheritanceTypeConfigFile.schema.json' ); - const configFileLoader: ConfigurationFile = - new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: projectRelativeFilePath, jsonSchemaPath: schemaPath }); @@ -1332,8 +1378,8 @@ describe(ConfigurationFile.name, () => { 'simpleInheritanceTypeConfigFile.schema.json' ); - const configFileLoader: ConfigurationFile = - new ConfigurationFile({ + const configFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ projectRelativeFilePath: projectRelativeFilePath, jsonSchemaPath: schemaPath }); @@ -1378,7 +1424,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 +1440,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 +1456,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 +1472,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 +1488,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 +1504,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 +1520,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 +1536,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 +1552,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 +1568,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 +1595,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 +1625,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 +1652,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 +1680,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 +1707,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 +1718,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 +1734,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 +1751,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 +1768,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 +1783,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 +1812,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 +1848,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 +1869,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 +1886,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 +1903,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 +1920,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 +1937,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 +1954,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 +1971,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 +1988,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 +2004,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 +2020,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, 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": "", 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/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index adec7035a54..92009e39c84 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -1,7 +1,9 @@ // 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'; import { type IPackageManagerOptionsJsonBase, @@ -161,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; @@ -434,9 +434,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: Terminal = new Terminal(new ConsoleTerminalProvider()); + + const pnpmOptionsConfigFile: NonProjectConfigurationFile = + new NonProjectConfigurationFile({ + jsonSchemaObject: schemaJson + }); + const pnpmOptionJson: IPnpmOptionsJson = pnpmOptionsConfigFile.loadConfigurationFile( + terminal, + jsonFilename ); return new PnpmOptionsConfiguration(pnpmOptionJson || {}, commonTempFolder, jsonFilename); } diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts index 6ed38a7de65..8fc9a7552d2 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts @@ -3,6 +3,7 @@ import * as path from 'path'; import { PnpmOptionsConfiguration } from '../PnpmOptionsConfiguration'; +import { TestUtilities } from '@rushstack/heft-config-file'; const fakeCommonTempFolder: string = path.join(__dirname, 'common', 'temp'); @@ -31,14 +32,14 @@ describe(PnpmOptionsConfiguration.name, () => { 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: { 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" 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: {