@oclif/multi-stage-output 0.7.9 → 0.7.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -28,6 +28,10 @@ type Info<T extends Record<string, unknown>> = {
|
|
|
28
28
|
* Set to `true` to prevent this key-value pair or message from being collapsed when the window is too short. Defaults to false.
|
|
29
29
|
*/
|
|
30
30
|
neverCollapse?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Set to `true` to only show this key-value pair or message at the very end of the CI output. Defaults to false.
|
|
33
|
+
*/
|
|
34
|
+
onlyShowAtEndInCI?: boolean;
|
|
31
35
|
};
|
|
32
36
|
export type KeyValuePair<T extends Record<string, unknown>> = Info<T> & {
|
|
33
37
|
/**
|
|
@@ -53,25 +53,44 @@ export type MultiStageOutputOptions<T extends Record<string, unknown>> = {
|
|
|
53
53
|
readonly jsonEnabled: boolean;
|
|
54
54
|
};
|
|
55
55
|
declare class CIMultiStageOutput<T extends Record<string, unknown>> {
|
|
56
|
+
private readonly completedStages;
|
|
56
57
|
private data?;
|
|
57
58
|
private readonly design;
|
|
58
59
|
private readonly hasElapsedTime?;
|
|
59
60
|
private readonly hasStageTime?;
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Amount of time (in milliseconds) between heartbeat updates
|
|
63
|
+
*/
|
|
64
|
+
private readonly heartbeat;
|
|
65
|
+
/**
|
|
66
|
+
* Time of the last heartbeat
|
|
67
|
+
*/
|
|
68
|
+
private lastHeartbeatTime;
|
|
69
|
+
/**
|
|
70
|
+
* Map of the last time a specific piece of info was updated. This is used for throttling messages
|
|
71
|
+
*/
|
|
72
|
+
private readonly lastUpdateByInfo;
|
|
62
73
|
private readonly postStagesBlock?;
|
|
63
74
|
private readonly preStagesBlock?;
|
|
64
|
-
private readonly
|
|
65
|
-
private readonly seenStages;
|
|
75
|
+
private readonly seenStrings;
|
|
66
76
|
private readonly stages;
|
|
67
77
|
private readonly stageSpecificBlock?;
|
|
68
78
|
private readonly startTime;
|
|
69
79
|
private readonly startTimes;
|
|
80
|
+
/**
|
|
81
|
+
* Amount of time (in milliseconds) between throttled updates
|
|
82
|
+
*/
|
|
83
|
+
private readonly throttle;
|
|
70
84
|
private readonly timerUnit;
|
|
85
|
+
/**
|
|
86
|
+
* Map of intervals used to trigger heartbeat updates
|
|
87
|
+
*/
|
|
88
|
+
private readonly updateIntervals;
|
|
71
89
|
constructor({ data, design, postStagesBlock, preStagesBlock, showElapsedTime, showStageTime, stageSpecificBlock, stages, timerUnit, title, }: MultiStageOutputOptions<T>);
|
|
72
90
|
stop(stageTracker: StageTracker): void;
|
|
73
91
|
update(stageTracker: StageTracker, data?: Partial<T>): void;
|
|
74
|
-
private
|
|
92
|
+
private maybePrintInfo;
|
|
93
|
+
private maybeStdout;
|
|
75
94
|
}
|
|
76
95
|
declare class MultiStageOutputBase<T extends Record<string, unknown>> implements Disposable {
|
|
77
96
|
protected readonly ciInstance: CIMultiStageOutput<T> | undefined;
|
|
@@ -33,21 +33,39 @@ function shouldUseCIMode() {
|
|
|
33
33
|
}
|
|
34
34
|
const isInCi = shouldUseCIMode();
|
|
35
35
|
class CIMultiStageOutput {
|
|
36
|
+
completedStages = new Set();
|
|
36
37
|
data;
|
|
37
38
|
design;
|
|
38
39
|
hasElapsedTime;
|
|
39
40
|
hasStageTime;
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Amount of time (in milliseconds) between heartbeat updates
|
|
43
|
+
*/
|
|
44
|
+
heartbeat = Number.parseInt(env.OCLIF_CI_HEARTBEAT_FREQUENCY_MS ?? env.SF_CI_HEARTBEAT_FREQUENCY_MS ?? '300000', 10) ?? 300_000;
|
|
45
|
+
/**
|
|
46
|
+
* Time of the last heartbeat
|
|
47
|
+
*/
|
|
48
|
+
lastHeartbeatTime;
|
|
49
|
+
/**
|
|
50
|
+
* Map of the last time a specific piece of info was updated. This is used for throttling messages
|
|
51
|
+
*/
|
|
52
|
+
lastUpdateByInfo = new Map();
|
|
42
53
|
postStagesBlock;
|
|
43
54
|
preStagesBlock;
|
|
44
|
-
|
|
45
|
-
seenStages = new Set();
|
|
55
|
+
seenStrings = new Set();
|
|
46
56
|
stages;
|
|
47
57
|
stageSpecificBlock;
|
|
48
58
|
startTime;
|
|
49
59
|
startTimes = new Map();
|
|
60
|
+
/**
|
|
61
|
+
* Amount of time (in milliseconds) between throttled updates
|
|
62
|
+
*/
|
|
63
|
+
throttle = Number.parseInt(env.OCLIF_CI_UPDATE_FREQUENCY_MS ?? env.SF_CI_UPDATE_FREQUENCY_MS ?? '5000', 10) ?? 5000;
|
|
50
64
|
timerUnit;
|
|
65
|
+
/**
|
|
66
|
+
* Map of intervals used to trigger heartbeat updates
|
|
67
|
+
*/
|
|
68
|
+
updateIntervals = new Map();
|
|
51
69
|
constructor({ data, design, postStagesBlock, preStagesBlock, showElapsedTime, showStageTime, stageSpecificBlock, stages, timerUnit, title, }) {
|
|
52
70
|
this.design = constructDesignParams(design);
|
|
53
71
|
this.stages = stages;
|
|
@@ -58,7 +76,7 @@ class CIMultiStageOutput {
|
|
|
58
76
|
this.stageSpecificBlock = stageSpecificBlock;
|
|
59
77
|
this.timerUnit = timerUnit ?? 'ms';
|
|
60
78
|
this.data = data;
|
|
61
|
-
this.
|
|
79
|
+
this.lastHeartbeatTime = Date.now();
|
|
62
80
|
if (title)
|
|
63
81
|
ux.stdout(`───── ${title} ─────`);
|
|
64
82
|
ux.stdout('Stages:');
|
|
@@ -73,20 +91,24 @@ class CIMultiStageOutput {
|
|
|
73
91
|
stop(stageTracker) {
|
|
74
92
|
this.update(stageTracker);
|
|
75
93
|
ux.stdout();
|
|
76
|
-
this.
|
|
77
|
-
this.
|
|
94
|
+
this.maybePrintInfo(this.preStagesBlock, 0, true);
|
|
95
|
+
this.maybePrintInfo(this.postStagesBlock, 0, true);
|
|
78
96
|
if (this.startTime) {
|
|
79
97
|
const elapsedTime = Date.now() - this.startTime;
|
|
80
98
|
ux.stdout();
|
|
81
99
|
const displayTime = readableTime(elapsedTime, this.timerUnit);
|
|
82
100
|
ux.stdout(`Elapsed time: ${displayTime}`);
|
|
83
101
|
}
|
|
102
|
+
for (const interval of this.updateIntervals.values()) {
|
|
103
|
+
clearInterval(interval);
|
|
104
|
+
}
|
|
84
105
|
}
|
|
106
|
+
// eslint-disable-next-line complexity
|
|
85
107
|
update(stageTracker, data) {
|
|
86
108
|
this.data = { ...this.data, ...data };
|
|
87
109
|
for (const [stage, status] of stageTracker.entries()) {
|
|
88
110
|
// no need to re-render completed, failed, or skipped stages
|
|
89
|
-
if (this.
|
|
111
|
+
if (this.completedStages.has(stage))
|
|
90
112
|
continue;
|
|
91
113
|
switch (status) {
|
|
92
114
|
case 'pending': {
|
|
@@ -96,13 +118,33 @@ class CIMultiStageOutput {
|
|
|
96
118
|
case 'current': {
|
|
97
119
|
if (!this.startTimes.has(stage))
|
|
98
120
|
this.startTimes.set(stage, Date.now());
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
121
|
+
const stageInfos = this.stageSpecificBlock?.filter((info) => info.stage === stage);
|
|
122
|
+
const iconAndStage = `${this.design.icons.current.figure} ${stage}…`;
|
|
123
|
+
if (Date.now() - this.lastHeartbeatTime < this.heartbeat) {
|
|
124
|
+
// only print if it hasn't been seen before
|
|
125
|
+
this.maybeStdout(iconAndStage);
|
|
126
|
+
this.maybePrintInfo(this.preStagesBlock, 3);
|
|
127
|
+
this.maybePrintInfo(stageInfos, 3);
|
|
128
|
+
this.maybePrintInfo(this.postStagesBlock, 3);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// force a reprint if it's been too long
|
|
132
|
+
this.lastHeartbeatTime = Date.now();
|
|
133
|
+
if (stageInfos?.length) {
|
|
134
|
+
// only reprint the stage infos if it has them
|
|
135
|
+
this.maybePrintInfo(stageInfos, 3, true);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// only reprint the stage
|
|
139
|
+
this.maybeStdout(iconAndStage, 0, true);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!this.updateIntervals.has(stage)) {
|
|
143
|
+
// set interval to update the stage message - this is used for long running stages in CI environments that timeout after a certain period without output
|
|
144
|
+
this.updateIntervals.set(stage, setInterval(() => {
|
|
145
|
+
this.update(stageTracker);
|
|
146
|
+
}, this.heartbeat));
|
|
147
|
+
}
|
|
106
148
|
break;
|
|
107
149
|
}
|
|
108
150
|
case 'failed':
|
|
@@ -112,24 +154,35 @@ class CIMultiStageOutput {
|
|
|
112
154
|
case 'async':
|
|
113
155
|
case 'warning':
|
|
114
156
|
case 'completed': {
|
|
115
|
-
|
|
157
|
+
// clear the heartbeat interval since it's no longer needed
|
|
158
|
+
const interval = this.updateIntervals.get(stage);
|
|
159
|
+
if (interval) {
|
|
160
|
+
clearInterval(interval);
|
|
161
|
+
this.updateIntervals.delete(stage);
|
|
162
|
+
}
|
|
163
|
+
// clear all throttled messages since the stage is done
|
|
164
|
+
for (const key of this.lastUpdateByInfo.keys()) {
|
|
165
|
+
this.lastUpdateByInfo.delete(key);
|
|
166
|
+
}
|
|
167
|
+
const stageInfos = this.stageSpecificBlock?.filter((info) => info.stage === stage);
|
|
168
|
+
this.completedStages.add(stage);
|
|
116
169
|
if (this.hasStageTime && status !== 'skipped') {
|
|
117
170
|
const startTime = this.startTimes.get(stage);
|
|
118
171
|
const elapsedTime = startTime ? Date.now() - startTime : 0;
|
|
119
172
|
const displayTime = readableTime(elapsedTime, this.timerUnit);
|
|
120
|
-
|
|
121
|
-
this.
|
|
122
|
-
this.
|
|
123
|
-
this.
|
|
173
|
+
this.maybeStdout(`${this.design.icons[status].figure} ${stage} (${displayTime})`);
|
|
174
|
+
this.maybePrintInfo(this.preStagesBlock, 3);
|
|
175
|
+
this.maybePrintInfo(stageInfos, 3);
|
|
176
|
+
this.maybePrintInfo(this.postStagesBlock, 3);
|
|
124
177
|
}
|
|
125
178
|
else if (status === 'skipped') {
|
|
126
|
-
|
|
179
|
+
this.maybeStdout(`${this.design.icons[status].figure} ${stage} - Skipped`);
|
|
127
180
|
}
|
|
128
181
|
else {
|
|
129
|
-
|
|
130
|
-
this.
|
|
131
|
-
this.
|
|
132
|
-
this.
|
|
182
|
+
this.maybeStdout(`${this.design.icons[status].figure} ${stage}`);
|
|
183
|
+
this.maybePrintInfo(this.preStagesBlock, 3);
|
|
184
|
+
this.maybePrintInfo(stageInfos, 3);
|
|
185
|
+
this.maybePrintInfo(this.postStagesBlock, 3);
|
|
133
186
|
}
|
|
134
187
|
break;
|
|
135
188
|
}
|
|
@@ -138,21 +191,34 @@ class CIMultiStageOutput {
|
|
|
138
191
|
}
|
|
139
192
|
}
|
|
140
193
|
}
|
|
141
|
-
|
|
142
|
-
const spaces = ' '.repeat(indent);
|
|
194
|
+
maybePrintInfo(infoBlock, indent = 0, force = false) {
|
|
143
195
|
if (infoBlock?.length) {
|
|
144
196
|
for (const info of infoBlock) {
|
|
197
|
+
if (info.onlyShowAtEndInCI && !force)
|
|
198
|
+
continue;
|
|
145
199
|
const formattedData = info.get ? info.get(this.data) : undefined;
|
|
146
200
|
if (!formattedData)
|
|
147
201
|
continue;
|
|
202
|
+
const key = info.type === 'message' ? formattedData : info.label;
|
|
148
203
|
const str = info.type === 'message' ? formattedData : `${info.label}: ${formattedData}`;
|
|
149
|
-
|
|
204
|
+
const lastUpdateTime = this.lastUpdateByInfo.get(key);
|
|
205
|
+
// Skip if the info has been printed before the throttle time
|
|
206
|
+
if (lastUpdateTime && Date.now() - lastUpdateTime < this.throttle && !force)
|
|
150
207
|
continue;
|
|
151
|
-
|
|
152
|
-
|
|
208
|
+
const didPrint = this.maybeStdout(str, indent, force);
|
|
209
|
+
if (didPrint)
|
|
210
|
+
this.lastUpdateByInfo.set(key, Date.now());
|
|
153
211
|
}
|
|
154
212
|
}
|
|
155
213
|
}
|
|
214
|
+
maybeStdout(str, indent = 0, force = false) {
|
|
215
|
+
const spaces = ' '.repeat(indent);
|
|
216
|
+
if (!force && this.seenStrings.has(str))
|
|
217
|
+
return false;
|
|
218
|
+
ux.stdout(`${spaces}${str}`);
|
|
219
|
+
this.seenStrings.add(str);
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
156
222
|
}
|
|
157
223
|
class MultiStageOutputBase {
|
|
158
224
|
ciInstance;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oclif/multi-stage-output",
|
|
3
3
|
"description": "Terminal output for oclif commands with multiple stages",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.11",
|
|
5
5
|
"author": "Salesforce",
|
|
6
6
|
"bugs": "https://github.com/oclif/multi-stage-output/issues",
|
|
7
7
|
"dependencies": {
|