@oclif/multi-stage-output 0.3.3 → 0.4.0

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.
package/README.md CHANGED
@@ -8,6 +8,14 @@
8
8
 
9
9
  This is a framework for showing multi-stage output in the terminal. It's integrated with oclif's builtin [Performance](https://oclif.io/docs/performance/) capabilities so that perf metrics are automatically captured for each stage.
10
10
 
11
+ ![Demo](./assets/demo.gif?raw=true 'Demo')
12
+
13
+ # Features
14
+
15
+ - Integrated Performance Tracking: this is integrated with oclif's builtin [Performance](https://oclif.io/docs/performance/) capabilities so that perf metrics are automatically captured for each stage.
16
+ - Responsive Design: elements will be added or removed based on the height of the terminal window. It even resizes itself if you resize the screen while the command is running.
17
+ - CI Friendly Output: a simpler output will be shown inside non-tty environments like CI systems to avoid any excessive output that might be hard to read in CI logs.
18
+
11
19
  # Examples
12
20
 
13
21
  You can see examples of how to use it in the [examples](./examples/) directory.
@@ -18,6 +26,153 @@ You can run any of these with the following:
18
26
  node --loader=ts-node/esm examples/basic.ts
19
27
  ```
20
28
 
29
+ # Usage
30
+
31
+ ## Basic Usage
32
+
33
+ ```typescript
34
+ const ms = new MultiStageOutput({
35
+ jsonEnabled: false,
36
+ stages: ['stage 1', 'stage 2', 'stage 3'],
37
+ title: 'Basic Example',
38
+ })
39
+ // goto `stage 1`
40
+ ms.goto('stage 1')
41
+ // do some stuff
42
+
43
+ // goto `stage 2`
44
+ ms.goto('stage 2')
45
+
46
+ // As a convenience, use .next() to goto the next stage
47
+ ms.next()
48
+
49
+ // stop the multi-stage output from running anymore. Pass in an Error if applicable.
50
+ ms.stop()
51
+ ```
52
+
53
+ ## Adding information blocks before or after the stages output.
54
+
55
+ You can add blocks on information before or after the list of stages. There are 3 kinds of information that can be displayed:
56
+
57
+ - `message`: a simple string message
58
+ - `static-key-value`: a simple key:value pair, e.g. `name: Foo`. If the value is undefined, the key will not be shown.
59
+ - `dynamic-key-value`: a key:value pair where the value is expected to come in at an unknown time. This will display a spinner until the value is provided. If `.stop` is called with an error before the value is provided, then a `✘` will be displayed.
60
+
61
+ ```typescript
62
+ const ms = new MultiStageOutput<{message: string; staticValue: string; dynamicValue: string}>({
63
+ jsonEnabled: false,
64
+ stages: ['stage 1', 'stage 2', 'stage 3'],
65
+ // preStagesBlock will be displayed BEFORE the list of stages
66
+ postStagesBlock: [
67
+ {
68
+ get: (data) => data?.message,
69
+ type: 'message',
70
+ },
71
+ ],
72
+ // postStagesBlock will be displayed AFTER the list of stages
73
+ postStagesBlock: [
74
+ {
75
+ get: (data) => data?.staticValue,
76
+ label: 'Static',
77
+ type: 'static-key-value',
78
+ },
79
+ {
80
+ get: (data) => data?.dynamicValue,
81
+ label: 'Dynamic',
82
+ type: 'dynamic-key-value',
83
+ },
84
+ ],
85
+ })
86
+ // Goto `stage 1` and provide partial data to use for the information blocks
87
+ ms.goto('stage 1', {message: 'This is a message', staticValue: 'This is a static key:value pair'})
88
+
89
+ // Provide more data to use
90
+ ms.updateData({dynamicValue: 'This is a dynamic key:value pair'})
91
+
92
+ // Goto `stage 2` and provide more partial data
93
+ ms.goto('stage 2')
94
+
95
+ // Goto stage 3
96
+ ms.goto('stage 3')
97
+
98
+ ms.stop()
99
+ ```
100
+
101
+ ## Adding information blocks on a specific stage
102
+
103
+ You can also add information blocks onto specific stages, which will nest the information underneath the stage.
104
+
105
+ ```typescript
106
+ const ms = new MultiStageOutput<{message: string; staticValue: string; dynamicValue: string}>({
107
+ jsonEnabled: false,
108
+ stageSpecificBlock: [
109
+ // This will be nested underneath `stage 1`
110
+ {
111
+ get: (data) => data?.message,
112
+ stage: 'one',
113
+ type: 'message',
114
+ },
115
+ // This will be nested underneath `stage 2`
116
+ {
117
+ get: (data) => data?.staticValue,
118
+ label: 'Static',
119
+ stage: 'two',
120
+ type: 'static-key-value',
121
+ },
122
+ // This will be nested underneath `stage 1`
123
+ {
124
+ get: (data) => data?.dynamicValue,
125
+ label: 'Dynamic',
126
+ stage: 'one',
127
+ type: 'dynamic-key-value',
128
+ },
129
+ ],
130
+ stages: ['stage 1', 'stage 2', 'stage 3'],
131
+ title: 'Stage-Specific Information Block Example',
132
+ })
133
+
134
+ ms.goto('stage 1', {message: 'This is a message', staticValue: 'This is a static key:value pair'})
135
+
136
+ ms.goto('stage 2', {dynamicValue: 'This is a dynamic key:value pair'})
137
+
138
+ ms.goto('stage 3')
139
+
140
+ ms.stop()
141
+ ```
142
+
143
+ ## Customizing the Design
144
+
145
+ You can customize the design of the multi-stage output using the `design` property:
146
+
147
+ ```typescript
148
+ const ms = new MultiStageOutput({
149
+ jsonEnabled: false,
150
+ stages: ['stage 1', 'stage 2', 'stage 3'],
151
+ title: 'Basic Example',
152
+ design: {
153
+ title: {
154
+ textColor: 'red',
155
+ }
156
+ icons: {
157
+ completed: {
158
+ figure: 'C',
159
+ paddingLeft: 1,
160
+ color: '#00FF00'
161
+ }
162
+ }
163
+ }
164
+ })
165
+ ```
166
+
167
+ See [`Design`](./src/design.ts) for all the options available to you.
168
+
169
+ ## Other Options
170
+
171
+ - `showElapsedTime`: Optional. Whether or not to show the `Elapsed Time` at the bottom. Defaults to `true`
172
+ - `showStageTime`: Optional. Whether or not to show the time for each stage. Defaults to `true`
173
+ - `title`: Optional. The title to show at the top.
174
+ - `timerUnit`: The unit to use for the elapsed time and stage time. Can be `ms` or `s`. Defaults to `ms`
175
+
21
176
  # Contributing
22
177
 
23
178
  See the [contributing guide](./CONRTIBUTING.md).
@@ -1,5 +1,6 @@
1
1
  import { KeyValuePair, SimpleMessage, StageInfoBlock } from './components/stages.js';
2
2
  import { Design } from './design.js';
3
+ import { StageStatus } from './stage-tracker.js';
3
4
  export type MultiStageOutputOptions<T extends Record<string, unknown>> = {
4
5
  /**
5
6
  * Stages to render.
@@ -66,13 +67,61 @@ export declare class MultiStageOutput<T extends Record<string, unknown>> impleme
66
67
  private readonly timerUnit?;
67
68
  private readonly title?;
68
69
  constructor({ data, design, jsonEnabled, postStagesBlock, preStagesBlock, showElapsedTime, showStageTime, stageSpecificBlock, stages, timerUnit, title, }: MultiStageOutputOptions<T>);
70
+ /**
71
+ * Stop multi-stage output from running with a failed status.
72
+ */
73
+ error(): void;
74
+ /**
75
+ * Go to a stage, marking any stages in between the current stage and the provided stage as completed.
76
+ *
77
+ * If the stage does not exist or is before the current stage, nothing will happen.
78
+ *
79
+ * If the stage is the same as the current stage, the data will be updated.
80
+ *
81
+ * @param stage Stage to go to
82
+ * @param data - Optional data to pass to the next stage.
83
+ * @returns void
84
+ */
69
85
  goto(stage: string, data?: Partial<T>): void;
86
+ /**
87
+ * Moves to the next stage of the process.
88
+ *
89
+ * @param data - Optional data to pass to the next stage.
90
+ * @returns void
91
+ */
70
92
  next(data?: Partial<T>): void;
71
- stop(error?: Error): void;
93
+ /**
94
+ * Go to a stage, marking any stages in between the current stage and the provided stage as skipped.
95
+ *
96
+ * If the stage does not exist or is before the current stage, nothing will happen.
97
+ *
98
+ * If the stage is the same as the current stage, the data will be updated.
99
+ *
100
+ * @param stage Stage to go to
101
+ * @param data - Optional data to pass to the next stage.
102
+ * @returns void
103
+ */
104
+ skipTo(stage: string, data?: Partial<T>): void;
105
+ /**
106
+ * Stop multi-stage output from running.
107
+ *
108
+ * The stage currently running will be changed to the provided `finalStatus`.
109
+ *
110
+ * @param finalStatus - The status to set the current stage to.
111
+ * @returns void
112
+ */
113
+ stop(finalStatus?: StageStatus): void;
72
114
  [Symbol.dispose](): void;
115
+ /**
116
+ * Updates the data of the component.
117
+ *
118
+ * @param data - The partial data object to update the component's data with.
119
+ * @returns void
120
+ */
73
121
  updateData(data: Partial<T>): void;
74
122
  private formatKeyValuePairs;
75
123
  /** shared method to populate everything needed for Stages cmp */
76
124
  private generateStagesInput;
125
+ private rerender;
77
126
  private update;
78
127
  }
@@ -177,6 +177,23 @@ export class MultiStageOutput {
177
177
  this.inkInstance = render(React.createElement(Stages, { ...this.generateStagesInput() }));
178
178
  }
179
179
  }
180
+ /**
181
+ * Stop multi-stage output from running with a failed status.
182
+ */
183
+ error() {
184
+ this.stop('failed');
185
+ }
186
+ /**
187
+ * Go to a stage, marking any stages in between the current stage and the provided stage as completed.
188
+ *
189
+ * If the stage does not exist or is before the current stage, nothing will happen.
190
+ *
191
+ * If the stage is the same as the current stage, the data will be updated.
192
+ *
193
+ * @param stage Stage to go to
194
+ * @param data - Optional data to pass to the next stage.
195
+ * @returns void
196
+ */
180
197
  goto(stage, data) {
181
198
  if (this.stopped)
182
199
  return;
@@ -186,25 +203,67 @@ export class MultiStageOutput {
186
203
  // prevent going to a previous stage
187
204
  if (this.stages.indexOf(stage) < this.stages.indexOf(this.stageTracker.current ?? this.stages[0]))
188
205
  return;
189
- this.update(stage, data);
206
+ this.update(stage, 'completed', data);
190
207
  }
208
+ /**
209
+ * Moves to the next stage of the process.
210
+ *
211
+ * @param data - Optional data to pass to the next stage.
212
+ * @returns void
213
+ */
191
214
  next(data) {
192
215
  if (this.stopped)
193
216
  return;
194
217
  const nextStageIndex = this.stages.indexOf(this.stageTracker.current ?? this.stages[0]) + 1;
195
218
  if (nextStageIndex < this.stages.length) {
196
- this.update(this.stages[nextStageIndex], data);
219
+ this.update(this.stages[nextStageIndex], 'completed', data);
197
220
  }
198
221
  }
199
- stop(error) {
222
+ /**
223
+ * Go to a stage, marking any stages in between the current stage and the provided stage as skipped.
224
+ *
225
+ * If the stage does not exist or is before the current stage, nothing will happen.
226
+ *
227
+ * If the stage is the same as the current stage, the data will be updated.
228
+ *
229
+ * @param stage Stage to go to
230
+ * @param data - Optional data to pass to the next stage.
231
+ * @returns void
232
+ */
233
+ skipTo(stage, data) {
234
+ if (this.stopped)
235
+ return;
236
+ // ignore non-existent stages
237
+ if (!this.stages.includes(stage))
238
+ return;
239
+ // prevent going to a previous stage
240
+ if (this.stages.indexOf(stage) < this.stages.indexOf(this.stageTracker.current ?? this.stages[0]))
241
+ return;
242
+ this.update(stage, 'skipped', data);
243
+ }
244
+ /**
245
+ * Stop multi-stage output from running.
246
+ *
247
+ * The stage currently running will be changed to the provided `finalStatus`.
248
+ *
249
+ * @param finalStatus - The status to set the current stage to.
250
+ * @returns void
251
+ */
252
+ stop(finalStatus = 'completed') {
200
253
  if (this.stopped)
201
254
  return;
202
255
  this.stopped = true;
203
- this.stageTracker.refresh(this.stageTracker.current ?? this.stages[0], { hasError: Boolean(error), isStopping: true });
256
+ this.stageTracker.refresh(this.stageTracker.current ?? this.stages[0], {
257
+ finalStatus,
258
+ });
204
259
  if (isInCi) {
205
260
  this.ciInstance?.stop(this.stageTracker);
206
261
  return;
207
262
  }
263
+ // The underlying components expect an Error, although they don't currently use anything on the error - they check if it exists.
264
+ // Instead of refactoring the components to take a boolean, we pass in a placeholder Error,
265
+ // which, gives us the flexibility in the future to pass in an actual Error if we want
266
+ const error = finalStatus === 'failed' ? new Error('Error') : undefined;
208
267
  const stagesInput = { ...this.generateStagesInput(), ...(error ? { error } : {}) };
209
268
  this.inkInstance?.rerender(React.createElement(Stages, { ...stagesInput }));
210
269
  this.inkInstance?.unmount();
@@ -212,11 +271,17 @@ export class MultiStageOutput {
212
271
  [Symbol.dispose]() {
213
272
  this.inkInstance?.unmount();
214
273
  }
274
+ /**
275
+ * Updates the data of the component.
276
+ *
277
+ * @param data - The partial data object to update the component's data with.
278
+ * @returns void
279
+ */
215
280
  updateData(data) {
216
281
  if (this.stopped)
217
282
  return;
218
283
  this.data = { ...this.data, ...data };
219
- this.update(this.stageTracker.current ?? this.stages[0], data);
284
+ this.rerender();
220
285
  }
221
286
  formatKeyValuePairs(infoBlock) {
222
287
  return (infoBlock?.map((info) => {
@@ -245,9 +310,7 @@ export class MultiStageOutput {
245
310
  title: this.title,
246
311
  };
247
312
  }
248
- update(stage, data) {
249
- this.data = { ...this.data, ...data };
250
- this.stageTracker.refresh(stage);
313
+ rerender() {
251
314
  if (isInCi) {
252
315
  this.ciInstance?.update(this.stageTracker, this.data);
253
316
  }
@@ -255,4 +318,9 @@ export class MultiStageOutput {
255
318
  this.inkInstance?.rerender(React.createElement(Stages, { ...this.generateStagesInput() }));
256
319
  }
257
320
  }
321
+ update(stage, bypassStatus, data) {
322
+ this.data = { ...this.data, ...data };
323
+ this.stageTracker.refresh(stage, { bypassStatus });
324
+ this.rerender();
325
+ }
258
326
  }
@@ -7,8 +7,8 @@ export declare class StageTracker {
7
7
  entries(): IterableIterator<[string, StageStatus]>;
8
8
  get(stage: string): StageStatus | undefined;
9
9
  refresh(nextStage: string, opts?: {
10
- hasError?: boolean;
11
- isStopping?: boolean;
10
+ finalStatus?: StageStatus;
11
+ bypassStatus?: StageStatus;
12
12
  }): void;
13
13
  set(stage: string, status: StageStatus): void;
14
14
  values(): IterableIterator<StageStatus>;
@@ -19,15 +19,9 @@ export class StageTracker {
19
19
  continue;
20
20
  if (this.map.get(stage) === 'failed')
21
21
  continue;
22
- // .stop() was called with an error => set the stage to failed
23
- if (nextStage === stage && opts?.hasError) {
24
- this.set(stage, 'failed');
25
- this.stopMarker(stage);
26
- continue;
27
- }
28
- // .stop() was called without an error => set the stage to completed
29
- if (nextStage === stage && opts?.isStopping) {
30
- this.set(stage, 'completed');
22
+ // .stop() was called with a finalStatus
23
+ if (nextStage === stage && opts?.finalStatus) {
24
+ this.set(stage, opts.finalStatus);
31
25
  this.stopMarker(stage);
32
26
  continue;
33
27
  }
@@ -40,12 +34,12 @@ export class StageTracker {
40
34
  }
41
35
  continue;
42
36
  }
43
- // any stage before the current stage should be marked as skipped if it's still pending
37
+ // any pending stage before the current stage should be marked using opts.bypassStatus
44
38
  if (stages.indexOf(stage) < stages.indexOf(nextStage) && this.map.get(stage) === 'pending') {
45
- this.set(stage, 'skipped');
39
+ this.set(stage, opts?.bypassStatus ?? 'completed');
46
40
  continue;
47
41
  }
48
- // any stage before the current stage should be as completed (if it hasn't been marked as skipped or failed yet)
42
+ // any stage before the current stage should be marked as completed (if it hasn't been marked as skipped or failed yet)
49
43
  if (stages.indexOf(nextStage) > stages.indexOf(stage)) {
50
44
  this.set(stage, 'completed');
51
45
  this.stopMarker(stage);
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.3.3",
4
+ "version": "0.4.0",
5
5
  "author": "Salesforce",
6
6
  "bugs": "https://github.com/oclif/multi-stage-output/issues",
7
7
  "dependencies": {
@@ -30,7 +30,7 @@
30
30
  "eslint-config-xo-react": "^0.27.0",
31
31
  "eslint-plugin-react": "^7.34.3",
32
32
  "eslint-plugin-react-hooks": "^4.6.2",
33
- "husky": "^9.1.3",
33
+ "husky": "^9.1.5",
34
34
  "ink-testing-library": "^4.0.0",
35
35
  "lint-staged": "^15",
36
36
  "mocha": "^10.7.3",