Skip to content

[heft] Add tryLoadProjectConfigurationFileAsync API and use in plugins #5147

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 8, 2025
60 changes: 60 additions & 0 deletions apps/heft/src/configuration/HeftConfiguration.ts
Original file line number Diff line number Diff line change
@@ -4,6 +4,10 @@
import * as path from 'path';
import { type IPackageJson, PackageJsonLookup, InternalError, Path } from '@rushstack/node-core-library';
import { Terminal, type ITerminalProvider, type ITerminal } from '@rushstack/terminal';
import {
type IProjectConfigurationFileSpecification,
ProjectConfigurationFile
} from '@rushstack/heft-config-file';
import { type IRigConfig, RigConfig } from '@rushstack/rig-package';

import { Constants } from '../utilities/Constants';
@@ -33,6 +37,11 @@ interface IHeftConfigurationOptions extends IHeftConfigurationInitializationOpti
buildFolderPath: string;
}

interface IProjectConfigurationFileEntry<TConfigFile> {
options: IProjectConfigurationFileSpecification<TConfigFile>;
loader: ProjectConfigurationFile<TConfigFile>;
}

/**
* @public
*/
@@ -43,6 +52,8 @@ export class HeftConfiguration {
private _rigConfig: IRigConfig | undefined;
private _rigPackageResolver: RigPackageResolver | undefined;

private readonly _knownConfigurationFiles: Map<string, IProjectConfigurationFileEntry<unknown>> = new Map();

/**
* Project build folder path. This is the folder containing the project's package.json file.
*/
@@ -161,6 +172,34 @@ export class HeftConfiguration {
}
}

/**
* Attempts to load a riggable project configuration file using blocking, synchronous I/O.
* @param options - The options for the configuration file loader from `@rushstack/heft-config-file`. If invoking this function multiple times for the same file, reuse the same object.
* @param terminal - The terminal to log messages during configuration file loading.
* @returns The configuration file, or undefined if it could not be loaded.
*/
public tryLoadProjectConfigurationFile<TConfigFile>(
options: IProjectConfigurationFileSpecification<TConfigFile>,
terminal: ITerminal
): TConfigFile | undefined {
const loader: ProjectConfigurationFile<TConfigFile> = this._getConfigFileLoader(options);
return loader.tryLoadConfigurationFileForProject(terminal, this.buildFolderPath, this._rigConfig);
}

/**
* Attempts to load a riggable project configuration file using asynchronous I/O.
* @param options - The options for the configuration file loader from `@rushstack/heft-config-file`. If invoking this function multiple times for the same file, reuse the same object.
* @param terminal - The terminal to log messages during configuration file loading.
* @returns A promise that resolves to the configuration file, or undefined if it could not be loaded.
*/
public async tryLoadProjectConfigurationFileAsync<TConfigFile>(
options: IProjectConfigurationFileSpecification<TConfigFile>,
terminal: ITerminal
): Promise<TConfigFile | undefined> {
const loader: ProjectConfigurationFile<TConfigFile> = this._getConfigFileLoader(options);
return loader.tryLoadConfigurationFileForProjectAsync(terminal, this.buildFolderPath, this._rigConfig);
}

/**
* @internal
*/
@@ -187,4 +226,25 @@ export class HeftConfiguration {
});
return configuration;
}

private _getConfigFileLoader<TConfigFile>(
options: IProjectConfigurationFileSpecification<TConfigFile>
): ProjectConfigurationFile<TConfigFile> {
let entry: IProjectConfigurationFileEntry<TConfigFile> | undefined = this._knownConfigurationFiles.get(
options.projectRelativeFilePath
) as IProjectConfigurationFileEntry<TConfigFile> | undefined;

if (!entry) {
entry = {
options: Object.freeze(options),
loader: new ProjectConfigurationFile<TConfigFile>(options)
};
} else if (options !== entry.options) {
throw new Error(
`The project configuration file for ${options.projectRelativeFilePath} has already been loaded with different options. Please ensure that options object used to load the configuration file is the same referenced object in all calls.`
);
}

return entry.loader;
}
}
20 changes: 20 additions & 0 deletions apps/heft/src/configuration/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

export type {
CustomValidationFunction,
ICustomJsonPathMetadata,
ICustomPropertyInheritance,
IJsonPathMetadata,
IJsonPathMetadataResolverOptions,
IJsonPathsMetadata,
INonCustomJsonPathMetadata,
IOriginalValueOptions,
IProjectConfigurationFileSpecification,
IPropertiesInheritance,
IPropertyInheritance,
IPropertyInheritanceDefaults,
InheritanceType,
PathResolutionMethod,
PropertyInheritanceCustomFunction
} from '@rushstack/heft-config-file';
3 changes: 3 additions & 0 deletions apps/heft/src/index.ts
Original file line number Diff line number Diff line change
@@ -11,6 +11,9 @@
* @packageDocumentation
*/

import type * as ConfigurationFile from './configuration/types';
export type { ConfigurationFile };

export {
HeftConfiguration,
type IHeftConfigurationInitializationOptions as _IHeftConfigurationInitializationOptions
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/heft-api-extractor-plugin",
"comment": "Use `tryLoadProjectConfigurationFileAsync` Heft API to remove direct dependency on `@rushstack/heft-config-file`.",
"type": "minor"
}
],
"packageName": "@rushstack/heft-api-extractor-plugin"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/heft-config-file",
"comment": "Fix an issue with `PathResolutionMethod.resolvePathRelativeToProjectRoot` when extending files across packages.",
"type": "minor"
}
],
"packageName": "@rushstack/heft-config-file"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/heft-config-file",
"comment": "Add a new `customValidationFunction` option for custom validation logic on loaded configuration files.",
"type": "minor"
}
],
"packageName": "@rushstack/heft-config-file"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/heft-sass-plugin",
"comment": "Use `tryLoadProjectConfigurationFileAsync` API to remove direct dependency on `@rushstack/heft-config-file`.",
"type": "minor"
}
],
"packageName": "@rushstack/heft-sass-plugin"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/heft",
"comment": "Add a method `tryLoadProjectConfigurationFileAsync<TConfigFile>(options, terminal)` to `HeftConfiguration`.",
"type": "minor"
}
],
"packageName": "@rushstack/heft"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/heft-typescript-plugin",
"comment": "Leverage Heft's new `tryLoadProjectConfigurationFileAsync` method.",
"type": "minor"
}
],
"packageName": "@rushstack/heft-typescript-plugin"
}
Original file line number Diff line number Diff line change
@@ -2,5 +2,5 @@
{
"pnpmShrinkwrapHash": "d9e805ea30f80e290b5d5ea83856c8b5d7302941",
"preferredVersionsHash": "54149ea3f01558a859c96dee2052b797d4defe68",
"packageJsonInjectedDependenciesHash": "8982ce433cee12f925dfe4d72638fad18cbf641c"
"packageJsonInjectedDependenciesHash": "949bb6038c34cb0580b82a6f728b26a66fff3177"
}
6 changes: 0 additions & 6 deletions common/config/subspaces/default/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 48 additions & 26 deletions common/reviews/api/heft-config-file.api.md
Original file line number Diff line number Diff line change
@@ -21,20 +21,20 @@ export abstract class ConfigurationFileBase<TConfigurationFile, TExtraOptions ex
getObjectSourceFilePath<TObject extends object>(obj: TObject): string | undefined;
getPropertyOriginalValue<TParentProperty extends object, TValue>(options: IOriginalValueOptions<TParentProperty>): TValue | undefined;
// (undocumented)
protected _loadConfigurationFileInnerWithCache(terminal: ITerminal, resolvedConfigurationFilePath: string, visitedConfigurationFilePaths: Set<string>, rigConfig: IRigConfig | undefined): TConfigurationFile;
protected _loadConfigurationFileInnerWithCache(terminal: ITerminal, resolvedConfigurationFilePath: string, projectFolderPath: string | undefined, onConfigurationFileNotFound?: IOnConfigurationFileNotFoundCallback): TConfigurationFile;
// (undocumented)
protected _loadConfigurationFileInnerWithCacheAsync(terminal: ITerminal, resolvedConfigurationFilePath: string, visitedConfigurationFilePaths: Set<string>, rigConfig: IRigConfig | undefined): Promise<TConfigurationFile>;
// (undocumented)
protected abstract _tryLoadConfigurationFileInRig(terminal: ITerminal, rigConfig: IRigConfig, visitedConfigurationFilePaths: Set<string>): TConfigurationFile | undefined;
// (undocumented)
protected abstract _tryLoadConfigurationFileInRigAsync(terminal: ITerminal, rigConfig: IRigConfig, visitedConfigurationFilePaths: Set<string>): Promise<TConfigurationFile | undefined>;
protected _loadConfigurationFileInnerWithCacheAsync(terminal: ITerminal, resolvedConfigurationFilePath: string, projectFolderPath: string | undefined, onFileNotFound?: IOnConfigurationFileNotFoundCallback): Promise<TConfigurationFile>;
}

// @beta
export type CustomValidationFunction<TConfigurationFile> = (configurationFile: TConfigurationFile, resolvedConfigurationFilePathForLogging: string, terminal: ITerminal) => boolean;

// @beta (undocumented)
export type IConfigurationFileOptions<TConfigurationFile, TExtraOptions extends object> = IConfigurationFileOptionsWithJsonSchemaFilePath<TConfigurationFile, TExtraOptions> | IConfigurationFileOptionsWithJsonSchemaObject<TConfigurationFile, TExtraOptions>;

// @beta (undocumented)
export interface IConfigurationFileOptionsBase<TConfigurationFile> {
customValidationFunction?: CustomValidationFunction<TConfigurationFile>;
jsonPathMetadata?: IJsonPathsMetadata<TConfigurationFile>;
propertyInheritance?: IPropertiesInheritance<TConfigurationFile>;
propertyInheritanceDefaults?: IPropertyInheritanceDefaults;
@@ -70,6 +70,7 @@ export type IJsonPathMetadata<T> = ICustomJsonPathMetadata<T> | INonCustomJsonPa
export interface IJsonPathMetadataResolverOptions<TConfigurationFile> {
configurationFile: Partial<TConfigurationFile>;
configurationFilePath: string;
projectFolderPath?: string;
propertyName: string;
propertyValue: string;
}
@@ -80,19 +81,33 @@ export interface IJsonPathsMetadata<TConfigurationFile> {
[jsonPath: string]: IJsonPathMetadata<TConfigurationFile>;
}

// @beta
export const InheritanceType: {
readonly append: "append";
readonly merge: "merge";
readonly replace: "replace";
readonly custom: "custom";
};

// @beta (undocumented)
export type InheritanceType = (typeof InheritanceType)[keyof typeof InheritanceType];

// @beta (undocumented)
export enum InheritanceType {
append = "append",
custom = "custom",
merge = "merge",
replace = "replace"
export namespace InheritanceType {
export type append = typeof InheritanceType.append;
export type custom = typeof InheritanceType.custom;
export type merge = typeof InheritanceType.merge;
export type replace = typeof InheritanceType.replace;
}

// @beta
export interface INonCustomJsonPathMetadata {
pathResolutionMethod?: PathResolutionMethod.NodeResolve | PathResolutionMethod.nodeResolve | PathResolutionMethod.resolvePathRelativeToConfigurationFile | PathResolutionMethod.resolvePathRelativeToProjectRoot;
}

// @beta
export type IOnConfigurationFileNotFoundCallback = (resolvedConfigurationFilePathForLogging: string) => string | undefined;

// @beta (undocumented)
export interface IOriginalValueOptions<TParentProperty> {
// (undocumented)
@@ -106,6 +121,9 @@ export interface IProjectConfigurationFileOptions {
projectRelativeFilePath: string;
}

// @beta
export type IProjectConfigurationFileSpecification<TConfigFile> = IConfigurationFileOptions<TConfigFile, IProjectConfigurationFileOptions>;

// @beta (undocumented)
export type IPropertiesInheritance<TConfigurationFile> = {
[propertyName in keyof TConfigurationFile]?: IPropertyInheritance<InheritanceType.append | InheritanceType.merge | InheritanceType.replace> | ICustomPropertyInheritance<TConfigurationFile[propertyName]>;
@@ -131,34 +149,38 @@ export class NonProjectConfigurationFile<TConfigurationFile> extends Configurati
loadConfigurationFileAsync(terminal: ITerminal, filePath: string): Promise<TConfigurationFile>;
tryLoadConfigurationFile(terminal: ITerminal, filePath: string): TConfigurationFile | undefined;
tryLoadConfigurationFileAsync(terminal: ITerminal, filePath: string): Promise<TConfigurationFile | undefined>;
// (undocumented)
protected _tryLoadConfigurationFileInRig(terminal: ITerminal, rigConfig: IRigConfig, visitedConfigurationFilePaths: Set<string>): TConfigurationFile | undefined;
// (undocumented)
protected _tryLoadConfigurationFileInRigAsync(terminal: ITerminal, rigConfig: IRigConfig, visitedConfigurationFilePaths: Set<string>): Promise<TConfigurationFile | undefined>;
}

// @beta
export const PathResolutionMethod: {
readonly resolvePathRelativeToConfigurationFile: "resolvePathRelativeToConfigurationFile";
readonly resolvePathRelativeToProjectRoot: "resolvePathRelativeToProjectRoot";
readonly NodeResolve: "NodeResolve";
readonly nodeResolve: "nodeResolve";
readonly custom: "custom";
};

// @beta (undocumented)
export type PathResolutionMethod = (typeof PathResolutionMethod)[keyof typeof PathResolutionMethod];

// @beta (undocumented)
export enum PathResolutionMethod {
custom = "custom",
export namespace PathResolutionMethod {
export type custom = typeof PathResolutionMethod.custom;
// @deprecated
NodeResolve = "NodeResolve",
nodeResolve = "nodeResolve",
resolvePathRelativeToConfigurationFile = "resolvePathRelativeToConfigurationFile",
resolvePathRelativeToProjectRoot = "resolvePathRelativeToProjectRoot"
export type NodeResolve = typeof PathResolutionMethod.NodeResolve;
export type nodeResolve = typeof PathResolutionMethod.nodeResolve;
export type resolvePathRelativeToConfigurationFile = typeof PathResolutionMethod.resolvePathRelativeToConfigurationFile;
export type resolvePathRelativeToProjectRoot = typeof PathResolutionMethod.resolvePathRelativeToProjectRoot;
}

// @beta (undocumented)
export class ProjectConfigurationFile<TConfigurationFile> extends ConfigurationFileBase<TConfigurationFile, IProjectConfigurationFileOptions> {
constructor(options: IConfigurationFileOptions<TConfigurationFile, IProjectConfigurationFileOptions>);
constructor(options: IProjectConfigurationFileSpecification<TConfigurationFile>);
loadConfigurationFileForProject(terminal: ITerminal, projectPath: string, rigConfig?: IRigConfig): TConfigurationFile;
loadConfigurationFileForProjectAsync(terminal: ITerminal, projectPath: string, rigConfig?: IRigConfig): Promise<TConfigurationFile>;
readonly projectRelativeFilePath: string;
tryLoadConfigurationFileForProject(terminal: ITerminal, projectPath: string, rigConfig?: IRigConfig): TConfigurationFile | undefined;
tryLoadConfigurationFileForProjectAsync(terminal: ITerminal, projectPath: string, rigConfig?: IRigConfig): Promise<TConfigurationFile | undefined>;
// (undocumented)
protected _tryLoadConfigurationFileInRig(terminal: ITerminal, rigConfig: IRigConfig, visitedConfigurationFilePaths: Set<string>): TConfigurationFile | undefined;
// (undocumented)
protected _tryLoadConfigurationFileInRigAsync(terminal: ITerminal, rigConfig: IRigConfig, visitedConfigurationFilePaths: Set<string>): Promise<TConfigurationFile | undefined>;
}

// @beta (undocumented)
Loading
Loading