/
CustomTipsConfiguration.ts
381 lines (346 loc) · 13 KB
/
CustomTipsConfiguration.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as path from 'path';
import { FileSystem, JsonFile, JsonSchema } from '@rushstack/node-core-library';
import { type ITerminal, PrintUtilities, Colorize } from '@rushstack/terminal';
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 {
/**
* 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;
}
/**
* 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 enum CustomTipId {
// Events from the Rush process should with "TIP_RUSH_".
TIP_RUSH_INCONSISTENT_VERSIONS = 'TIP_RUSH_INCONSISTENT_VERSIONS',
TIP_RUSH_DISALLOW_INSECURE_SHA1 = 'TIP_RUSH_DISALLOW_INSECURE_SHA1',
// Events from a PNPM subprocess should start with "TIP_PNPM_".
TIP_PNPM_UNEXPECTED_STORE = 'TIP_PNPM_UNEXPECTED_STORE',
TIP_PNPM_NO_MATCHING_VERSION = 'TIP_PNPM_NO_MATCHING_VERSION',
TIP_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE = 'TIP_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE',
TIP_PNPM_PEER_DEP_ISSUES = 'TIP_PNPM_PEER_DEP_ISSUES',
TIP_PNPM_OUTDATED_LOCKFILE = 'TIP_PNPM_OUTDATED_LOCKFILE',
TIP_PNPM_TARBALL_INTEGRITY = 'TIP_PNPM_TARBALL_INTEGRITY',
TIP_PNPM_MISMATCHED_RELEASE_CHANNEL = 'TIP_PNPM_MISMATCHED_RELEASE_CHANNEL',
TIP_PNPM_INVALID_NODE_VERSION = 'TIP_PNPM_INVALID_NODE_VERSION'
}
/**
* The severity of a custom tip.
* It determines the printing severity ("Error" = red, "Warning" = yellow, "Info" = normal).
*
* @beta
*/
export enum CustomTipSeverity {
Warning = 'Warning',
Error = 'Error',
Info = 'Info'
}
/**
* The type of the custom tip.
*
* @remarks
* There might be types like `git` in the future.
*
* @beta
*/
export enum CustomTipType {
rush = 'rush',
pnpm = 'pnpm'
}
/**
* Metadata for a custom tip.
*
* @remarks
* This differs from the {@link ICustomTipItemJson} interface in that these are not configurable by the user;
* it's the inherent state of a custom tip. For example, the custom tip for `ERR_PNPM_NO_MATCHING_VERSION`
* has a inherent severity of `Error`, and a inherent match function that rush maintainer defines.
*
* @beta
*/
export interface ICustomTipInfo {
tipId: CustomTipId;
/**
* The severity of the custom tip. It will determine the printing severity ("Error" = red, "Warning" = yellow, "Info" = normal).
*
* @remarks
* The severity should be consistent with the original message, unless there are strong reasons not to.
*/
severity: CustomTipSeverity;
/**
* The type of the custom tip.
*/
type: CustomTipType;
/**
* The function to determine how to match this tipId.
*
* @remarks
* This function might need to be updated if the depending package is updated.
* For example, if `pnpm` change the error logs for "ERR_PNPM_NO_MATCHING_VERSION", we will need to update the match function accordingly.
*/
isMatch?: (str: string) => boolean;
}
export const RUSH_CUSTOM_TIPS: Readonly<Record<`TIP_RUSH_${string}` & CustomTipId, ICustomTipInfo>> = {
[CustomTipId.TIP_RUSH_DISALLOW_INSECURE_SHA1]: {
tipId: CustomTipId.TIP_RUSH_DISALLOW_INSECURE_SHA1,
severity: CustomTipSeverity.Error,
type: CustomTipType.pnpm,
isMatch: (str: string) => {
return str.includes('ERR_PNPM_DISALLOW_INSECURE_SHA1');
}
},
[CustomTipId.TIP_RUSH_INCONSISTENT_VERSIONS]: {
tipId: CustomTipId.TIP_RUSH_INCONSISTENT_VERSIONS,
severity: CustomTipSeverity.Error,
type: CustomTipType.rush
}
};
export const PNPM_CUSTOM_TIPS: Readonly<Record<`TIP_PNPM_${string}` & CustomTipId, ICustomTipInfo>> = {
[CustomTipId.TIP_PNPM_UNEXPECTED_STORE]: {
tipId: CustomTipId.TIP_PNPM_UNEXPECTED_STORE,
severity: CustomTipSeverity.Error,
type: CustomTipType.pnpm,
isMatch: (str: string) => {
return str.includes('ERR_PNPM_UNEXPECTED_STORE');
}
},
[CustomTipId.TIP_PNPM_NO_MATCHING_VERSION]: {
tipId: CustomTipId.TIP_PNPM_NO_MATCHING_VERSION,
severity: CustomTipSeverity.Error,
type: CustomTipType.pnpm,
isMatch: (str: string) => {
// Example message: (do notice the difference between this one and the TIP_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE)
// Error Message: ERR_PNPM_NO_MATCHING_VERSION No matching version found for @babel/types@^7.22.5
// The latest release of @babel/types is "7.22.4".
// Other releases are:
// * esm: 7.21.4-esm.4
return str.includes('No matching version found for') && str.includes('The latest release of');
}
},
[CustomTipId.TIP_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE]: {
tipId: CustomTipId.TIP_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE,
severity: CustomTipSeverity.Error,
type: CustomTipType.pnpm,
isMatch: (str: string) => {
return str.includes('ERR_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE');
}
},
[CustomTipId.TIP_PNPM_PEER_DEP_ISSUES]: {
tipId: CustomTipId.TIP_PNPM_PEER_DEP_ISSUES,
severity: CustomTipSeverity.Error,
type: CustomTipType.pnpm,
isMatch: (str: string) => {
return str.includes('ERR_PNPM_PEER_DEP_ISSUES');
}
},
[CustomTipId.TIP_PNPM_OUTDATED_LOCKFILE]: {
tipId: CustomTipId.TIP_PNPM_OUTDATED_LOCKFILE,
severity: CustomTipSeverity.Error,
type: CustomTipType.pnpm,
isMatch: (str: string) => {
// Todo: verify this
return str.includes('ERR_PNPM_OUTDATED_LOCKFILE');
}
},
[CustomTipId.TIP_PNPM_TARBALL_INTEGRITY]: {
tipId: CustomTipId.TIP_PNPM_TARBALL_INTEGRITY,
severity: CustomTipSeverity.Error,
type: CustomTipType.pnpm,
isMatch: (str: string) => {
// Todo: verify this
return str.includes('ERR_PNPM_TARBALL_INTEGRITY');
}
},
[CustomTipId.TIP_PNPM_MISMATCHED_RELEASE_CHANNEL]: {
tipId: CustomTipId.TIP_PNPM_MISMATCHED_RELEASE_CHANNEL,
severity: CustomTipSeverity.Error,
type: CustomTipType.pnpm,
isMatch: (str: string) => {
// Todo: verify this
return str.includes('ERR_PNPM_MISMATCHED_RELEASE_CHANNEL');
}
},
[CustomTipId.TIP_PNPM_INVALID_NODE_VERSION]: {
tipId: CustomTipId.TIP_PNPM_INVALID_NODE_VERSION,
severity: CustomTipSeverity.Error,
type: CustomTipType.pnpm,
isMatch: (str: string) => {
// Todo: verify this
return str.includes('ERR_PNPM_INVALID_NODE_VERSION');
}
}
};
/**
* 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);
public readonly providedCustomTipsByTipId: ReadonlyMap<CustomTipId, ICustomTipItemJson>;
/**
* A registry mapping custom tip IDs to their corresponding metadata.
*
* @remarks
* This registry is used to look up metadata for custom tips based on their IDs. The metadata includes
* information such as the severity level, the type of tip, and an optional matching function.
*
* Each key in the registry corresponds to a `CustomTipIdEnum` value, and each value is an object
* implementing the `ICustomTipInfo` interface.
*
* @example
* ```typescript
* const tipInfo = CustomTipsConfiguration.customTipRegistry[CustomTipIdEnum.TIP_RUSH_INCONSISTENT_VERSIONS];
* console.log(tipInfo.severity); // Output: CustomTipSeverity.Error
* ```
*
* See {@link CustomTipId} for the list of custom tip IDs.
* See {@link ICustomTipInfo} for the structure of the metadata.
*/
public static customTipRegistry: Readonly<Record<CustomTipId, ICustomTipInfo>> = {
...RUSH_CUSTOM_TIPS,
...PNPM_CUSTOM_TIPS
};
public constructor(configFilePath: string) {
const providedCustomTips: Map<CustomTipId, ICustomTipItemJson> = new Map();
let configuration: ICustomTipsJson | undefined;
try {
configuration = JsonFile.loadAndValidate(configFilePath, CustomTipsConfiguration._jsonSchema);
} catch (e) {
if (!FileSystem.isNotExistError(e)) {
throw e;
}
}
const customTips: ICustomTipItemJson[] | undefined = configuration?.customTips;
if (customTips) {
for (const tipItem of customTips) {
if (!(tipItem.tipId in CustomTipId)) {
throw new Error(
`The ${path.basename(configFilePath)} configuration` +
` references an unknown ID "${tipItem.tipId}"`
);
}
if (providedCustomTips.has(tipItem.tipId)) {
throw new Error(
`The ${path.basename(configFilePath)} configuration` +
` specifies a duplicate definition for "${tipItem.tipId}"`
);
} else {
providedCustomTips.set(tipItem.tipId, tipItem);
}
}
}
this.providedCustomTipsByTipId = providedCustomTips;
}
/**
* If custom-tips.json defines a tip for the specified tipId, display the tip on the terminal.
*
* @remarks
* The severity of the tip is defined in ${@link CustomTipsConfiguration.customTipRegistry}.
* If you want to change the severity specifically for this call,
* use other APIs such as {@link CustomTipsConfiguration._showErrorTip}.
*
* Custom tips by design do not replace Rush's standard messaging; instead, they annotate Rush's
* output with additional team-specific advice.
*
* @internal
*/
public _showTip(terminal: ITerminal, tipId: CustomTipId): void {
const severityOfOriginalMessage: CustomTipSeverity =
CustomTipsConfiguration.customTipRegistry[tipId].severity;
this._writeMessageWithPipes(terminal, severityOfOriginalMessage, tipId);
}
/**
* If custom-tips.json defines a tip for the specified tipId, display the tip on the terminal.
* @remarks
* Custom tips by design do not replace Rush's standard messaging; instead, they annotate Rush's
* output with additional team-specific advice.
* @internal
*/
public _showInfoTip(terminal: ITerminal, tipId: CustomTipId): void {
this._writeMessageWithPipes(terminal, CustomTipSeverity.Info, tipId);
}
/**
* If custom-tips.json defines a tip for the specified tipId, display the tip on the terminal.
* @remarks
* Custom tips by design do not replace Rush's standard messaging; instead, they annotate Rush's
* output with additional team-specific advice.
* @internal
*/
public _showWarningTip(terminal: ITerminal, tipId: CustomTipId): void {
this._writeMessageWithPipes(terminal, CustomTipSeverity.Warning, tipId);
}
/**
* If custom-tips.json defines a tip for the specified tipId, display the tip on the terminal.
* @remarks
* Custom tips by design do not replace Rush's standard messaging; instead, they annotate Rush's
* output with additional team-specific advice.
* @internal
*/
public _showErrorTip(terminal: ITerminal, tipId: CustomTipId): void {
this._writeMessageWithPipes(terminal, CustomTipSeverity.Error, tipId);
}
private _writeMessageWithPipes(terminal: ITerminal, severity: CustomTipSeverity, tipId: CustomTipId): void {
const customTipJsonItem: ICustomTipItemJson | undefined = this.providedCustomTipsByTipId.get(tipId);
if (customTipJsonItem) {
let writeFunction:
| typeof terminal.writeErrorLine
| typeof terminal.writeWarningLine
| typeof terminal.writeLine;
let prefix: string;
switch (severity) {
case CustomTipSeverity.Error:
writeFunction = terminal.writeErrorLine.bind(terminal);
prefix = Colorize.red('| ');
break;
case CustomTipSeverity.Warning:
writeFunction = terminal.writeWarningLine.bind(terminal);
prefix = Colorize.yellow('| ');
break;
default:
writeFunction = terminal.writeLine.bind(terminal);
prefix = '| ';
break;
}
writeFunction(`| Custom Tip (${tipId})`);
writeFunction('|');
const message: string = customTipJsonItem.message;
const wrappedAndIndentedMessage: string = PrintUtilities.wrapWords(message, undefined, prefix);
writeFunction(...wrappedAndIndentedMessage, { doNotOverrideSgrCodes: true });
terminal.writeLine();
}
}
}