@oclif/multi-stage-output 0.1.4 → 0.2.1

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.
@@ -1,10 +1,10 @@
1
1
  import React from 'react';
2
- export declare function Divider({ dividerChar, dividerColor, padding, title, titleColor, titlePadding, width, }: {
2
+ export declare function Divider({ dividerChar, dividerColor, padding, textColor, textPadding: titlePadding, title, width, }: {
3
3
  readonly title?: string;
4
4
  readonly width?: number | 'full';
5
5
  readonly padding?: number;
6
- readonly titleColor?: string;
7
- readonly titlePadding?: number;
6
+ readonly textColor?: string;
7
+ readonly textPadding?: number;
8
8
  readonly dividerChar?: string;
9
9
  readonly dividerColor?: string;
10
10
  }): React.ReactNode;
@@ -3,7 +3,7 @@ import React from 'react';
3
3
  const getSideDividerWidth = (width, titleWidth) => (width - titleWidth) / 2;
4
4
  const getNumberOfCharsPerWidth = (char, width) => width / char.length;
5
5
  const PAD = ' ';
6
- export function Divider({ dividerChar = '─', dividerColor = 'dim', padding = 1, title = '', titleColor = 'white', titlePadding = 1, width = 50, }) {
6
+ export function Divider({ dividerChar = '─', dividerColor = 'dim', padding = 1, textColor = 'white', textPadding: titlePadding = 1, title = '', width = 50, }) {
7
7
  const titleString = title ? `${PAD.repeat(titlePadding) + title + PAD.repeat(titlePadding)}` : '';
8
8
  const titleWidth = titleString.length;
9
9
  const terminalWidth = process.stdout.columns ?? 80;
@@ -16,7 +16,7 @@ export function Divider({ dividerChar = '─', dividerColor = 'dim', padding = 1
16
16
  React.createElement(Text, null,
17
17
  paddingString,
18
18
  React.createElement(Text, { color: dividerColor }, dividerSideString),
19
- React.createElement(Text, { color: titleColor }, titleString),
19
+ React.createElement(Text, { color: textColor }, titleString),
20
20
  React.createElement(Text, { color: dividerColor }, dividerSideString),
21
21
  paddingString)));
22
22
  }
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ export type IconProps = {
3
+ figure?: string;
4
+ paddingLeft?: number;
5
+ paddingRight?: number;
6
+ color?: string | false;
7
+ };
8
+ export declare function Icon({ children, icon, }: {
9
+ readonly children?: React.ReactNode;
10
+ readonly icon: IconProps;
11
+ }): React.ReactNode;
@@ -0,0 +1,11 @@
1
+ import { Box, Text } from 'ink';
2
+ import React from 'react';
3
+ export function Icon({ children, icon, }) {
4
+ if (!icon)
5
+ return false;
6
+ return (React.createElement(Box, null,
7
+ React.createElement(Box, { paddingLeft: icon.paddingLeft, paddingRight: icon.paddingRight },
8
+ icon.color && React.createElement(Text, { color: icon.color }, icon.figure),
9
+ !icon.color && React.createElement(Text, null, icon.figure)),
10
+ React.createElement(Box, null, children)));
11
+ }
@@ -1,6 +1,7 @@
1
1
  import { type SpinnerName } from 'cli-spinners';
2
2
  import React from 'react';
3
- export type UseSpinnerProps = {
3
+ import { IconProps } from './icon.js';
4
+ type UseSpinnerProps = {
4
5
  /**
5
6
  * Type of a spinner.
6
7
  * See [cli-spinners](https://github.com/sindresorhus/cli-spinners) for available spinners.
@@ -9,11 +10,7 @@ export type UseSpinnerProps = {
9
10
  */
10
11
  readonly type?: SpinnerName;
11
12
  };
12
- export type UseSpinnerResult = {
13
- frame: string;
14
- };
15
- export declare function useSpinner({ type }: UseSpinnerProps): UseSpinnerResult;
16
- export type SpinnerProps = UseSpinnerProps & {
13
+ type SpinnerProps = UseSpinnerProps & {
17
14
  /**
18
15
  * Label to show near the spinner.
19
16
  */
@@ -22,10 +19,13 @@ export type SpinnerProps = UseSpinnerProps & {
22
19
  readonly labelPosition?: 'left' | 'right';
23
20
  };
24
21
  export declare function Spinner({ isBold, label, labelPosition, type }: SpinnerProps): React.ReactElement;
25
- export declare function SpinnerOrError({ error, labelPosition, ...props }: SpinnerProps & {
22
+ export declare function SpinnerOrError({ error, failedIcon, labelPosition, ...props }: SpinnerProps & {
26
23
  readonly error?: Error;
24
+ readonly failedIcon: IconProps;
27
25
  }): React.ReactElement;
28
26
  export declare function SpinnerOrErrorOrChildren({ children, error, ...props }: SpinnerProps & {
29
27
  readonly children?: React.ReactNode;
30
28
  readonly error?: Error;
29
+ readonly failedIcon: IconProps;
31
30
  }): React.ReactElement;
31
+ export {};
@@ -1,8 +1,8 @@
1
1
  import spinners from 'cli-spinners';
2
2
  import { Box, Text } from 'ink';
3
3
  import React, { useEffect, useState } from 'react';
4
- import { icons } from '../design-elements.js';
5
- export function useSpinner({ type = 'dots' }) {
4
+ import { Icon } from './icon.js';
5
+ function useSpinner({ type = 'dots' }) {
6
6
  const [frame, setFrame] = useState(0);
7
7
  const spinner = spinners[type];
8
8
  useEffect(() => {
@@ -31,13 +31,13 @@ export function Spinner({ isBold, label, labelPosition = 'right', type }) {
31
31
  " ",
32
32
  label)));
33
33
  }
34
- export function SpinnerOrError({ error, labelPosition = 'right', ...props }) {
34
+ export function SpinnerOrError({ error, failedIcon, labelPosition = 'right', ...props }) {
35
35
  if (error) {
36
36
  return (React.createElement(Box, null,
37
37
  props.label && labelPosition === 'left' && React.createElement(Text, null,
38
38
  props.label,
39
39
  " "),
40
- React.createElement(Text, { color: "red" }, icons.failed),
40
+ React.createElement(Icon, { icon: failedIcon }),
41
41
  props.label && labelPosition === 'right' && React.createElement(Text, null,
42
42
  " ",
43
43
  props.label)));
@@ -0,0 +1,65 @@
1
+ import React from 'react';
2
+ import { RequiredDesign } from '../design.js';
3
+ import { StageTracker } from '../stage-tracker.js';
4
+ type Info<T extends Record<string, unknown>> = {
5
+ /**
6
+ * key-value: Display a key-value pair with a spinner.
7
+ * static-key-value: Display a key-value pair without a spinner.
8
+ * message: Display a message.
9
+ */
10
+ type: 'dynamic-key-value' | 'static-key-value' | 'message';
11
+ /**
12
+ * Color of the value.
13
+ */
14
+ color?: string;
15
+ /**
16
+ * Get the value to display. Takes the data property on the MultiStageComponent as an argument.
17
+ * Useful if you want to apply some logic (like rendering a link) to the data before displaying it.
18
+ *
19
+ * @param data The data property on the MultiStageComponent.
20
+ * @returns {string | undefined}
21
+ */
22
+ get: (data?: T) => string | undefined;
23
+ /**
24
+ * Whether the value should be bold.
25
+ */
26
+ bold?: boolean;
27
+ };
28
+ export type KeyValuePair<T extends Record<string, unknown>> = Info<T> & {
29
+ /**
30
+ * Label of the key-value pair.
31
+ */
32
+ label: string;
33
+ type: 'dynamic-key-value' | 'static-key-value';
34
+ };
35
+ export type SimpleMessage<T extends Record<string, unknown>> = Info<T> & {
36
+ type: 'message';
37
+ };
38
+ export type InfoBlock<T extends Record<string, unknown>> = Array<KeyValuePair<T> | SimpleMessage<T>>;
39
+ export type StageInfoBlock<T extends Record<string, unknown>> = Array<(KeyValuePair<T> & {
40
+ stage: string;
41
+ }) | (SimpleMessage<T> & {
42
+ stage: string;
43
+ })>;
44
+ export type FormattedKeyValue = {
45
+ readonly color?: string;
46
+ readonly isBold?: boolean;
47
+ readonly label?: string;
48
+ readonly value: string | undefined;
49
+ readonly stage?: string;
50
+ readonly type: 'dynamic-key-value' | 'static-key-value' | 'message';
51
+ };
52
+ export type StagesProps = {
53
+ readonly design?: RequiredDesign;
54
+ readonly error?: Error | undefined;
55
+ readonly postStagesBlock?: FormattedKeyValue[];
56
+ readonly preStagesBlock?: FormattedKeyValue[];
57
+ readonly stageSpecificBlock?: FormattedKeyValue[];
58
+ readonly title?: string;
59
+ readonly hasElapsedTime?: boolean;
60
+ readonly hasStageTime?: boolean;
61
+ readonly timerUnit?: 'ms' | 's';
62
+ readonly stageTracker: StageTracker;
63
+ };
64
+ export declare function Stages({ design, error, hasElapsedTime, hasStageTime, postStagesBlock, preStagesBlock, stageSpecificBlock, stageTracker, timerUnit, title, }: StagesProps): React.ReactNode;
65
+ export {};
@@ -0,0 +1,86 @@
1
+ import { capitalCase } from 'change-case';
2
+ import { Box, Text } from 'ink';
3
+ import React from 'react';
4
+ import { constructDesignParams } from '../design.js';
5
+ import { Divider } from './divider.js';
6
+ import { Icon } from './icon.js';
7
+ import { SpinnerOrError, SpinnerOrErrorOrChildren } from './spinner.js';
8
+ import { Timer } from './timer.js';
9
+ function StaticKeyValue({ color, isBold, label, value }) {
10
+ if (!value)
11
+ return false;
12
+ return (React.createElement(Box, { key: label },
13
+ React.createElement(Text, { bold: isBold },
14
+ label,
15
+ ": "),
16
+ React.createElement(Text, { color: color }, value)));
17
+ }
18
+ function SimpleMessage({ color, isBold, value }) {
19
+ if (!value)
20
+ return false;
21
+ return (React.createElement(Text, { bold: isBold, color: color }, value));
22
+ }
23
+ function StageInfos({ design, error, keyValuePairs, stage, }) {
24
+ return keyValuePairs
25
+ .filter((kv) => kv.stage === stage)
26
+ .map((kv) => {
27
+ const key = `${kv.label}-${kv.value}`;
28
+ if (kv.type === 'message') {
29
+ return (React.createElement(Box, { key: key, flexDirection: "row" },
30
+ React.createElement(Icon, { icon: design.icons.info }),
31
+ React.createElement(SimpleMessage, { ...kv })));
32
+ }
33
+ if (kv.type === 'dynamic-key-value') {
34
+ return (React.createElement(Box, { key: key },
35
+ React.createElement(Icon, { icon: design.icons.info }),
36
+ React.createElement(SpinnerOrErrorOrChildren, { error: error, label: `${kv.label}:`, labelPosition: "left", type: design.spinners.info, failedIcon: design.icons.failed }, kv.value && (React.createElement(Text, { bold: kv.isBold, color: kv.color }, kv.value)))));
37
+ }
38
+ if (kv.type === 'static-key-value') {
39
+ return (React.createElement(Box, { key: key },
40
+ React.createElement(Icon, { icon: design.icons.info }),
41
+ React.createElement(StaticKeyValue, { key: key, ...kv })));
42
+ }
43
+ return false;
44
+ });
45
+ }
46
+ function Infos({ design, error, keyValuePairs, }) {
47
+ return keyValuePairs.map((kv) => {
48
+ const key = `${kv.label}-${kv.value}`;
49
+ if (kv.type === 'message') {
50
+ return React.createElement(SimpleMessage, { key: key, ...kv });
51
+ }
52
+ if (kv.type === 'dynamic-key-value') {
53
+ return (React.createElement(SpinnerOrErrorOrChildren, { key: key, error: error, label: `${kv.label}:`, labelPosition: "left", type: design.spinners.info, failedIcon: design.icons.failed }, kv.value && (React.createElement(Text, { bold: kv.isBold, color: kv.color }, kv.value))));
54
+ }
55
+ if (kv.type === 'static-key-value') {
56
+ return React.createElement(StaticKeyValue, { key: key, ...kv });
57
+ }
58
+ return false;
59
+ });
60
+ }
61
+ export function Stages({ design = constructDesignParams(), error, hasElapsedTime = true, hasStageTime = true, postStagesBlock, preStagesBlock, stageSpecificBlock, stageTracker, timerUnit = 'ms', title, }) {
62
+ return (React.createElement(Box, { flexDirection: "column", paddingTop: 1, paddingBottom: 1 },
63
+ title && React.createElement(Divider, { title: title, ...design.title }),
64
+ preStagesBlock && preStagesBlock.length > 0 && (React.createElement(Box, { flexDirection: "column", marginLeft: 1, paddingTop: 1 },
65
+ React.createElement(Infos, { design: design, error: error, keyValuePairs: preStagesBlock }))),
66
+ React.createElement(Box, { flexDirection: "column", marginLeft: 1, paddingTop: 1 }, [...stageTracker.entries()].map(([stage, status]) => (React.createElement(Box, { key: stage, flexDirection: "column" },
67
+ React.createElement(Box, null,
68
+ (status === 'current' || status === 'failed') && (React.createElement(SpinnerOrError, { error: error, label: capitalCase(stage), type: design.spinners.stage, failedIcon: design.icons.failed })),
69
+ status === 'skipped' && (React.createElement(Icon, { icon: design.icons.skipped },
70
+ React.createElement(Text, { color: "dim" },
71
+ capitalCase(stage),
72
+ " - Skipped"))),
73
+ status === 'completed' && (React.createElement(Icon, { icon: design.icons.completed },
74
+ React.createElement(Text, null, capitalCase(stage)))),
75
+ status === 'pending' && (React.createElement(Icon, { icon: design.icons.pending },
76
+ React.createElement(Text, null, capitalCase(stage)))),
77
+ status !== 'pending' && status !== 'skipped' && hasStageTime && (React.createElement(Box, null,
78
+ React.createElement(Text, null, " "),
79
+ React.createElement(Timer, { color: "dim", isStopped: status === 'completed', unit: timerUnit })))),
80
+ stageSpecificBlock && stageSpecificBlock.length > 0 && status !== 'pending' && status !== 'skipped' && (React.createElement(StageInfos, { design: design, error: error, keyValuePairs: stageSpecificBlock, stage: stage })))))),
81
+ postStagesBlock && postStagesBlock.length > 0 && (React.createElement(Box, { flexDirection: "column", marginLeft: 1, paddingTop: 1 },
82
+ React.createElement(Infos, { design: design, error: error, keyValuePairs: postStagesBlock }))),
83
+ hasElapsedTime && (React.createElement(Box, { marginLeft: 1 },
84
+ React.createElement(Text, null, "Elapsed Time: "),
85
+ React.createElement(Timer, { unit: timerUnit })))));
86
+ }
@@ -0,0 +1,76 @@
1
+ import { type SpinnerName } from 'cli-spinners';
2
+ import { IconProps } from './components/icon.js';
3
+ export type Design = {
4
+ icons?: {
5
+ /**
6
+ * Icon to display for a completed stage. Defaults to '✔'
7
+ */
8
+ completed?: IconProps;
9
+ /**
10
+ * Icon to display for the current stage in CI environments. Defaults to '▶'
11
+ *
12
+ * Non-CI environments will display the spinner instead.
13
+ */
14
+ current?: IconProps;
15
+ /**
16
+ * Icon to display for a failed stage. Defaults to '✘'
17
+ */
18
+ failed?: IconProps;
19
+ /**
20
+ * Icon to display for a pending stage. Defaults to '◼'
21
+ */
22
+ pending?: IconProps;
23
+ /**
24
+ * Icon to display for a skipped stage. Defaults to '◯'
25
+ */
26
+ skipped?: IconProps;
27
+ /**
28
+ * Icon to display for stage specific information. Defaults to '▸'
29
+ */
30
+ info?: IconProps;
31
+ };
32
+ title?: {
33
+ /**
34
+ * Character to use as a divider for the title. Defaults to '─'
35
+ */
36
+ dividerChar?: string;
37
+ /**
38
+ * Color of the divider. Defaults to 'dim'
39
+ */
40
+ dividerColor?: string;
41
+ /**
42
+ * Padding to add above and below the title. Defaults to 1
43
+ */
44
+ padding?: number;
45
+ /**
46
+ * Color of the title. Defaults to 'white'
47
+ */
48
+ textColor?: string;
49
+ /**
50
+ * Padding to add to the left and right of the title. Defaults to 1
51
+ */
52
+ textPadding?: number;
53
+ /**
54
+ * Width of the title. Defaults to 50
55
+ *
56
+ * The `full` value will use the terminal width minus the title padding.
57
+ */
58
+ width?: number | 'full';
59
+ };
60
+ spinners?: {
61
+ /**
62
+ * Spinner to display for dynamic info blocks. Defaults to 'line' on Windows and 'arc' on other platforms
63
+ */
64
+ info?: SpinnerName;
65
+ /**
66
+ * Spinner to display for stages. Defaults to 'line' on Windows and 'dots2' on other platforms
67
+ */
68
+ stage?: SpinnerName;
69
+ };
70
+ };
71
+ type RecursiveRequired<T> = Required<{
72
+ [P in keyof T]: T[P] extends object | undefined ? RecursiveRequired<Required<T[P]>> : T[P];
73
+ }>;
74
+ export type RequiredDesign = RecursiveRequired<Design>;
75
+ export declare function constructDesignParams(design?: Design): RequiredDesign;
76
+ export {};
package/lib/design.js ADDED
@@ -0,0 +1,63 @@
1
+ import figures from 'figures';
2
+ export function constructDesignParams(design) {
3
+ return {
4
+ icons: {
5
+ completed: {
6
+ color: 'green',
7
+ figure: figures.tick,
8
+ paddingLeft: 0,
9
+ paddingRight: 0,
10
+ ...design?.icons?.completed,
11
+ },
12
+ current: {
13
+ color: 'yellow',
14
+ figure: figures.play,
15
+ paddingLeft: 0,
16
+ paddingRight: 0,
17
+ ...design?.icons?.current,
18
+ },
19
+ failed: {
20
+ color: 'red',
21
+ figure: figures.cross,
22
+ paddingLeft: 0,
23
+ paddingRight: 0,
24
+ ...design?.icons?.failed,
25
+ },
26
+ info: {
27
+ color: false,
28
+ figure: figures.triangleRightSmall,
29
+ paddingLeft: 2,
30
+ paddingRight: 1,
31
+ ...design?.icons?.info,
32
+ },
33
+ pending: {
34
+ color: 'dim',
35
+ figure: figures.squareSmallFilled,
36
+ paddingLeft: 0,
37
+ paddingRight: 0,
38
+ ...design?.icons?.pending,
39
+ },
40
+ skipped: {
41
+ color: 'dim',
42
+ figure: figures.circle,
43
+ paddingLeft: 0,
44
+ paddingRight: 1,
45
+ ...design?.icons?.skipped,
46
+ },
47
+ },
48
+ spinners: {
49
+ info: process.platform === 'win32' ? 'line' : 'arc',
50
+ stage: process.platform === 'win32' ? 'line' : 'dots2',
51
+ ...design?.spinners,
52
+ },
53
+ title: {
54
+ dividerChar: '─',
55
+ dividerColor: 'dim',
56
+ padding: 1,
57
+ textColor: 'white',
58
+ textPadding: 1,
59
+ width: 50,
60
+ ...design?.title,
61
+ },
62
+ };
63
+ }
package/lib/index.d.ts CHANGED
@@ -1 +1 @@
1
- export { MultiStageOutput } from './components/multi-stage-output.js';
1
+ export { MultiStageOutput, MultiStageOutputOptions } from './multi-stage-output.js';
package/lib/index.js CHANGED
@@ -1 +1 @@
1
- export { MultiStageOutput } from './components/multi-stage-output.js';
1
+ export { MultiStageOutput } from './multi-stage-output.js';
@@ -0,0 +1,74 @@
1
+ import { KeyValuePair, SimpleMessage, StageInfoBlock } from './components/stages.js';
2
+ import { Design } from './design.js';
3
+ export type MultiStageOutputOptions<T extends Record<string, unknown>> = {
4
+ /**
5
+ * Stages to render.
6
+ */
7
+ readonly stages: readonly string[] | string[];
8
+ /**
9
+ * Title to display at the top of the stages component.
10
+ */
11
+ readonly title?: string;
12
+ /**
13
+ * Information to display at the bottom of the stages component.
14
+ */
15
+ readonly postStagesBlock?: Array<KeyValuePair<T> | SimpleMessage<T>>;
16
+ /**
17
+ * Information to display below the title but above the stages.
18
+ */
19
+ readonly preStagesBlock?: Array<KeyValuePair<T> | SimpleMessage<T>>;
20
+ /**
21
+ * Whether to show the total elapsed time. Defaults to true
22
+ */
23
+ readonly showElapsedTime?: boolean;
24
+ /**
25
+ * Whether to show the time spent on each stage. Defaults to true
26
+ */
27
+ readonly showStageTime?: boolean;
28
+ /**
29
+ * Whether to show the title. Defaults to true
30
+ */
31
+ readonly showTitle?: boolean;
32
+ /**
33
+ * Information to display for a specific stage. Each object must have a stage property set.
34
+ */
35
+ readonly stageSpecificBlock?: StageInfoBlock<T>;
36
+ /**
37
+ * The unit to use for the timer. Defaults to 'ms'
38
+ */
39
+ readonly timerUnit?: 'ms' | 's';
40
+ /**
41
+ * Data to display in the stages component. This data will be passed to the get function in the info object.
42
+ */
43
+ readonly data?: Partial<T>;
44
+ /**
45
+ * Design options to customize the output.
46
+ */
47
+ readonly design?: Design;
48
+ };
49
+ export declare class MultiStageOutput<T extends Record<string, unknown>> implements Disposable {
50
+ private ciInstance;
51
+ private data?;
52
+ private readonly design;
53
+ private readonly hasElapsedTime?;
54
+ private readonly hasStageTime?;
55
+ private inkInstance;
56
+ private readonly postStagesBlock?;
57
+ private readonly preStagesBlock?;
58
+ private readonly stages;
59
+ private readonly stageSpecificBlock?;
60
+ private stageTracker;
61
+ private stopped;
62
+ private readonly timerUnit?;
63
+ private readonly title?;
64
+ constructor({ data, design, postStagesBlock, preStagesBlock, showElapsedTime, showStageTime, stageSpecificBlock, stages, timerUnit, title, }: MultiStageOutputOptions<T>);
65
+ goto(stage: string, data?: Partial<T>): void;
66
+ next(data?: Partial<T>): void;
67
+ stop(error?: Error): void;
68
+ [Symbol.dispose](): void;
69
+ updateData(data: Partial<T>): void;
70
+ private formatKeyValuePairs;
71
+ /** shared method to populate everything needed for Stages cmp */
72
+ private generateStagesInput;
73
+ private update;
74
+ }
@@ -1,84 +1,19 @@
1
1
  import { ux } from '@oclif/core/ux';
2
2
  import { capitalCase } from 'change-case';
3
- import { Box, Text, render } from 'ink';
3
+ import { render } from 'ink';
4
4
  import { env } from 'node:process';
5
5
  import React from 'react';
6
- import { icons, spinners } from '../design-elements.js';
7
- import { StageTracker } from '../stage-tracker.js';
8
- import { readableTime } from '../utils.js';
9
- import { Divider } from './divider.js';
10
- import { SpinnerOrError, SpinnerOrErrorOrChildren } from './spinner.js';
11
- import { Timer } from './timer.js';
6
+ import { Stages, } from './components/stages.js';
7
+ import { constructDesignParams } from './design.js';
8
+ import { StageTracker } from './stage-tracker.js';
9
+ import { readableTime } from './utils.js';
12
10
  // Taken from https://github.com/sindresorhus/is-in-ci
13
11
  const isInCi = env.CI !== '0' &&
14
12
  env.CI !== 'false' &&
15
13
  ('CI' in env || 'CONTINUOUS_INTEGRATION' in env || Object.keys(env).some((key) => key.startsWith('CI_')));
16
- function StaticKeyValue({ color, isBold, label, value }) {
17
- if (!value)
18
- return;
19
- return (React.createElement(Box, { key: label },
20
- React.createElement(Text, { bold: isBold },
21
- label,
22
- ": "),
23
- React.createElement(Text, { color: color }, value)));
24
- }
25
- function SimpleMessage({ color, isBold, value }) {
26
- if (!value)
27
- return;
28
- return (React.createElement(Text, { bold: isBold, color: color }, value));
29
- }
30
- function Infos({ error, keyValuePairs, stage, }) {
31
- return (keyValuePairs
32
- // If stage is provided, only show info for that stage
33
- // otherwise, show all infos that don't have a specified stage
34
- .filter((kv) => (stage ? kv.stage === stage : !kv.stage))
35
- .map((kv) => {
36
- const key = `${kv.label}-${kv.value}`;
37
- if (kv.type === 'message') {
38
- return React.createElement(SimpleMessage, { key: key, ...kv });
39
- }
40
- if (kv.type === 'dynamic-key-value') {
41
- return (React.createElement(SpinnerOrErrorOrChildren, { key: key, error: error, label: `${kv.label}:`, labelPosition: "left", type: spinners.info }, kv.value && (React.createElement(Text, { bold: kv.isBold, color: kv.color }, kv.value))));
42
- }
43
- if (kv.type === 'static-key-value') {
44
- return React.createElement(StaticKeyValue, { key: key, ...kv });
45
- }
46
- return null;
47
- }));
48
- }
49
- export function Stages({ error, hasElapsedTime = true, hasStageTime = true, postStagesBlock, preStagesBlock, stageSpecificBlock, stageTracker, timerUnit = 'ms', title, }) {
50
- return (React.createElement(Box, { flexDirection: "column", paddingTop: 1, paddingBottom: 1 },
51
- React.createElement(Divider, { title: title }),
52
- preStagesBlock && preStagesBlock.length > 0 && (React.createElement(Box, { flexDirection: "column", marginLeft: 1, paddingTop: 1 },
53
- React.createElement(Infos, { error: error, keyValuePairs: preStagesBlock }))),
54
- React.createElement(Box, { flexDirection: "column", marginLeft: 1, paddingTop: 1 }, [...stageTracker.entries()].map(([stage, status]) => (React.createElement(Box, { key: stage, flexDirection: "column" },
55
- React.createElement(Box, null,
56
- (status === 'current' || status === 'failed') && (React.createElement(SpinnerOrError, { error: error, label: capitalCase(stage), type: spinners.stage })),
57
- status === 'skipped' && (React.createElement(Text, { color: "dim" },
58
- icons.skipped,
59
- " ",
60
- capitalCase(stage),
61
- " - Skipped")),
62
- status === 'completed' && (React.createElement(Box, null,
63
- React.createElement(Text, { color: "green" }, icons.completed),
64
- React.createElement(Text, null, capitalCase(stage)))),
65
- status === 'pending' && (React.createElement(Text, { color: "dim" },
66
- icons.pending,
67
- " ",
68
- capitalCase(stage))),
69
- status !== 'pending' && status !== 'skipped' && hasStageTime && (React.createElement(Box, null,
70
- React.createElement(Text, null, " "),
71
- React.createElement(Timer, { color: "dim", isStopped: status === 'completed', unit: timerUnit })))),
72
- stageSpecificBlock && stageSpecificBlock.length > 0 && status !== 'pending' && status !== 'skipped' && (React.createElement(Box, { flexDirection: "column", marginLeft: 5 },
73
- React.createElement(Infos, { error: error, keyValuePairs: stageSpecificBlock, stage: stage }))))))),
74
- postStagesBlock && postStagesBlock.length > 0 && (React.createElement(Box, { flexDirection: "column", marginLeft: 1, paddingTop: 1 },
75
- React.createElement(Infos, { error: error, keyValuePairs: postStagesBlock }))),
76
- hasElapsedTime && (React.createElement(Box, { marginLeft: 1, paddingTop: 1 },
77
- React.createElement(Text, null, "Elapsed Time: "),
78
- React.createElement(Timer, { unit: timerUnit })))));
79
- }
80
14
  class CIMultiStageOutput {
81
15
  data;
16
+ design;
82
17
  hasElapsedTime;
83
18
  hasStageTime;
84
19
  lastUpdateTime;
@@ -91,9 +26,8 @@ class CIMultiStageOutput {
91
26
  startTime;
92
27
  startTimes = new Map();
93
28
  timerUnit;
94
- title;
95
- constructor({ data, postStagesBlock, preStagesBlock, showElapsedTime, showStageTime, stageSpecificBlock, stages, timerUnit, title, }) {
96
- this.title = title;
29
+ constructor({ data, design, postStagesBlock, preStagesBlock, showElapsedTime, showStageTime, stageSpecificBlock, stages, timerUnit, title, }) {
30
+ this.design = constructDesignParams(design);
97
31
  this.stages = stages;
98
32
  this.postStagesBlock = postStagesBlock;
99
33
  this.preStagesBlock = preStagesBlock;
@@ -103,7 +37,8 @@ class CIMultiStageOutput {
103
37
  this.timerUnit = timerUnit ?? 'ms';
104
38
  this.data = data;
105
39
  this.lastUpdateTime = Date.now();
106
- ux.stdout(`───── ${this.title} ─────`);
40
+ if (title)
41
+ ux.stdout(`───── ${title} ─────`);
107
42
  ux.stdout('Stages:');
108
43
  for (const stage of this.stages) {
109
44
  ux.stdout(`${this.stages.indexOf(stage) + 1}. ${capitalCase(stage)}`);
@@ -142,7 +77,7 @@ class CIMultiStageOutput {
142
77
  this.lastUpdateTime = Date.now();
143
78
  if (!this.startTimes.has(stage))
144
79
  this.startTimes.set(stage, Date.now());
145
- ux.stdout(`${icons.current} ${capitalCase(stage)}...`);
80
+ ux.stdout(`${this.design.icons.current} ${capitalCase(stage)}...`);
146
81
  this.printInfo(this.preStagesBlock, 3);
147
82
  this.printInfo(this.stageSpecificBlock?.filter((info) => info.stage === stage), 3);
148
83
  this.printInfo(this.postStagesBlock, 3);
@@ -156,16 +91,16 @@ class CIMultiStageOutput {
156
91
  const startTime = this.startTimes.get(stage);
157
92
  const elapsedTime = startTime ? Date.now() - startTime : 0;
158
93
  const displayTime = readableTime(elapsedTime, this.timerUnit);
159
- ux.stdout(`${icons[status]} ${capitalCase(stage)} (${displayTime})`);
94
+ ux.stdout(`${this.design.icons[status]} ${capitalCase(stage)} (${displayTime})`);
160
95
  this.printInfo(this.preStagesBlock, 3);
161
96
  this.printInfo(this.stageSpecificBlock?.filter((info) => info.stage === stage), 3);
162
97
  this.printInfo(this.postStagesBlock, 3);
163
98
  }
164
99
  else if (status === 'skipped') {
165
- ux.stdout(`${icons[status]} ${capitalCase(stage)} - Skipped`);
100
+ ux.stdout(`${this.design.icons[status]} ${capitalCase(stage)} - Skipped`);
166
101
  }
167
102
  else {
168
- ux.stdout(`${icons[status]} ${capitalCase(stage)}`);
103
+ ux.stdout(`${this.design.icons[status]} ${capitalCase(stage)}`);
169
104
  this.printInfo(this.preStagesBlock, 3);
170
105
  this.printInfo(this.stageSpecificBlock?.filter((info) => info.stage === stage), 3);
171
106
  this.printInfo(this.postStagesBlock, 3);
@@ -197,6 +132,7 @@ class CIMultiStageOutput {
197
132
  export class MultiStageOutput {
198
133
  ciInstance;
199
134
  data;
135
+ design;
200
136
  hasElapsedTime;
201
137
  hasStageTime;
202
138
  inkInstance;
@@ -208,8 +144,9 @@ export class MultiStageOutput {
208
144
  stopped = false;
209
145
  timerUnit;
210
146
  title;
211
- constructor({ data, jsonEnabled, postStagesBlock, preStagesBlock, showElapsedTime, showStageTime, stageSpecificBlock, stages, timerUnit, title, }) {
147
+ constructor({ data, design, postStagesBlock, preStagesBlock, showElapsedTime, showStageTime, stageSpecificBlock, stages, timerUnit, title, }) {
212
148
  this.data = data;
149
+ this.design = constructDesignParams(design);
213
150
  this.stages = stages;
214
151
  this.title = title;
215
152
  this.postStagesBlock = postStagesBlock;
@@ -219,12 +156,10 @@ export class MultiStageOutput {
219
156
  this.timerUnit = timerUnit ?? 'ms';
220
157
  this.stageTracker = new StageTracker(stages);
221
158
  this.stageSpecificBlock = stageSpecificBlock;
222
- if (jsonEnabled)
223
- return;
224
159
  if (isInCi) {
225
160
  this.ciInstance = new CIMultiStageOutput({
226
161
  data,
227
- jsonEnabled,
162
+ design,
228
163
  postStagesBlock,
229
164
  preStagesBlock,
230
165
  showElapsedTime,
@@ -236,7 +171,7 @@ export class MultiStageOutput {
236
171
  });
237
172
  }
238
173
  else {
239
- this.inkInstance = render(React.createElement(Stages, { hasElapsedTime: this.hasElapsedTime, hasStageTime: this.hasStageTime, postStagesBlock: this.formatKeyValuePairs(this.postStagesBlock), preStagesBlock: this.formatKeyValuePairs(this.preStagesBlock), stageSpecificBlock: this.formatKeyValuePairs(this.stageSpecificBlock), stageTracker: this.stageTracker, timerUnit: this.timerUnit, title: this.title }));
174
+ this.inkInstance = render(React.createElement(Stages, { ...this.generateStagesInput() }));
240
175
  }
241
176
  }
242
177
  goto(stage, data) {
@@ -267,12 +202,8 @@ export class MultiStageOutput {
267
202
  this.ciInstance?.stop(this.stageTracker);
268
203
  return;
269
204
  }
270
- if (error) {
271
- this.inkInstance?.rerender(React.createElement(Stages, { error: error, hasElapsedTime: this.hasElapsedTime, hasStageTime: this.hasStageTime, postStagesBlock: this.formatKeyValuePairs(this.postStagesBlock), preStagesBlock: this.formatKeyValuePairs(this.preStagesBlock), stageSpecificBlock: this.formatKeyValuePairs(this.stageSpecificBlock), stageTracker: this.stageTracker, timerUnit: this.timerUnit, title: this.title }));
272
- }
273
- else {
274
- this.inkInstance?.rerender(React.createElement(Stages, { hasElapsedTime: this.hasElapsedTime, hasStageTime: this.hasStageTime, postStagesBlock: this.formatKeyValuePairs(this.postStagesBlock), preStagesBlock: this.formatKeyValuePairs(this.preStagesBlock), stageSpecificBlock: this.formatKeyValuePairs(this.stageSpecificBlock), stageTracker: this.stageTracker, timerUnit: this.timerUnit, title: this.title }));
275
- }
205
+ const stagesInput = { ...this.generateStagesInput(), ...(error ? { error } : {}) };
206
+ this.inkInstance?.rerender(React.createElement(Stages, { ...stagesInput }));
276
207
  this.inkInstance?.unmount();
277
208
  }
278
209
  [Symbol.dispose]() {
@@ -297,6 +228,20 @@ export class MultiStageOutput {
297
228
  };
298
229
  }) ?? []);
299
230
  }
231
+ /** shared method to populate everything needed for Stages cmp */
232
+ generateStagesInput() {
233
+ return {
234
+ design: this.design,
235
+ hasElapsedTime: this.hasElapsedTime,
236
+ hasStageTime: this.hasStageTime,
237
+ postStagesBlock: this.formatKeyValuePairs(this.postStagesBlock),
238
+ preStagesBlock: this.formatKeyValuePairs(this.preStagesBlock),
239
+ stageSpecificBlock: this.formatKeyValuePairs(this.stageSpecificBlock),
240
+ stageTracker: this.stageTracker,
241
+ timerUnit: this.timerUnit,
242
+ title: this.title,
243
+ };
244
+ }
300
245
  update(stage, data) {
301
246
  this.data = { ...this.data, ...data };
302
247
  this.stageTracker.refresh(stage);
@@ -304,7 +249,7 @@ export class MultiStageOutput {
304
249
  this.ciInstance?.update(this.stageTracker, this.data);
305
250
  }
306
251
  else {
307
- this.inkInstance?.rerender(React.createElement(Stages, { hasElapsedTime: this.hasElapsedTime, hasStageTime: this.hasStageTime, postStagesBlock: this.formatKeyValuePairs(this.postStagesBlock), preStagesBlock: this.formatKeyValuePairs(this.preStagesBlock), stageSpecificBlock: this.formatKeyValuePairs(this.stageSpecificBlock), stageTracker: this.stageTracker, timerUnit: this.timerUnit, title: this.title }));
252
+ this.inkInstance?.rerender(React.createElement(Stages, { ...this.generateStagesInput() }));
308
253
  }
309
254
  }
310
255
  }
@@ -1,12 +1,16 @@
1
1
  export type StageStatus = 'pending' | 'current' | 'completed' | 'skipped' | 'failed';
2
- export declare class StageTracker extends Map<string, StageStatus> {
2
+ export declare class StageTracker {
3
3
  current: string | undefined;
4
+ private map;
4
5
  private markers;
5
6
  constructor(stages: readonly string[] | string[]);
7
+ entries(): IterableIterator<[string, StageStatus]>;
8
+ get(stage: string): StageStatus | undefined;
6
9
  refresh(nextStage: string, opts?: {
7
10
  hasError?: boolean;
8
11
  isStopping?: boolean;
9
12
  }): void;
10
- set(stage: string, status: StageStatus): this;
13
+ set(stage: string, status: StageStatus): void;
14
+ values(): IterableIterator<StageStatus>;
11
15
  private stopMarker;
12
16
  }
@@ -1,16 +1,23 @@
1
1
  import { Performance } from '@oclif/core/performance';
2
- export class StageTracker extends Map {
2
+ export class StageTracker {
3
3
  current;
4
+ map = new Map();
4
5
  markers = new Map();
5
6
  constructor(stages) {
6
- super(stages.map((stage) => [stage, 'pending']));
7
+ this.map = new Map(stages.map((stage) => [stage, 'pending']));
8
+ }
9
+ entries() {
10
+ return this.map.entries();
11
+ }
12
+ get(stage) {
13
+ return this.map.get(stage);
7
14
  }
8
15
  refresh(nextStage, opts) {
9
- const stages = [...this.keys()];
16
+ const stages = [...this.map.keys()];
10
17
  for (const stage of stages) {
11
- if (this.get(stage) === 'skipped')
18
+ if (this.map.get(stage) === 'skipped')
12
19
  continue;
13
- if (this.get(stage) === 'failed')
20
+ if (this.map.get(stage) === 'failed')
14
21
  continue;
15
22
  // .stop() was called with an error => set the stage to failed
16
23
  if (nextStage === stage && opts?.hasError) {
@@ -34,7 +41,7 @@ export class StageTracker extends Map {
34
41
  continue;
35
42
  }
36
43
  // any stage before the current stage should be marked as skipped if it's still pending
37
- if (stages.indexOf(stage) < stages.indexOf(nextStage) && this.get(stage) === 'pending') {
44
+ if (stages.indexOf(stage) < stages.indexOf(nextStage) && this.map.get(stage) === 'pending') {
38
45
  this.set(stage, 'skipped');
39
46
  continue;
40
47
  }
@@ -52,7 +59,10 @@ export class StageTracker extends Map {
52
59
  if (status === 'current') {
53
60
  this.current = stage;
54
61
  }
55
- return super.set(stage, status);
62
+ this.map.set(stage, status);
63
+ }
64
+ values() {
65
+ return this.map.values();
56
66
  }
57
67
  stopMarker(stage) {
58
68
  const marker = this.markers.get(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.1.4",
4
+ "version": "0.2.1",
5
5
  "author": "Salesforce",
6
6
  "bugs": "https://github.com/oclif/multi-stage-output/issues",
7
7
  "dependencies": {
@@ -1,130 +0,0 @@
1
- import React from 'react';
2
- import { StageTracker } from '../stage-tracker.js';
3
- type Info<T extends Record<string, unknown>> = {
4
- /**
5
- * key-value: Display a key-value pair with a spinner.
6
- * static-key-value: Display a key-value pair without a spinner.
7
- * message: Display a message.
8
- */
9
- type: 'dynamic-key-value' | 'static-key-value' | 'message';
10
- /**
11
- * Color of the value.
12
- */
13
- color?: string;
14
- /**
15
- * Get the value to display. Takes the data property on the MultiStageComponent as an argument.
16
- * Useful if you want to apply some logic (like rendering a link) to the data before displaying it.
17
- *
18
- * @param data The data property on the MultiStageComponent.
19
- * @returns {string | undefined}
20
- */
21
- get: (data?: T) => string | undefined;
22
- /**
23
- * Whether the value should be bold.
24
- */
25
- bold?: boolean;
26
- };
27
- type KeyValuePair<T extends Record<string, unknown>> = Info<T> & {
28
- /**
29
- * Label of the key-value pair.
30
- */
31
- label: string;
32
- type: 'dynamic-key-value' | 'static-key-value';
33
- };
34
- type SimpleMessage<T extends Record<string, unknown>> = Info<T> & {
35
- type: 'message';
36
- };
37
- type StageInfoBlock<T extends Record<string, unknown>> = Array<(KeyValuePair<T> & {
38
- stage: string;
39
- }) | (SimpleMessage<T> & {
40
- stage: string;
41
- })>;
42
- export type FormattedKeyValue = {
43
- readonly color?: string;
44
- readonly isBold?: boolean;
45
- readonly label?: string;
46
- readonly value: string | undefined;
47
- readonly stage?: string;
48
- readonly type: 'dynamic-key-value' | 'static-key-value' | 'message';
49
- };
50
- type MultiStageOutputOptions<T extends Record<string, unknown>> = {
51
- /**
52
- * Stages to render.
53
- */
54
- readonly stages: readonly string[] | string[];
55
- /**
56
- * Title to display at the top of the stages component.
57
- */
58
- readonly title: string;
59
- /**
60
- * Information to display at the bottom of the stages component.
61
- */
62
- readonly postStagesBlock?: Array<KeyValuePair<T> | SimpleMessage<T>>;
63
- /**
64
- * Information to display below the title but above the stages.
65
- */
66
- readonly preStagesBlock?: Array<KeyValuePair<T> | SimpleMessage<T>>;
67
- /**
68
- * Whether to show the total elapsed time. Defaults to true
69
- */
70
- readonly showElapsedTime?: boolean;
71
- /**
72
- * Whether to show the time spent on each stage. Defaults to true
73
- */
74
- readonly showStageTime?: boolean;
75
- /**
76
- * Information to display for a specific stage. Each object must have a stage property set.
77
- */
78
- readonly stageSpecificBlock?: StageInfoBlock<T>;
79
- /**
80
- * The unit to use for the timer. Defaults to 'ms'
81
- */
82
- readonly timerUnit?: 'ms' | 's';
83
- /**
84
- * Data to display in the stages component. This data will be passed to the get function in the info object.
85
- */
86
- readonly data?: Partial<T>;
87
- /**
88
- * Whether JSON output is enabled. Defaults to false.
89
- *
90
- * Pass in this.jsonEnabled() from the command class to determine if JSON output is enabled.
91
- */
92
- readonly jsonEnabled: boolean;
93
- };
94
- type StagesProps = {
95
- readonly error?: Error | undefined;
96
- readonly postStagesBlock?: FormattedKeyValue[];
97
- readonly preStagesBlock?: FormattedKeyValue[];
98
- readonly stageSpecificBlock?: FormattedKeyValue[];
99
- readonly title: string;
100
- readonly hasElapsedTime?: boolean;
101
- readonly hasStageTime?: boolean;
102
- readonly timerUnit?: 'ms' | 's';
103
- readonly stageTracker: StageTracker;
104
- };
105
- declare function SimpleMessage({ color, isBold, value }: FormattedKeyValue): React.ReactNode;
106
- export declare function Stages({ error, hasElapsedTime, hasStageTime, postStagesBlock, preStagesBlock, stageSpecificBlock, stageTracker, timerUnit, title, }: StagesProps): React.ReactNode;
107
- export declare class MultiStageOutput<T extends Record<string, unknown>> implements Disposable {
108
- private ciInstance;
109
- private data?;
110
- private readonly hasElapsedTime?;
111
- private readonly hasStageTime?;
112
- private inkInstance;
113
- private readonly postStagesBlock?;
114
- private readonly preStagesBlock?;
115
- private readonly stages;
116
- private readonly stageSpecificBlock?;
117
- private stageTracker;
118
- private stopped;
119
- private readonly timerUnit?;
120
- private readonly title;
121
- constructor({ data, jsonEnabled, postStagesBlock, preStagesBlock, showElapsedTime, showStageTime, stageSpecificBlock, stages, timerUnit, title, }: MultiStageOutputOptions<T>);
122
- goto(stage: string, data?: Partial<T>): void;
123
- next(data?: Partial<T>): void;
124
- stop(error?: Error): void;
125
- [Symbol.dispose](): void;
126
- updateData(data: Partial<T>): void;
127
- private formatKeyValuePairs;
128
- private update;
129
- }
130
- export {};
@@ -1,9 +0,0 @@
1
- import { type SpinnerName } from 'cli-spinners';
2
- export declare const icons: {
3
- completed: string;
4
- current: string;
5
- failed: string;
6
- pending: string;
7
- skipped: string;
8
- };
9
- export declare const spinners: Record<string, SpinnerName>;
@@ -1,12 +0,0 @@
1
- import figures from 'figures';
2
- export const icons = {
3
- completed: figures.tick,
4
- current: figures.play,
5
- failed: figures.cross,
6
- pending: figures.squareSmallFilled,
7
- skipped: figures.circle,
8
- };
9
- export const spinners = {
10
- info: process.platform === 'win32' ? 'line' : 'arc',
11
- stage: process.platform === 'win32' ? 'line' : 'dots2',
12
- };