Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-clocks-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ice/pkg': minor
---

feat: support plain text reporter and use seconds
4 changes: 2 additions & 2 deletions packages/pkg/src/commands/build.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fse from 'fs-extra';
import type { BuildTask, Context, OutputResult } from '../types.js';
import { RunnerLinerTerminalReporter } from '../helpers/runnerReporter.js';
import { createRunnerReporter } from '../helpers/runnerReporter.js';
import { getTaskRunners } from '../helpers/getTaskRunners.js';
import { RunnerScheduler } from '../helpers/runnerScheduler.js';

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

try {
const terminal = new RunnerLinerTerminalReporter();
const terminal = createRunnerReporter();
const taskGroup = new RunnerScheduler(tasks, terminal);

const results = taskGroup.run();
Expand Down
4 changes: 2 additions & 2 deletions packages/pkg/src/commands/start.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { consola } from 'consola';
import { createBatchChangeHandler, createWatcher } from '../helpers/watcher.js';
import type { OutputResult, Context, WatchChangedFile, BuildTask } from '../types.js';
import { RunnerLinerTerminalReporter } from '../helpers/runnerReporter.js';
import { createRunnerReporter } from '../helpers/runnerReporter.js';
import { getTaskRunners } from '../helpers/getTaskRunners.js';
import { RunnerScheduler } from '../helpers/runnerScheduler.js';
import { createServer } from '../server/createServer.js';
Expand Down Expand Up @@ -45,7 +45,7 @@ export default async function start(context: Context) {

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

const terminal = new RunnerLinerTerminalReporter();
const terminal = createRunnerReporter();
const taskGroup = new RunnerScheduler(tasks, terminal);

const outputResults: OutputResult[] = await taskGroup.run();
Expand Down
96 changes: 70 additions & 26 deletions packages/pkg/src/helpers/runnerReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export interface RunnerReporter {
onStop?: (options: RunnerReporterStopOptions) => void;
}

export interface RunnerReporterOptions {
stream?: NodeJS.WriteStream;
}

export class RunnerLinerTerminalReporter implements RunnerReporter {
private stream: NodeJS.WriteStream;
private timer: any = null;
Expand All @@ -28,11 +32,7 @@ export class RunnerLinerTerminalReporter implements RunnerReporter {
private isRendering = false;
private runningRunners: Runner[] = [];

constructor(
options: {
stream?: NodeJS.WriteStream;
} = {},
) {
constructor(options: RunnerReporterOptions = {}) {
this.stream = options.stream ?? process.stderr;
}

Expand All @@ -43,25 +43,25 @@ export class RunnerLinerTerminalReporter implements RunnerReporter {
onRunnerEnd(runner: Runner) {
this.runningRunners.splice(this.runningRunners.indexOf(runner), 1);
const { status, context } = runner;
if (status === RunnerStatus.Finished) {
// TODO: for error
const items: string[] = [
runner.isError ? chalk.red(figures.cross) : chalk.green(figures.tick),
chalk.cyan(runner.name),
formatTimeCost(runner.getMetric(TASK_MARK).cost),
];

if (context.mode === 'development') {
items.push(chalk.red('dev'));
}

// remove loading
this.clear();
// eslint-disable-next-line no-console
console.log(` ${items.join(' ')}`);
// resume loading
this.render();
if (status !== RunnerStatus.Finished && status !== RunnerStatus.Error) {
return;
}

const items: string[] = [
runner.isError ? chalk.red(figures.cross) : chalk.green(figures.tick),
chalk.cyan(runner.name),
formatTimeCost(runner.getMetric(TASK_MARK).cost),
];

if (context.mode === 'development') {
items.push(chalk.red('dev'));
}

// remove loading
this.clear();
this.stream.write(` ${items.join(' ')}\n`);
// resume loading
this.render();
}

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

Expand Down Expand Up @@ -120,3 +119,48 @@ export class RunnerLinerTerminalReporter implements RunnerReporter {
return ` ${chalk.dim(this.spinner.frames[this.frame])} ${chalk.dim('Running...')}`;
}
}

export class RunnerPlainTextReporter implements RunnerReporter {
private stream: NodeJS.WriteStream;

constructor(options: RunnerReporterOptions = {}) {
this.stream = options.stream ?? process.stderr;
}

onRunnerEnd(runner: Runner) {
const { status, context } = runner;
if (status !== RunnerStatus.Finished && status !== RunnerStatus.Error) {
return;
}

const items: string[] = [
runner.isError ? figures.cross : figures.tick,
runner.name,
formatTimeCost(runner.getMetric(TASK_MARK).cost, false),
];

if (context.mode === 'development') {
items.push('dev');
}

this.stream.write(` ${items.join(' ')}\n`);
}

onStop(options: RunnerReporterStopOptions) {
this.stream.write(
` ${figures.info} Done in ${formatTimeCost(options.cost, false)} for ${options.runners.length} tasks\n`,
);
}
}

export function isTerminalEnvironment(stream: NodeJS.WriteStream = process.stderr) {
return Boolean(stream.isTTY) && !process.env.CI;
}

export function createRunnerReporter(options: RunnerReporterOptions = {}): RunnerReporter {
const stream = options.stream ?? process.stderr;
if (isTerminalEnvironment(stream)) {
return new RunnerLinerTerminalReporter({ stream });
}
return new RunnerPlainTextReporter({ stream });
}
58 changes: 43 additions & 15 deletions packages/pkg/src/helpers/runnerScheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,48 @@ export class RunnerScheduler<T> {
async run(changedFiles?: WatchChangedFile[]): Promise<T[]> {
const startTime = Date.now();
this.reporter.onStart?.();
const parallelPromise = Promise.all(this.parallelRunners.map((runner) => runner.run(changedFiles)));
const concurrentPromise = concurrentPromiseAll(
this.concurrentRunners.map((runner) => () => runner.run(changedFiles)),
1,
);

const [parallelResults, concurrentResults] = await Promise.all([parallelPromise, concurrentPromise]);
const stopTime = Date.now();
this.reporter.onStop?.({
startTime,
stopTime,
cost: stopTime - startTime,
runners: this.runners,
});
return [...parallelResults, ...concurrentResults];
try {
const parallelPromise = Promise.allSettled(this.parallelRunners.map((runner) => runner.run(changedFiles)));
const concurrentPromise = concurrentPromiseAll(
this.concurrentRunners.map((runner) => () => runner.run(changedFiles)),
1,
).then(
(value) => ({ status: 'fulfilled' as const, value }),
(reason) => ({ status: 'rejected' as const, reason }),
);

const [parallelSettled, concurrentSettled] = await Promise.all([parallelPromise, concurrentPromise]);

let firstError: unknown;
const parallelResults: T[] = [];
for (const item of parallelSettled) {
if (item.status === 'fulfilled') {
parallelResults.push(item.value);
} else if (firstError === undefined) {
firstError = item.reason;
}
}

let concurrentResults: T[] = [];
if (concurrentSettled.status === 'fulfilled') {
concurrentResults = concurrentSettled.value;
} else if (firstError === undefined) {
firstError = concurrentSettled.reason;
}

if (firstError !== undefined) {
throw firstError;
}

return [...parallelResults, ...concurrentResults];
} finally {
const stopTime = Date.now();
this.reporter.onStop?.({
startTime,
stopTime,
cost: stopTime - startTime,
runners: this.runners,
});
}
}
}
33 changes: 17 additions & 16 deletions packages/pkg/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,30 +235,31 @@ export function debouncePromise<T extends unknown[]>(
};
}

// Build time 0-500ms Green
// 500-3000 Yellow
// 3000- Red
export const timeFrom = (start: number, subtract = 0): string => {
const time: number | string = performance.now() - start - subtract;
const timeString = `${time.toFixed(2)} ms`.padEnd(5, ' ');
if (time < 500) {
// Build time <1s Green
// <5s Yellow
// >=5s Red
function colorizeTimeCost(time: number, timeString: string) {
if (time < 1000) {
return picocolors.green(timeString);
} else if (time < 3000) {
} else if (time < 5000) {
return picocolors.yellow(timeString);
} else {
return picocolors.red(timeString);
}
}

export const timeFrom = (start: number, subtract = 0): string => {
const time = performance.now() - start - subtract;
const timeString = `${(time / 1000).toFixed(2)}s`;
return colorizeTimeCost(time, timeString);
};

export function formatTimeCost(time: number) {
const timeString = `${time.toFixed(2)} ms`.padEnd(5, ' ');
if (time < 500) {
return picocolors.green(timeString);
} else if (time < 3000) {
return picocolors.yellow(timeString);
} else {
return picocolors.red(timeString);
export function formatTimeCost(time: number, colored = true) {
const timeString = `${(time / 1000).toFixed(2)}s`;
if (!colored) {
return timeString;
}
return colorizeTimeCost(time, timeString);
}

export const unique = <T>(arr: T[]): T[] => {
Expand Down
39 changes: 39 additions & 0 deletions packages/pkg/tests/helpers/runnerScheduler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ class MockRunner extends Runner {
}

doRun(files?: WatchChangedFile[]): Promise<any> {
if (typeof this._value === 'function') {
return this._value(files);
}
if (this._value instanceof Error) {
return Promise.reject(this._value);
}
return Promise.resolve(this._value);
}
}
Expand Down Expand Up @@ -47,3 +53,36 @@ it('should initialize with correct distribution of runners', async () => {
expect(reporter.onStop).toHaveBeenCalledTimes(1);
expect(reporter.onStart).toHaveBeenCalledTimes(1);
});

it('should call reporter cleanup when runner fails', async () => {
const failedRunner = new MockRunner(mockContext, false, new Error('boom'));
const reporter = new MockReporter();

const scheduler = new RunnerScheduler([failedRunner], reporter);

await expect(scheduler.run()).rejects.toThrow('boom');
expect(reporter.onRunnerStart).toHaveBeenCalledTimes(1);
expect(reporter.onRunnerEnd).toHaveBeenCalledTimes(1);
expect(reporter.onStop).toHaveBeenCalledTimes(1);
expect(reporter.onStart).toHaveBeenCalledTimes(1);
});

it('should call onStop after all runners settled when one fails early', async () => {
const failedRunner = new MockRunner(mockContext, true, new Error('boom'));
const slowRunner = new MockRunner(
mockContext,
true,
() => new Promise<number>((resolve) => setTimeout(() => resolve(1), 20)),
);
const reporter = new MockReporter();

const scheduler = new RunnerScheduler([failedRunner, slowRunner], reporter);

await expect(scheduler.run()).rejects.toThrow('boom');
expect(reporter.onRunnerEnd).toHaveBeenCalledTimes(2);
expect(reporter.onStop).toHaveBeenCalledTimes(1);

const stopOrder = reporter.onStop.mock.invocationCallOrder[0];
const endOrders = reporter.onRunnerEnd.mock.invocationCallOrder;
expect(stopOrder).toBeGreaterThan(Math.max(...endOrders));
});
Loading
Loading