From 5d79d6e162626bd0f216594922e34feea3d5cf2f Mon Sep 17 00:00:00 2001 From: neverland Date: Mon, 3 Aug 2026 16:39:09 +0800 Subject: [PATCH 1/2] feat(fmt): improve formatting summary output --- packages/rstack/src/fmt/cli.ts | 87 ++++++++++++++++++--- packages/rstack/tests/cli/fmt/index.test.ts | 69 +++++++++++----- packages/rstack/tests/fmt/cli.test.ts | 18 ++++- 3 files changed, 146 insertions(+), 28 deletions(-) diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index 0d447d4..4a6c178 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -1,4 +1,5 @@ import path from 'node:path'; +import { performance } from 'node:perf_hooks'; import { parseArgs } from 'node:util'; import { color, logger } from 'rslog'; import { loadRstackConfig } from '../config.ts'; @@ -83,7 +84,42 @@ const getDisplayPath = (cwd: string, filePath: string): string => { return path.sep === '\\' ? relativePath.replaceAll('\\', '/') : relativePath; }; -const logFmtResult = (result: FmtRunResult, mode: FmtMode, cwd: string): void => { +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 getFileLabel = (count: number): string => (count === 1 ? 'file' : 'files'); +const formatFileCount = (count: number): string => color.bold(count); + +const logFmtResult = ( + result: FmtRunResult, + mode: FmtMode, + cwd: string, + matchedFileCount: number, + durationSeconds: number, +): void => { + let writtenCount = 0; let differentCount = 0; let errorCount = 0; @@ -91,7 +127,7 @@ const logFmtResult = (result: FmtRunResult, mode: FmtMode, cwd: string): void => const displayPath = getDisplayPath(cwd, file.path); if (file.status === 'written') { - logger.success(displayPath); + writtenCount++; } else if (file.status === 'different') { differentCount++; logger[mode === 'check' ? 'error' : 'log'](displayPath); @@ -101,17 +137,43 @@ const logFmtResult = (result: FmtRunResult, mode: FmtMode, cwd: string): void => } } + if (mode === 'write') { + if (errorCount > 0) { + return; + } + + const files = getFileLabel(matchedFileCount); + const matchedCount = formatFileCount(matchedFileCount); + const time = prettyTime(durationSeconds); + if (writtenCount > 0) { + logger.success( + `Formatted ${formatFileCount(writtenCount)} of ${matchedCount} ${files} in ${time}.`, + ); + } else { + logger.success(`Checked ${matchedCount} ${files} in ${time}. No changes needed.`); + } + return; + } + if (mode !== 'check') { return; } if (differentCount > 0) { - const files = differentCount === 1 ? 'file' : 'files'; - const count = color.bold(color.red(differentCount)); - const writeCommand = color.cyan('rs fmt --write'); - logger.error(`Code style issues found in ${count} ${files}. Run ${writeCommand} to fix.`); + const differentFiles = getFileLabel(differentCount); + const matchedFiles = getFileLabel(matchedFileCount); + const count = color.red(formatFileCount(differentCount)); + const matchedCount = formatFileCount(matchedFileCount); + const checkOption = color.cyan('--check'); + logger.error( + `Formatting issues found in ${count} ${differentFiles}. Run without ${checkOption} to fix.`, + ); + logger.info(`Checked ${matchedCount} ${matchedFiles} in ${prettyTime(durationSeconds)}.`); } else if (errorCount === 0) { - logger.success('All matched files are correctly formatted.'); + const files = getFileLabel(matchedFileCount); + logger.success( + `Checked ${formatFileCount(matchedFileCount)} ${files} in ${prettyTime(durationSeconds)}. No issues found.`, + ); } }; @@ -123,6 +185,7 @@ const runFmtCLI = async (args: string[]): Promise => { } const cwd = process.cwd(); + const startTime = performance.now(); try { const { configs, filePath } = await loadRstackConfig(); @@ -133,6 +196,11 @@ const runFmtCLI = async (args: string[]): Promise => { }); const files = await discoverFmtFiles({ cwd, patterns, config }); + if (files.length === 0 && mode !== 'list-different') { + logger.info('No files matched.'); + return; + } + if (mode === 'check') { logger.start('Checking formatting...'); } @@ -143,7 +211,8 @@ const runFmtCLI = async (args: string[]): Promise => { maxWorkers, }); - logFmtResult(result, mode, cwd); + const durationSeconds = (performance.now() - startTime) / 1000; + logFmtResult(result, mode, cwd, files.length, durationSeconds); process.exitCode = result.exitCode; } catch (error) { logger.error(error); @@ -151,5 +220,5 @@ const runFmtCLI = async (args: string[]): Promise => { } }; -export { fmtHelpMessage, parseFmtCLIArgs, runFmtCLI }; +export { fmtHelpMessage, parseFmtCLIArgs, prettyTime, runFmtCLI }; export type { ParsedFmtCLIArgs }; diff --git a/packages/rstack/tests/cli/fmt/index.test.ts b/packages/rstack/tests/cli/fmt/index.test.ts index f57a7ff..d7bd34a 100644 --- a/packages/rstack/tests/cli/fmt/index.test.ts +++ b/packages/rstack/tests/cli/fmt/index.test.ts @@ -46,6 +46,21 @@ const runCLI = (args: string[]) => { const runFmt = (args: string[] = []) => runCLI(['fmt', ...args]); +const normalizeDuration = (output: string): string => + output.replace(/\d+m(?: \d+(?:\.\d+)?s)?|\d+(?:\.\d+)?s/g, ''); + +const expectWriteSummary = ( + output: string, + matchedFileCount: number, + writtenCount: number, +): void => { + const files = matchedFileCount === 1 ? 'file' : 'files'; + const message = writtenCount + ? `Formatted ${writtenCount} of ${matchedFileCount} ${files} in .` + : `Checked ${matchedFileCount} ${files} in . No changes needed.`; + expect(normalizeDuration(output)).toBe(`success ${message}\n`); +}; + beforeEach(() => { projectPath = mkdtempSync(path.join(import.meta.dirname, 'test-temp-fmt-')); // Prevent repository-level ignore rules from affecting the fixture. @@ -76,7 +91,7 @@ test('supports format as an alias for fmt', () => { const result = runCLI(['format', 'index.ts']); expect(result.status).toBe(0); - expect(result.stdout).toBe('success index.ts\n'); + expectWriteSummary(result.stdout, 1, 1); expect(result.stderr).toBe(''); expect(readProjectFile('index.ts')).toBe('const message = "hello";\n'); }); @@ -97,11 +112,21 @@ test('formats the current directory with Prettier defaults', () => { const result = runFmt(); expect(result.status).toBe(0); - expect(result.stdout).toBe('success index.ts\n'); + expectWriteSummary(result.stdout, 2, 1); expect(result.stderr).toBe(''); expect(readProjectFile('index.ts')).toBe('const message = "hello";\n'); }); +test('summarizes write mode when no files change', () => { + writeProjectFile('index.ts', 'const message = "hello";\n'); + + const result = runFmt(['index.ts']); + + expect(result.status).toBe(0); + expectWriteSummary(result.stdout, 1, 0); + expect(result.stderr).toBe(''); +}); + test('does not sort package.json by default', () => { writeProjectFile('package.json', packageJsonSource); @@ -139,7 +164,7 @@ test('supports configuring the worker count', () => { const result = runFmt(['--parallel-workers', '1', 'first.ts', 'second.ts']); expect(result.status).toBe(0); - expect(result.stdout).toBe('success first.ts\nsuccess second.ts\n'); + expectWriteSummary(result.stdout, 2, 2); expect(result.stderr).toBe(''); expect(readProjectFile('first.ts')).toBe('const first = "first";\n'); expect(readProjectFile('second.ts')).toBe('const second = "second";\n'); @@ -154,7 +179,7 @@ test('does not load Prettier config or ignore files', () => { const result = runFmt(['index.ts']); expect(result.status).toBe(0); - expect(result.stdout).toBe('success index.ts\n'); + expectWriteSummary(result.stdout, 1, 1); expect(result.stderr).toBe(''); expect(readProjectFile('index.ts')).toBe('function getMessage() {\n return "hello";\n}\n'); }); @@ -186,7 +211,7 @@ define.fmt({ const result = runFmt(['--write', 'src/**/*.ts']); expect(result.status).toBe(0); - expect(result.stdout).toBe('success src/index.test.ts\nsuccess src/index.ts\n'); + expectWriteSummary(result.stdout, 2, 2); expect(result.stderr).toBe(''); expect(readProjectFile('src/index.ts')).toBe("const message = 'hello';\n"); expect(readProjectFile('src/index.test.ts')).toBe("const test = 'test'\n"); @@ -209,7 +234,7 @@ define.fmt({ const result = runFmt(['index.ts', '--config', 'custom.config.ts']); expect(result.status).toBe(0); - expect(result.stdout).toBe('success index.ts\n'); + expectWriteSummary(result.stdout, 1, 1); expect(result.stderr).toBe(''); expect(readProjectFile('index.ts')).toBe("const message = 'hello';\n"); }); @@ -221,10 +246,12 @@ test('checks formatting without writing files', () => { const result = runFmt(['--check', 'index.ts']); expect(result.status).toBe(1); - expect(result.stdout).toBe('start Checking formatting...\n'); + expect(normalizeDuration(result.stdout)).toBe( + 'start Checking formatting...\ninfo Checked 1 file in .\n', + ); expect(result.stderr).toContain('error index.ts'); - expect(result.stderr).toContain( - 'error Code style issues found in 1 file. Run rs fmt --write to fix.', + expect(normalizeDuration(result.stderr)).toContain( + 'error Formatting issues found in 1 file. Run without --check to fix.', ); expect(readProjectFile('index.ts')).toBe(source); @@ -232,8 +259,8 @@ test('checks formatting without writing files', () => { const formattedResult = runFmt(['--check', 'index.ts']); expect(formattedResult.status).toBe(0); - expect(formattedResult.stdout).toBe( - 'start Checking formatting...\nsuccess All matched files are correctly formatted.\n', + expect(normalizeDuration(formattedResult.stdout)).toBe( + 'start Checking formatting...\nsuccess Checked 1 file in . No issues found.\n', ); expect(formattedResult.stderr).toBe(''); }); @@ -278,7 +305,7 @@ define.fmt({ const result = runFmt(['*.fixture']); expect(result.status).toBe(0); - expect(result.stdout).toBe('success first.fixture\nsuccess second.fixture\n'); + expectWriteSummary(result.stdout, 2, 2); expect(result.stderr).toBe(''); expect(readProjectFile('first.fixture')).toBe('{ "first": true }\n'); expect(readProjectFile('second.fixture')).toBe('{ "second": true }\n'); @@ -306,7 +333,7 @@ define.fmt({ const result = runFmt(['data.fixture', 'index.ts']); expect(result.status).toBe(0); - expect(result.stdout).toBe('success data.fixture\nsuccess index.ts\n'); + expectWriteSummary(result.stdout, 2, 2); expect(result.stderr).toBe(''); expect(readProjectFile('data.fixture')).toBe('{ "value": true }\n'); expect(readProjectFile('index.ts')).toBe('const value = true;\n'); @@ -343,10 +370,16 @@ test('returns exit code 2 for formatting errors', () => { expect(result.stderr).toContain('error index.ts: SyntaxError:'); }); -test('succeeds when no files can be formatted', () => { - const result = runFmt(['missing/**/*.ts']); +test('reports when no files match', () => { + const writeResult = runFmt(['missing/**/*.ts']); - expect(result.status).toBe(0); - expect(result.stdout).toBe(''); - expect(result.stderr).toBe(''); + expect(writeResult.status).toBe(0); + expect(writeResult.stdout).toBe('info No files matched.\n'); + expect(writeResult.stderr).toBe(''); + + const checkResult = runFmt(['--check', 'missing/**/*.ts']); + + expect(checkResult.status).toBe(0); + expect(checkResult.stdout).toBe('info No files matched.\n'); + expect(checkResult.stderr).toBe(''); }); diff --git a/packages/rstack/tests/fmt/cli.test.ts b/packages/rstack/tests/fmt/cli.test.ts index 3b9cfe4..6708c83 100644 --- a/packages/rstack/tests/fmt/cli.test.ts +++ b/packages/rstack/tests/fmt/cli.test.ts @@ -1,5 +1,21 @@ +import { stripVTControlCharacters } from 'node:util'; import { expect, test } from 'rstack/test'; -import { fmtHelpMessage, parseFmtCLIArgs } from '../../src/fmt/cli.ts'; +import { fmtHelpMessage, parseFmtCLIArgs, prettyTime } from '../../src/fmt/cli.ts'; + +test.each([ + [0, '0.000s'], + [0.009, '0.009s'], + [0.01, '0.01s'], + [9.876, '9.88s'], + [10, '10.0s'], + [59.9, '59.9s'], + [60, '1m'], + [61, '1m 1s'], + [61.25, '1m 1.3s'], + [125.25, '2m 5.3s'], +] as const)('formats %s seconds as %s', (seconds, expected) => { + expect(stripVTControlCharacters(prettyTime(seconds))).toBe(expected); +}); test('uses write mode by default', () => { expect(parseFmtCLIArgs([])).toEqual({ From 8f46044021bbae60e4d72225d935ceb464c1e45b Mon Sep 17 00:00:00 2001 From: neverland Date: Mon, 3 Aug 2026 17:03:35 +0800 Subject: [PATCH 2/2] refactor(fmt): streamline summary logging --- packages/rstack/src/fmt/cli.ts | 52 ++++++++++++++++------------------ 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index 4a6c178..32a5ded 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -109,8 +109,11 @@ const prettyTime = (seconds: number): string => { return `${minutesLabel} ${secondsLabel}`; }; -const getFileLabel = (count: number): string => (count === 1 ? 'file' : 'files'); -const formatFileCount = (count: number): string => color.bold(count); +const formatCount = (count: number): string => color.bold(count); +const formatFileCount = (count: number, isError = false): string => { + const formattedCount = formatCount(count); + return `${isError ? color.red(formattedCount) : formattedCount} ${count === 1 ? 'file' : 'files'}`; +}; const logFmtResult = ( result: FmtRunResult, @@ -119,38 +122,34 @@ const logFmtResult = ( matchedFileCount: number, durationSeconds: number, ): void => { - let writtenCount = 0; let differentCount = 0; - let errorCount = 0; for (const file of result.files) { - const displayPath = getDisplayPath(cwd, file.path); - if (file.status === 'written') { - writtenCount++; - } else if (file.status === 'different') { + continue; + } + + const displayPath = getDisplayPath(cwd, file.path); + if (file.status === 'different') { differentCount++; logger[mode === 'check' ? 'error' : 'log'](displayPath); } else if (file.status === 'error') { - errorCount++; logger.error(`${displayPath}: ${String(file.error)}`); } } if (mode === 'write') { - if (errorCount > 0) { + if (result.exitCode !== 0) { return; } - const files = getFileLabel(matchedFileCount); - const matchedCount = formatFileCount(matchedFileCount); + const writtenCount = result.files.length; + const matchedFiles = formatFileCount(matchedFileCount); const time = prettyTime(durationSeconds); if (writtenCount > 0) { - logger.success( - `Formatted ${formatFileCount(writtenCount)} of ${matchedCount} ${files} in ${time}.`, - ); + logger.success(`Formatted ${formatCount(writtenCount)} of ${matchedFiles} in ${time}.`); } else { - logger.success(`Checked ${matchedCount} ${files} in ${time}. No changes needed.`); + logger.success(`Checked ${matchedFiles} in ${time}. No changes needed.`); } return; } @@ -160,19 +159,16 @@ const logFmtResult = ( } if (differentCount > 0) { - const differentFiles = getFileLabel(differentCount); - const matchedFiles = getFileLabel(matchedFileCount); - const count = color.red(formatFileCount(differentCount)); - const matchedCount = formatFileCount(matchedFileCount); + const differentFiles = formatFileCount(differentCount, true); + const matchedFiles = formatFileCount(matchedFileCount); const checkOption = color.cyan('--check'); logger.error( - `Formatting issues found in ${count} ${differentFiles}. Run without ${checkOption} to fix.`, + `Formatting issues found in ${differentFiles}. Run without ${checkOption} to fix.`, ); - logger.info(`Checked ${matchedCount} ${matchedFiles} in ${prettyTime(durationSeconds)}.`); - } else if (errorCount === 0) { - const files = getFileLabel(matchedFileCount); + logger.info(`Checked ${matchedFiles} in ${prettyTime(durationSeconds)}.`); + } else if (result.exitCode === 0) { logger.success( - `Checked ${formatFileCount(matchedFileCount)} ${files} in ${prettyTime(durationSeconds)}. No issues found.`, + `Checked ${formatFileCount(matchedFileCount)} in ${prettyTime(durationSeconds)}. No issues found.`, ); } }; @@ -196,8 +192,10 @@ const runFmtCLI = async (args: string[]): Promise => { }); const files = await discoverFmtFiles({ cwd, patterns, config }); - if (files.length === 0 && mode !== 'list-different') { - logger.info('No files matched.'); + if (files.length === 0) { + if (mode !== 'list-different') { + logger.info('No files matched.'); + } return; }