Skip to content

Commit f892d36

Browse files
committed
feat: support plain text reporter and use seconds
1 parent 12a99de commit f892d36

8 files changed

Lines changed: 227 additions & 64 deletions

File tree

.changeset/calm-clocks-hang.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ice/pkg': minor
3+
---
4+
5+
feat: support plain text reporter and use seconds

packages/pkg/src/commands/build.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import fse from 'fs-extra';
22
import type { BuildTask, Context, OutputResult } from '../types.js';
3-
import { RunnerLinerTerminalReporter } from '../helpers/runnerReporter.js';
3+
import { createRunnerReporter } from '../helpers/runnerReporter.js';
44
import { getTaskRunners } from '../helpers/getTaskRunners.js';
55
import { RunnerScheduler } from '../helpers/runnerScheduler.js';
66

@@ -31,7 +31,7 @@ export default async function build(context: Context) {
3131
const tasks = getTaskRunners(buildTasks, context);
3232

3333
try {
34-
const terminal = new RunnerLinerTerminalReporter();
34+
const terminal = createRunnerReporter();
3535
const taskGroup = new RunnerScheduler(tasks, terminal);
3636

3737
const results = taskGroup.run();

packages/pkg/src/commands/start.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { consola } from 'consola';
22
import { createBatchChangeHandler, createWatcher } from '../helpers/watcher.js';
33
import type { OutputResult, Context, WatchChangedFile, BuildTask } from '../types.js';
4-
import { RunnerLinerTerminalReporter } from '../helpers/runnerReporter.js';
4+
import { createRunnerReporter } from '../helpers/runnerReporter.js';
55
import { getTaskRunners } from '../helpers/getTaskRunners.js';
66
import { RunnerScheduler } from '../helpers/runnerScheduler.js';
77
import { createServer } from '../server/createServer.js';
@@ -45,7 +45,7 @@ export default async function start(context: Context) {
4545

4646
const tasks = getTaskRunners(buildTasks, context, watcher);
4747

48-
const terminal = new RunnerLinerTerminalReporter();
48+
const terminal = createRunnerReporter();
4949
const taskGroup = new RunnerScheduler(tasks, terminal);
5050

5151
const outputResults: OutputResult[] = await taskGroup.run();

packages/pkg/src/helpers/runnerReporter.ts

Lines changed: 70 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export interface RunnerReporter {
2020
onStop?: (options: RunnerReporterStopOptions) => void;
2121
}
2222

23+
export interface RunnerReporterOptions {
24+
stream?: NodeJS.WriteStream;
25+
}
26+
2327
export class RunnerLinerTerminalReporter implements RunnerReporter {
2428
private stream: NodeJS.WriteStream;
2529
private timer: any = null;
@@ -28,11 +32,7 @@ export class RunnerLinerTerminalReporter implements RunnerReporter {
2832
private isRendering = false;
2933
private runningRunners: Runner[] = [];
3034

31-
constructor(
32-
options: {
33-
stream?: NodeJS.WriteStream;
34-
} = {},
35-
) {
35+
constructor(options: RunnerReporterOptions = {}) {
3636
this.stream = options.stream ?? process.stderr;
3737
}
3838

@@ -43,25 +43,25 @@ export class RunnerLinerTerminalReporter implements RunnerReporter {
4343
onRunnerEnd(runner: Runner) {
4444
this.runningRunners.splice(this.runningRunners.indexOf(runner), 1);
4545
const { status, context } = runner;
46-
if (status === RunnerStatus.Finished) {
47-
// TODO: for error
48-
const items: string[] = [
49-
runner.isError ? chalk.red(figures.cross) : chalk.green(figures.tick),
50-
chalk.cyan(runner.name),
51-
formatTimeCost(runner.getMetric(TASK_MARK).cost),
52-
];
53-
54-
if (context.mode === 'development') {
55-
items.push(chalk.red('dev'));
56-
}
57-
58-
// remove loading
59-
this.clear();
60-
// eslint-disable-next-line no-console
61-
console.log(` ${items.join(' ')}`);
62-
// resume loading
63-
this.render();
46+
if (status !== RunnerStatus.Finished && status !== RunnerStatus.Error) {
47+
return;
6448
}
49+
50+
const items: string[] = [
51+
runner.isError ? chalk.red(figures.cross) : chalk.green(figures.tick),
52+
chalk.cyan(runner.name),
53+
formatTimeCost(runner.getMetric(TASK_MARK).cost),
54+
];
55+
56+
if (context.mode === 'development') {
57+
items.push(chalk.red('dev'));
58+
}
59+
60+
// remove loading
61+
this.clear();
62+
this.stream.write(` ${items.join(' ')}\n`);
63+
// resume loading
64+
this.render();
6565
}
6666

6767
onStart() {
@@ -77,9 +77,8 @@ export class RunnerLinerTerminalReporter implements RunnerReporter {
7777
// 停下来之后进行最后一次更新
7878
this.clear();
7979
this.isRendering = false;
80-
// eslint-disable-next-line no-console
81-
console.log(
82-
` ${chalk.blue(figures.info)} Done in ${formatTimeCost(options.cost)} for ${options.runners.length} tasks`,
80+
this.stream.write(
81+
` ${chalk.blue(figures.info)} Done in ${formatTimeCost(options.cost)} for ${options.runners.length} tasks\n`,
8382
);
8483
}
8584

@@ -120,3 +119,48 @@ export class RunnerLinerTerminalReporter implements RunnerReporter {
120119
return ` ${chalk.dim(this.spinner.frames[this.frame])} ${chalk.dim('Running...')}`;
121120
}
122121
}
122+
123+
export class RunnerPlainTextReporter implements RunnerReporter {
124+
private stream: NodeJS.WriteStream;
125+
126+
constructor(options: RunnerReporterOptions = {}) {
127+
this.stream = options.stream ?? process.stderr;
128+
}
129+
130+
onRunnerEnd(runner: Runner) {
131+
const { status, context } = runner;
132+
if (status !== RunnerStatus.Finished && status !== RunnerStatus.Error) {
133+
return;
134+
}
135+
136+
const items: string[] = [
137+
runner.isError ? figures.cross : figures.tick,
138+
runner.name,
139+
formatTimeCost(runner.getMetric(TASK_MARK).cost, false),
140+
];
141+
142+
if (context.mode === 'development') {
143+
items.push('dev');
144+
}
145+
146+
this.stream.write(` ${items.join(' ')}\n`);
147+
}
148+
149+
onStop(options: RunnerReporterStopOptions) {
150+
this.stream.write(
151+
` ${figures.info} Done in ${formatTimeCost(options.cost, false)} for ${options.runners.length} tasks\n`,
152+
);
153+
}
154+
}
155+
156+
export function isTerminalEnvironment(stream: NodeJS.WriteStream = process.stderr) {
157+
return Boolean(stream.isTTY) && !process.env.CI;
158+
}
159+
160+
export function createRunnerReporter(options: RunnerReporterOptions = {}): RunnerReporter {
161+
const stream = options.stream ?? process.stderr;
162+
if (isTerminalEnvironment(stream)) {
163+
return new RunnerLinerTerminalReporter({ stream });
164+
}
165+
return new RunnerPlainTextReporter({ stream });
166+
}

packages/pkg/src/helpers/runnerScheduler.ts

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,48 @@ export class RunnerScheduler<T> {
3737
async run(changedFiles?: WatchChangedFile[]): Promise<T[]> {
3838
const startTime = Date.now();
3939
this.reporter.onStart?.();
40-
const parallelPromise = Promise.all(this.parallelRunners.map((runner) => runner.run(changedFiles)));
41-
const concurrentPromise = concurrentPromiseAll(
42-
this.concurrentRunners.map((runner) => () => runner.run(changedFiles)),
43-
1,
44-
);
45-
46-
const [parallelResults, concurrentResults] = await Promise.all([parallelPromise, concurrentPromise]);
47-
const stopTime = Date.now();
48-
this.reporter.onStop?.({
49-
startTime,
50-
stopTime,
51-
cost: stopTime - startTime,
52-
runners: this.runners,
53-
});
54-
return [...parallelResults, ...concurrentResults];
40+
try {
41+
const parallelPromise = Promise.allSettled(this.parallelRunners.map((runner) => runner.run(changedFiles)));
42+
const concurrentPromise = concurrentPromiseAll(
43+
this.concurrentRunners.map((runner) => () => runner.run(changedFiles)),
44+
1,
45+
).then(
46+
(value) => ({ status: 'fulfilled' as const, value }),
47+
(reason) => ({ status: 'rejected' as const, reason }),
48+
);
49+
50+
const [parallelSettled, concurrentSettled] = await Promise.all([parallelPromise, concurrentPromise]);
51+
52+
let firstError: unknown;
53+
const parallelResults: T[] = [];
54+
for (const item of parallelSettled) {
55+
if (item.status === 'fulfilled') {
56+
parallelResults.push(item.value);
57+
} else if (firstError === undefined) {
58+
firstError = item.reason;
59+
}
60+
}
61+
62+
let concurrentResults: T[] = [];
63+
if (concurrentSettled.status === 'fulfilled') {
64+
concurrentResults = concurrentSettled.value;
65+
} else if (firstError === undefined) {
66+
firstError = concurrentSettled.reason;
67+
}
68+
69+
if (firstError !== undefined) {
70+
throw firstError;
71+
}
72+
73+
return [...parallelResults, ...concurrentResults];
74+
} finally {
75+
const stopTime = Date.now();
76+
this.reporter.onStop?.({
77+
startTime,
78+
stopTime,
79+
cost: stopTime - startTime,
80+
runners: this.runners,
81+
});
82+
}
5583
}
5684
}

packages/pkg/src/utils.ts

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -235,30 +235,31 @@ export function debouncePromise<T extends unknown[]>(
235235
};
236236
}
237237

238-
// Build time 0-500ms Green
239-
// 500-3000 Yellow
240-
// 3000- Red
241-
export const timeFrom = (start: number, subtract = 0): string => {
242-
const time: number | string = performance.now() - start - subtract;
243-
const timeString = `${time.toFixed(2)} ms`.padEnd(5, ' ');
244-
if (time < 500) {
238+
// Build time <1s Green
239+
// <5s Yellow
240+
// >=5s Red
241+
function colorizeTimeCost(time: number, timeString: string) {
242+
if (time < 1000) {
245243
return picocolors.green(timeString);
246-
} else if (time < 3000) {
244+
} else if (time < 5000) {
247245
return picocolors.yellow(timeString);
248246
} else {
249247
return picocolors.red(timeString);
250248
}
249+
}
250+
251+
export const timeFrom = (start: number, subtract = 0): string => {
252+
const time = performance.now() - start - subtract;
253+
const timeString = `${(time / 1000).toFixed(2)}s`;
254+
return colorizeTimeCost(time, timeString);
251255
};
252256

253-
export function formatTimeCost(time: number) {
254-
const timeString = `${time.toFixed(2)} ms`.padEnd(5, ' ');
255-
if (time < 500) {
256-
return picocolors.green(timeString);
257-
} else if (time < 3000) {
258-
return picocolors.yellow(timeString);
259-
} else {
260-
return picocolors.red(timeString);
257+
export function formatTimeCost(time: number, colored = true) {
258+
const timeString = `${(time / 1000).toFixed(2)}s`;
259+
if (!colored) {
260+
return timeString;
261261
}
262+
return colorizeTimeCost(time, timeString);
262263
}
263264

264265
export const unique = <T>(arr: T[]): T[] => {

packages/pkg/tests/helpers/runnerScheduler.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ class MockRunner extends Runner {
1818
}
1919

2020
doRun(files?: WatchChangedFile[]): Promise<any> {
21+
if (typeof this._value === 'function') {
22+
return this._value(files);
23+
}
24+
if (this._value instanceof Error) {
25+
return Promise.reject(this._value);
26+
}
2127
return Promise.resolve(this._value);
2228
}
2329
}
@@ -47,3 +53,36 @@ it('should initialize with correct distribution of runners', async () => {
4753
expect(reporter.onStop).toHaveBeenCalledTimes(1);
4854
expect(reporter.onStart).toHaveBeenCalledTimes(1);
4955
});
56+
57+
it('should call reporter cleanup when runner fails', async () => {
58+
const failedRunner = new MockRunner(mockContext, false, new Error('boom'));
59+
const reporter = new MockReporter();
60+
61+
const scheduler = new RunnerScheduler([failedRunner], reporter);
62+
63+
await expect(scheduler.run()).rejects.toThrow('boom');
64+
expect(reporter.onRunnerStart).toHaveBeenCalledTimes(1);
65+
expect(reporter.onRunnerEnd).toHaveBeenCalledTimes(1);
66+
expect(reporter.onStop).toHaveBeenCalledTimes(1);
67+
expect(reporter.onStart).toHaveBeenCalledTimes(1);
68+
});
69+
70+
it('should call onStop after all runners settled when one fails early', async () => {
71+
const failedRunner = new MockRunner(mockContext, true, new Error('boom'));
72+
const slowRunner = new MockRunner(
73+
mockContext,
74+
true,
75+
() => new Promise<number>((resolve) => setTimeout(() => resolve(1), 20)),
76+
);
77+
const reporter = new MockReporter();
78+
79+
const scheduler = new RunnerScheduler([failedRunner, slowRunner], reporter);
80+
81+
await expect(scheduler.run()).rejects.toThrow('boom');
82+
expect(reporter.onRunnerEnd).toHaveBeenCalledTimes(2);
83+
expect(reporter.onStop).toHaveBeenCalledTimes(1);
84+
85+
const stopOrder = reporter.onStop.mock.invocationCallOrder[0];
86+
const endOrders = reporter.onRunnerEnd.mock.invocationCallOrder;
87+
expect(stopOrder).toBeGreaterThan(Math.max(...endOrders));
88+
});

0 commit comments

Comments
 (0)