Skip to content

Commit

Permalink
Merge pull request #4208 from theJiawen/feat/rush-custom-tips
Browse files Browse the repository at this point in the history
feat(rush-lib): setup Rush Custom Tips
  • Loading branch information
octogonz committed Aug 12, 2023
2 parents 7688a8f + 06fb2ba commit c745085
Show file tree
Hide file tree
Showing 20 changed files with 513 additions and 134 deletions.
247 changes: 118 additions & 129 deletions build-tests/install-test-workspace/workspace/common/pnpm-lock.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@ console.log('Calling an internal API...');

// Use a path-based import to access an internal API (do so at your own risk!)
import { VersionMismatchFinder } from '@microsoft/rush-lib/lib/logic/versionMismatch/VersionMismatchFinder';
import { ConsoleTerminalProvider, Terminal } from '@rushstack/node-core-library';

VersionMismatchFinder.ensureConsistentVersions(config);
const terminal = new Terminal(new ConsoleTerminalProvider());
VersionMismatchFinder.ensureConsistentVersions(config, terminal);
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Add a new config file \"custom-tips.json\" for customizing Rush messages (GitHub #4207)",
"type": "none"
}
],
"packageName": "@microsoft/rush"
}
31 changes: 31 additions & 0 deletions common/reviews/api/rush-lib.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,19 @@ export class CredentialCache {
static usingAsync(options: ICredentialCacheOptions, doActionAsync: (credentialCache: CredentialCache) => Promise<void> | void): Promise<void>;
}

// @beta
export type CustomTipId = 'TIP_RUSH_INCONSISTENT_VERSIONS' | string;

// @beta
export class CustomTipsConfiguration {
constructor(configFilename: string);
readonly configuration: Readonly<ICustomTipsJson>;
showErrorTip(terminal: ITerminal, tipId: CustomTipId): void;
showInfoTip(terminal: ITerminal, tipId: CustomTipId): void;
showWarningTip(terminal: ITerminal, tipId: CustomTipId): void;
static readonly supportedTipIds: ReadonlySet<string>;
}

// @public (undocumented)
export enum DependencyType {
// (undocumented)
Expand Down Expand Up @@ -285,6 +298,19 @@ export interface ICredentialCacheOptions {
supportEditing: boolean;
}

// @beta
export interface ICustomTipItemJson {
message: string;
messagePrefix?: string;
tipId: CustomTipId;
}

// @beta
export interface ICustomTipsJson {
customTips?: ICustomTipItemJson[];
defaultMessagePrefix?: string;
}

// @beta (undocumented)
export interface IEnvironmentConfigurationInitializeOptions {
// (undocumented)
Expand Down Expand Up @@ -844,6 +870,10 @@ export class RushConfiguration {
get commonVersions(): CommonVersionsConfiguration;
get currentInstalledVariant(): string | undefined;
readonly currentVariantJsonFilename: string;
// @beta
readonly customTipsConfiguration: CustomTipsConfiguration;
// @beta
readonly customTipsConfigurationFilePath: string;
readonly ensureConsistentVersions: boolean;
// @beta
readonly eventHooks: EventHooks;
Expand Down Expand Up @@ -977,6 +1007,7 @@ export class RushConstants {
static readonly commandLineFilename: string;
static readonly commonFolderName: string;
static readonly commonVersionsFilename: string;
static readonly customTipsFilename: string;
static readonly defaultMaxInstallAttempts: number;
static readonly defaultWatchDebounceMs: number;
static readonly experimentsFilename: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* This configuration file allows repo maintainers to configure extra details to be
* printed alongside certain Rush messages. More documentation is available on the
* Rush website: https://rushjs.io
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/custom-tips.schema.json",

/**
* If specified, this prefix will be prepended to any the tip messages when they are displayed.
* The default value is an empty string.
*/
/*[LINE "HYPOTHETICAL"]*/ "defaultMessagePrefix": "⭐️ Contoso monorepo tip: ",

/**
* Specifies the custom tips to be displayed by Rush.
*/
"customTips": [
/*[BEGIN "DEMO"]*/
{
/**
* (REQUIRED) An identifier indicating a message that may be printed by Rush.
* If that message is printed, then this custom tip will be shown.
* Consult the Rush documentation for the current list of possible identifiers.
*/
"tipId": "TIP_RUSH_INCONSISTENT_VERSIONS",

/**
* (REQUIRED) The message text to be displayed for this tip.
*/
"message": "For additional troubleshooting information, refer this wiki article:\n\nhttps://intranet.contoso.com/docs/pnpm-mismatch",

/**
* Overrides the "defaultMessagePrefix" for this tip.
* Specify an empty string to omit the "defaultMessagePrefix" entirely.
*/
// "messagePrefix": ""
}
/*[END "DEMO"]*/
]
}
166 changes: 166 additions & 0 deletions libraries/rush-lib/src/api/CustomTipsConfiguration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import * as path from 'path';
import { FileSystem, ITerminal, InternalError, JsonFile, JsonSchema } from '@rushstack/node-core-library';

import schemaJson from '../schemas/custom-tips.schema.json';

/**
* This interface represents the raw custom-tips.json file which allows repo maintainers
* to configure extra details to be printed alongside certain Rush messages.
* @beta
*/
export interface ICustomTipsJson {
/**
* If specified, this prefix will be prepended to any the tip messages when they are displayed.
* The default value is an empty string.
*/
defaultMessagePrefix?: string;
/**
* Specifies the custom tips to be displayed by Rush.
*/
customTips?: ICustomTipItemJson[];
}

/**
* An item from the {@link ICustomTipsJson.customTips} list.
* @beta
*/
export interface ICustomTipItemJson {
/**
* (REQUIRED) An identifier indicating a message that may be printed by Rush.
* If that message is printed, then this custom tip will be shown.
* Consult the Rush documentation for the current list of possible identifiers.
*/
tipId: CustomTipId;

/**
* (REQUIRED) The message text to be displayed for this tip.
*/
message: string;

/**
* Overrides the "defaultMessagePrefix" for this tip.
* Specify an empty string to omit the "defaultMessagePrefix" entirely.
*/
messagePrefix?: string;
}

/**
* An identifier representing a Rush message that can be customized by
* defining a custom tip in `common/config/rush/custom-tips.json`.
* @remarks
* Custom tip ids always start with the `TIP_` prefix.
*
* @privateRemarks
* Events from the Rush process should with "TIP_RUSH_".
* Events from a PNPM subprocess should start with "TIP_PNPM_".
*
* @beta
*/
export type CustomTipId =
// The rush.json "ensureConsistentVersions" validation.
| 'TIP_RUSH_INCONSISTENT_VERSIONS'
// In the future, plugins can contribute other strings.
| string;

/**
* Used to access the `common/config/rush/custom-tips.json` config file,
* which allows repo maintainers to configure extra details to be printed alongside
* certain Rush messages.
* @beta
*/
export class CustomTipsConfiguration {
private static _jsonSchema: JsonSchema = JsonSchema.fromLoadedObject(schemaJson);

private readonly _tipMap: Map<CustomTipId, ICustomTipItemJson>;
private readonly _jsonFileName: string;

/**
* The JSON settings loaded from `custom-tips.json`.
*/
public readonly configuration: Readonly<ICustomTipsJson>;

/**
* The list of identifiers that are allowed to be used in the "tipId" field
* of the config file.
*/
public static readonly supportedTipIds: ReadonlySet<string> = new Set<string>([
'TIP_RUSH_INCONSISTENT_VERSIONS'
]);

public constructor(configFilename: string) {
this._jsonFileName = configFilename;
this._tipMap = new Map();

if (!FileSystem.exists(this._jsonFileName)) {
this.configuration = {};
} else {
this.configuration = JsonFile.loadAndValidate(this._jsonFileName, CustomTipsConfiguration._jsonSchema);

const customTips: ICustomTipItemJson[] | undefined = this.configuration?.customTips;
if (customTips) {
for (const tipItem of customTips) {
if (!CustomTipsConfiguration.supportedTipIds.has(tipItem.tipId)) {
throw new Error(
`The ${path.basename(this._jsonFileName)} configuration` +
` references an unknown ID "${tipItem.tipId}"`
);
}
if (this._tipMap.has(tipItem.tipId)) {
throw new Error(
`The ${path.basename(this._jsonFileName)} configuration` +
` specifies a duplicate definition for "${tipItem.tipId}"`
);
}
this._tipMap.set(tipItem.tipId, tipItem);
}
}
}
}

private _formatTipMessage(tipId: CustomTipId): string | undefined {
if (!tipId.startsWith('TIP_')) {
throw new InternalError('Identifiers for custom tips must start with the "TIP_" prefix');
}

const customTipItem: ICustomTipItemJson | undefined = this._tipMap.get(tipId);
if (!customTipItem) {
return undefined;
}

const prefix: string | undefined = customTipItem.messagePrefix ?? this.configuration.defaultMessagePrefix;
return `${prefix ?? ''}${customTipItem.message}`;
}

/**
* If custom-tips.json defines a tip for the specified tipId,
* display the tip on the terminal.
*/
public showInfoTip(terminal: ITerminal, tipId: CustomTipId): void {
const message: string | undefined = this._formatTipMessage(tipId);
if (message !== undefined) {
terminal.writeLine(message);
}
}

/**
* If custom-tips.json defines a tip for the specified tipId,
* display the tip on the terminal.
*/
public showWarningTip(terminal: ITerminal, tipId: CustomTipId): void {
const message: string | undefined = this._formatTipMessage(tipId);
if (message !== undefined) {
terminal.writeWarningLine(message);
}
}

/**
* If custom-tips.json defines a tip for the specified tipId,
* display the tip on the terminal.
*/
public showErrorTip(terminal: ITerminal, tipId: CustomTipId): void {
const message: string | undefined = this._formatTipMessage(tipId);
if (message !== undefined) {
terminal.writeErrorLine(message);
}
}
}
20 changes: 20 additions & 0 deletions libraries/rush-lib/src/api/RushConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import schemaJson from '../schemas/rush.schema.json';

import type * as DependencyAnalyzerModuleType from '../logic/DependencyAnalyzer';
import { PackageManagerOptionsConfigurationBase } from '../logic/base/BasePackageManagerOptionsConfiguration';
import { CustomTipsConfiguration } from './CustomTipsConfiguration';

const MINIMUM_SUPPORTED_RUSH_JSON_VERSION: string = '0.0.0';
const DEFAULT_BRANCH: string = 'main';
Expand All @@ -59,6 +60,7 @@ const knownRushConfigFilenames: string[] = [
RushConstants.buildCacheFilename,
RushConstants.commandLineFilename,
RushConstants.commonVersionsFilename,
RushConstants.customTipsFilename,
RushConstants.experimentsFilename,
RushConstants.nonbrowserApprovedPackagesFilename,
RushConstants.pinnedVersionsFilename,
Expand Down Expand Up @@ -535,6 +537,18 @@ export class RushConfiguration {
*/
public readonly versionPolicyConfigurationFilePath: string;

/**
* Accesses the custom-tips.json configuration.
* @beta
*/
public readonly customTipsConfiguration: CustomTipsConfiguration;

/**
* The absolute path to the custom tips configuration file.
* @beta
*/
public readonly customTipsConfigurationFilePath: string;

/**
* This configuration object contains settings repo maintainers have specified to enable
* and disable experimental Rush features.
Expand Down Expand Up @@ -805,6 +819,12 @@ export class RushConfiguration {
);
this.versionPolicyConfiguration = new VersionPolicyConfiguration(this.versionPolicyConfigurationFilePath);

this.customTipsConfigurationFilePath = path.join(
this.commonRushConfigFolder,
RushConstants.customTipsFilename
);
this.customTipsConfiguration = new CustomTipsConfiguration(this.customTipsConfigurationFilePath);

this._variants = new Set<string>();

if (rushConfigurationJson.variants) {
Expand Down
16 changes: 16 additions & 0 deletions libraries/rush-lib/src/api/test/CustomTipsConfiguration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { CustomTipsConfiguration } from '../CustomTipsConfiguration';
import { RushConfiguration } from '../RushConfiguration';

describe(CustomTipsConfiguration.name, () => {
it('loads the config file (custom-tips.json)', () => {
const rushFilename: string = `${__dirname}/repo/rush-npm.json`;
const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile(rushFilename);
expect(rushConfiguration.customTipsConfiguration.configuration.customTips?.length).toBe(1);
});

it('reports an error for duplicate tips', () => {
expect(() => {
new CustomTipsConfiguration(`${__dirname}/jsonFiles/custom-tips.error.json`);
}).toThrowError('TIP_RUSH_INCONSISTENT_VERSIONS');
});
});
14 changes: 14 additions & 0 deletions libraries/rush-lib/src/api/test/jsonFiles/custom-tips.error.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/custom-tips.schema.json",

"customTips": [
{
"tipId": "TIP_RUSH_INCONSISTENT_VERSIONS",
"message": "duplicate 1"
},
{
"tipId": "TIP_RUSH_INCONSISTENT_VERSIONS",
"message": "duplicate 2"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/custom-tips.schema.json",

// The prefix will be prepended to all the custom message.
"defaultMessagePrefix": "⭐️ [Monorepo Infra team tip]: ",

"customTips": [
{
"tipId": "TIP_RUSH_INCONSISTENT_VERSIONS",
"message": "This is so wrong my friend. Please read this doc for more information: google.com"
}
]
}
Loading

0 comments on commit c745085

Please sign in to comment.