From 9bb4b21fb59e8bdf44ba608740c6e15e65234452 Mon Sep 17 00:00:00 2001 From: neverland Date: Tue, 1 Sep 2026 10:18:49 +0800 Subject: [PATCH 1/4] feat(fmt): display millisecond durations --- packages/rstack/src/fmt/cli.ts | 45 ++++++----------------- packages/rstack/src/fmt/elapsed.ts | 35 ++++++++++++++++++ packages/rstack/tests/cli/fmt/helpers.ts | 5 ++- packages/rstack/tests/fmt/elapsed.test.ts | 18 +++++++++ 4 files changed, 68 insertions(+), 35 deletions(-) create mode 100644 packages/rstack/src/fmt/elapsed.ts create mode 100644 packages/rstack/tests/fmt/elapsed.test.ts diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index ed61c36b..d4dd1f09 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -8,6 +8,7 @@ import { ensureProjectCacheDir } from '../projectCache.ts'; import { fmtCacheFileName } from './cacheStore.ts'; import { resolveFmtConfig } from './config.ts'; import { discoverFmtFiles } from './discovery.ts'; +import { formatElapsed } from './elapsed.ts'; import { createRelativePathResolver, toPosixPath } from './pathHelpers.ts'; import { runFmtFiles } from './runner.ts'; import type { FmtMode, FmtRunResult, ResolvedFmtConfig } from './types.ts'; @@ -157,34 +158,8 @@ const createDisplayPathResolver = ( return (filePath) => toPosixPath(resolveRelativePath(filePath)); }; -const prettyTime = (seconds: number): string => { - const format = (time: string, unit: 'm' | 's') => - color.bold(`${time}${unit}`); - - if (seconds < 10) { - const digits = seconds >= 0.01 ? 2 : 3; - return format(seconds.toFixed(digits), 's'); - } - - if (seconds < 60) { - return format(seconds.toFixed(1), 's'); - } - - const minutes = Math.floor(seconds / 60); - const minutesLabel = format(minutes.toFixed(0), 'm'); - const remainingSeconds = seconds % 60; - - if (remainingSeconds === 0) { - return minutesLabel; - } - - const secondsLabel = format( - remainingSeconds.toFixed(remainingSeconds % 1 === 0 ? 0 : 1), - 's', - ); - - return `${minutesLabel} ${secondsLabel}`; -}; +const prettyTime = (milliseconds: number): string => + color.bold(formatElapsed(milliseconds)); const formatCount = (count: number): string => color.bold(count); const formatFileCount = (count: number, isError = false): string => { @@ -207,7 +182,7 @@ const logFmtResult = ( mode: FmtMode, cwd: string, processedFileCount: number, - durationSeconds: number, + durationMilliseconds: number, fixCommand?: string, ): void => { let writtenCount = 0; @@ -235,7 +210,7 @@ const logFmtResult = ( } const processedFiles = formatFileCount(processedFileCount); - const time = prettyTime(durationSeconds); + const time = prettyTime(durationMilliseconds); const message = writtenCount > 0 ? `Formatted ${formatCount(writtenCount)} of ${processedFiles} in ${time}.` @@ -255,10 +230,12 @@ const logFmtResult = ( ? `Run ${color.cyan(fixCommand)} to fix.` : `Rerun this command without ${color.cyan('--check')} to fix.`; logger.error(`Formatting issues found in ${differentFiles}. ${fixHint}`); - logger.info(`Checked ${processedFiles} in ${prettyTime(durationSeconds)}.`); + logger.info( + `Checked ${processedFiles} in ${prettyTime(durationMilliseconds)}.`, + ); } else if (result.exitCode === 0) { logger.success( - `Checked ${formatFileCount(processedFileCount)} in ${prettyTime(durationSeconds)}. No issues found.`, + `Checked ${formatFileCount(processedFileCount)} in ${prettyTime(durationMilliseconds)}. No issues found.`, ); } }; @@ -415,13 +392,13 @@ const runFmtCLI = async ( return; } - const durationSeconds = (performance.now() - startTime) / 1000; + const durationMilliseconds = performance.now() - startTime; logFmtResult( result, mode, cwd, result.processedFileCount, - durationSeconds, + durationMilliseconds, fixCommand, ); process.exitCode = result.exitCode; diff --git a/packages/rstack/src/fmt/elapsed.ts b/packages/rstack/src/fmt/elapsed.ts new file mode 100644 index 00000000..35dd9100 --- /dev/null +++ b/packages/rstack/src/fmt/elapsed.ts @@ -0,0 +1,35 @@ +const formatSeconds = (milliseconds: number): string => { + const seconds = Math.floor(milliseconds / 1000); + const remainder = milliseconds % 1000; + + if (remainder === 0) { + return `${seconds}s`; + } + + const fraction = remainder.toString().padStart(3, '0').replace(/0+$/, ''); + return `${seconds}.${fraction}s`; +}; + +/** Formats elapsed milliseconds after rounding to millisecond precision. */ +const formatElapsed = (milliseconds: number): string => { + if (milliseconds < 1) { + return '<1ms'; + } + + const roundedMilliseconds = Math.round(milliseconds); + if (roundedMilliseconds < 1000) { + return `${roundedMilliseconds}ms`; + } + + const hours = Math.floor(roundedMilliseconds / 3_600_000); + const minutes = Math.floor(roundedMilliseconds / 60_000) % 60; + const seconds = formatSeconds(roundedMilliseconds % 60_000); + + if (hours > 0) { + return `${hours}h${minutes}m${seconds}`; + } + + return minutes > 0 ? `${minutes}m${seconds}` : seconds; +}; + +export { formatElapsed }; diff --git a/packages/rstack/tests/cli/fmt/helpers.ts b/packages/rstack/tests/cli/fmt/helpers.ts index d3b62229..9a51e0fb 100644 --- a/packages/rstack/tests/cli/fmt/helpers.ts +++ b/packages/rstack/tests/cli/fmt/helpers.ts @@ -42,7 +42,10 @@ export const createCliEnv = (): NodeJS.ProcessEnv => { }; export const normalizeDuration = (output: string): string => - output.replace(/\d+m(?: \d+(?:\.\d+)?s)?|\d+(?:\.\d+)?s/g, ''); + output.replace( + /<1ms|\d+ms|\d+h\d+m\d+(?:\.\d+)?s|\d+m\d+(?:\.\d+)?s|\d+(?:\.\d+)?s/g, + '', + ); export const expectWriteSummary = ( output: string, diff --git a/packages/rstack/tests/fmt/elapsed.test.ts b/packages/rstack/tests/fmt/elapsed.test.ts new file mode 100644 index 00000000..a0ecbe03 --- /dev/null +++ b/packages/rstack/tests/fmt/elapsed.test.ts @@ -0,0 +1,18 @@ +import { expect, test } from 'rstack/test'; +import { formatElapsed } from '../../src/fmt/elapsed.ts'; + +test.each([ + [0, '<1ms'], + [0.999, '<1ms'], + [1, '1ms'], + [29.6, '30ms'], + [999.4, '999ms'], + [999.6, '1s'], + [1_390, '1.39s'], + [1_234, '1.234s'], + [60_000, '1m0s'], + [60_123, '1m0.123s'], + [3_661_234, '1h1m1.234s'], +] as const)('formats %sms as %s', (milliseconds, expected) => { + expect(formatElapsed(milliseconds)).toBe(expected); +}); From c77751aeba0f3eb8c7b3663e31ae9d3b9591a6ba Mon Sep 17 00:00:00 2001 From: neverland Date: Tue, 1 Sep 2026 10:35:52 +0800 Subject: [PATCH 2/4] refactor(fmt): rename duration formatter --- packages/rstack/src/fmt/cli.ts | 4 ++-- packages/rstack/src/fmt/{elapsed.ts => duration.ts} | 6 +++--- .../rstack/tests/fmt/{elapsed.test.ts => duration.test.ts} | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) rename packages/rstack/src/fmt/{elapsed.ts => duration.ts} (83%) rename packages/rstack/tests/fmt/{elapsed.test.ts => duration.test.ts} (74%) diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index d4dd1f09..463bbbb2 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -8,7 +8,7 @@ import { ensureProjectCacheDir } from '../projectCache.ts'; import { fmtCacheFileName } from './cacheStore.ts'; import { resolveFmtConfig } from './config.ts'; import { discoverFmtFiles } from './discovery.ts'; -import { formatElapsed } from './elapsed.ts'; +import { formatDuration } from './duration.ts'; import { createRelativePathResolver, toPosixPath } from './pathHelpers.ts'; import { runFmtFiles } from './runner.ts'; import type { FmtMode, FmtRunResult, ResolvedFmtConfig } from './types.ts'; @@ -159,7 +159,7 @@ const createDisplayPathResolver = ( }; const prettyTime = (milliseconds: number): string => - color.bold(formatElapsed(milliseconds)); + color.bold(formatDuration(milliseconds)); const formatCount = (count: number): string => color.bold(count); const formatFileCount = (count: number, isError = false): string => { diff --git a/packages/rstack/src/fmt/elapsed.ts b/packages/rstack/src/fmt/duration.ts similarity index 83% rename from packages/rstack/src/fmt/elapsed.ts rename to packages/rstack/src/fmt/duration.ts index 35dd9100..4ea5449e 100644 --- a/packages/rstack/src/fmt/elapsed.ts +++ b/packages/rstack/src/fmt/duration.ts @@ -10,8 +10,8 @@ const formatSeconds = (milliseconds: number): string => { return `${seconds}.${fraction}s`; }; -/** Formats elapsed milliseconds after rounding to millisecond precision. */ -const formatElapsed = (milliseconds: number): string => { +/** Formats a duration after rounding to millisecond precision. */ +const formatDuration = (milliseconds: number): string => { if (milliseconds < 1) { return '<1ms'; } @@ -32,4 +32,4 @@ const formatElapsed = (milliseconds: number): string => { return minutes > 0 ? `${minutes}m${seconds}` : seconds; }; -export { formatElapsed }; +export { formatDuration }; diff --git a/packages/rstack/tests/fmt/elapsed.test.ts b/packages/rstack/tests/fmt/duration.test.ts similarity index 74% rename from packages/rstack/tests/fmt/elapsed.test.ts rename to packages/rstack/tests/fmt/duration.test.ts index a0ecbe03..4800ab33 100644 --- a/packages/rstack/tests/fmt/elapsed.test.ts +++ b/packages/rstack/tests/fmt/duration.test.ts @@ -1,5 +1,5 @@ import { expect, test } from 'rstack/test'; -import { formatElapsed } from '../../src/fmt/elapsed.ts'; +import { formatDuration } from '../../src/fmt/duration.ts'; test.each([ [0, '<1ms'], @@ -14,5 +14,5 @@ test.each([ [60_123, '1m0.123s'], [3_661_234, '1h1m1.234s'], ] as const)('formats %sms as %s', (milliseconds, expected) => { - expect(formatElapsed(milliseconds)).toBe(expected); + expect(formatDuration(milliseconds)).toBe(expected); }); From aedb0e226cd22d3439b4efdd90bcaa0970a1aac7 Mon Sep 17 00:00:00 2001 From: neverland Date: Tue, 1 Sep 2026 10:39:35 +0800 Subject: [PATCH 3/4] refactor(fmt): simplify duration formatting --- packages/rstack/src/fmt/cli.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index 463bbbb2..40518c0f 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -158,9 +158,6 @@ const createDisplayPathResolver = ( return (filePath) => toPosixPath(resolveRelativePath(filePath)); }; -const prettyTime = (milliseconds: number): string => - color.bold(formatDuration(milliseconds)); - const formatCount = (count: number): string => color.bold(count); const formatFileCount = (count: number, isError = false): string => { const formattedCount = formatCount(count); @@ -204,13 +201,18 @@ const logFmtResult = ( } } + if (mode === 'list-different') { + return; + } + + const time = color.bold(formatDuration(durationMilliseconds)); + if (mode === 'write') { if (writtenCount === 0 && result.exitCode !== 0) { return; } const processedFiles = formatFileCount(processedFileCount); - const time = prettyTime(durationMilliseconds); const message = writtenCount > 0 ? `Formatted ${formatCount(writtenCount)} of ${processedFiles} in ${time}.` @@ -219,10 +221,6 @@ const logFmtResult = ( return; } - if (mode !== 'check') { - return; - } - if (differentCount > 0) { const differentFiles = formatFileCount(differentCount, true); const processedFiles = formatFileCount(processedFileCount); @@ -230,12 +228,10 @@ const logFmtResult = ( ? `Run ${color.cyan(fixCommand)} to fix.` : `Rerun this command without ${color.cyan('--check')} to fix.`; logger.error(`Formatting issues found in ${differentFiles}. ${fixHint}`); - logger.info( - `Checked ${processedFiles} in ${prettyTime(durationMilliseconds)}.`, - ); + logger.info(`Checked ${processedFiles} in ${time}.`); } else if (result.exitCode === 0) { logger.success( - `Checked ${formatFileCount(processedFileCount)} in ${prettyTime(durationMilliseconds)}. No issues found.`, + `Checked ${formatFileCount(processedFileCount)} in ${time}. No issues found.`, ); } }; From b049451d5a86d9167c96a1690056fbe7d7da9170 Mon Sep 17 00:00:00 2001 From: neverland Date: Tue, 1 Sep 2026 10:48:01 +0800 Subject: [PATCH 4/4] fix(fmt): preserve duration precision --- packages/rstack/src/fmt/duration.ts | 37 +++++++++++----------- packages/rstack/tests/cli/fmt/helpers.ts | 2 +- packages/rstack/tests/fmt/duration.test.ts | 11 ++++--- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/rstack/src/fmt/duration.ts b/packages/rstack/src/fmt/duration.ts index 4ea5449e..c093fab0 100644 --- a/packages/rstack/src/fmt/duration.ts +++ b/packages/rstack/src/fmt/duration.ts @@ -1,16 +1,4 @@ -const formatSeconds = (milliseconds: number): string => { - const seconds = Math.floor(milliseconds / 1000); - const remainder = milliseconds % 1000; - - if (remainder === 0) { - return `${seconds}s`; - } - - const fraction = remainder.toString().padStart(3, '0').replace(/0+$/, ''); - return `${seconds}.${fraction}s`; -}; - -/** Formats a duration after rounding to millisecond precision. */ +/** Formats sub-second durations in milliseconds and preserves the existing longer-duration format. */ const formatDuration = (milliseconds: number): string => { if (milliseconds < 1) { return '<1ms'; @@ -21,15 +9,26 @@ const formatDuration = (milliseconds: number): string => { return `${roundedMilliseconds}ms`; } - const hours = Math.floor(roundedMilliseconds / 3_600_000); - const minutes = Math.floor(roundedMilliseconds / 60_000) % 60; - const seconds = formatSeconds(roundedMilliseconds % 60_000); + const seconds = milliseconds / 1000; + if (seconds < 10) { + return `${seconds.toFixed(2)}s`; + } + + if (seconds < 60) { + return `${seconds.toFixed(1)}s`; + } + + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; - if (hours > 0) { - return `${hours}h${minutes}m${seconds}`; + if (remainingSeconds === 0) { + return `${minutes}m`; } - return minutes > 0 ? `${minutes}m${seconds}` : seconds; + const secondsLabel = remainingSeconds.toFixed( + remainingSeconds % 1 === 0 ? 0 : 1, + ); + return `${minutes}m ${secondsLabel}s`; }; export { formatDuration }; diff --git a/packages/rstack/tests/cli/fmt/helpers.ts b/packages/rstack/tests/cli/fmt/helpers.ts index 9a51e0fb..5086ce5d 100644 --- a/packages/rstack/tests/cli/fmt/helpers.ts +++ b/packages/rstack/tests/cli/fmt/helpers.ts @@ -43,7 +43,7 @@ export const createCliEnv = (): NodeJS.ProcessEnv => { export const normalizeDuration = (output: string): string => output.replace( - /<1ms|\d+ms|\d+h\d+m\d+(?:\.\d+)?s|\d+m\d+(?:\.\d+)?s|\d+(?:\.\d+)?s/g, + /<1ms|\d+ms|\d+m(?: \d+(?:\.\d+)?s)?|\d+(?:\.\d+)?s/g, '', ); diff --git a/packages/rstack/tests/fmt/duration.test.ts b/packages/rstack/tests/fmt/duration.test.ts index 4800ab33..ad208cbf 100644 --- a/packages/rstack/tests/fmt/duration.test.ts +++ b/packages/rstack/tests/fmt/duration.test.ts @@ -7,12 +7,13 @@ test.each([ [1, '1ms'], [29.6, '30ms'], [999.4, '999ms'], - [999.6, '1s'], + [999.6, '1.00s'], [1_390, '1.39s'], - [1_234, '1.234s'], - [60_000, '1m0s'], - [60_123, '1m0.123s'], - [3_661_234, '1h1m1.234s'], + [1_234, '1.23s'], + [12_340, '12.3s'], + [60_000, '1m'], + [60_123, '1m 0.1s'], + [3_661_234, '61m 1.2s'], ] as const)('formats %sms as %s', (milliseconds, expected) => { expect(formatDuration(milliseconds)).toBe(expected); });