Skip to content

Commit 5d79d6e

Browse files
committed
feat(fmt): improve formatting summary output
1 parent 6523f42 commit 5d79d6e

3 files changed

Lines changed: 146 additions & 28 deletions

File tree

‎packages/rstack/src/fmt/cli.ts‎

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import path from 'node:path';
2+
import { performance } from 'node:perf_hooks';
23
import { parseArgs } from 'node:util';
34
import { color, logger } from 'rslog';
45
import { loadRstackConfig } from '../config.ts';
@@ -83,15 +84,50 @@ const getDisplayPath = (cwd: string, filePath: string): string => {
8384
return path.sep === '\\' ? relativePath.replaceAll('\\', '/') : relativePath;
8485
};
8586

86-
const logFmtResult = (result: FmtRunResult, mode: FmtMode, cwd: string): void => {
87+
const prettyTime = (seconds: number): string => {
88+
const format = (time: string, unit: 'm' | 's') => color.bold(`${time}${unit}`);
89+
90+
if (seconds < 10) {
91+
const digits = seconds >= 0.01 ? 2 : 3;
92+
return format(seconds.toFixed(digits), 's');
93+
}
94+
95+
if (seconds < 60) {
96+
return format(seconds.toFixed(1), 's');
97+
}
98+
99+
const minutes = Math.floor(seconds / 60);
100+
const minutesLabel = format(minutes.toFixed(0), 'm');
101+
const remainingSeconds = seconds % 60;
102+
103+
if (remainingSeconds === 0) {
104+
return minutesLabel;
105+
}
106+
107+
const secondsLabel = format(remainingSeconds.toFixed(remainingSeconds % 1 === 0 ? 0 : 1), 's');
108+
109+
return `${minutesLabel} ${secondsLabel}`;
110+
};
111+
112+
const getFileLabel = (count: number): string => (count === 1 ? 'file' : 'files');
113+
const formatFileCount = (count: number): string => color.bold(count);
114+
115+
const logFmtResult = (
116+
result: FmtRunResult,
117+
mode: FmtMode,
118+
cwd: string,
119+
matchedFileCount: number,
120+
durationSeconds: number,
121+
): void => {
122+
let writtenCount = 0;
87123
let differentCount = 0;
88124
let errorCount = 0;
89125

90126
for (const file of result.files) {
91127
const displayPath = getDisplayPath(cwd, file.path);
92128

93129
if (file.status === 'written') {
94-
logger.success(displayPath);
130+
writtenCount++;
95131
} else if (file.status === 'different') {
96132
differentCount++;
97133
logger[mode === 'check' ? 'error' : 'log'](displayPath);
@@ -101,17 +137,43 @@ const logFmtResult = (result: FmtRunResult, mode: FmtMode, cwd: string): void =>
101137
}
102138
}
103139

140+
if (mode === 'write') {
141+
if (errorCount > 0) {
142+
return;
143+
}
144+
145+
const files = getFileLabel(matchedFileCount);
146+
const matchedCount = formatFileCount(matchedFileCount);
147+
const time = prettyTime(durationSeconds);
148+
if (writtenCount > 0) {
149+
logger.success(
150+
`Formatted ${formatFileCount(writtenCount)} of ${matchedCount} ${files} in ${time}.`,
151+
);
152+
} else {
153+
logger.success(`Checked ${matchedCount} ${files} in ${time}. No changes needed.`);
154+
}
155+
return;
156+
}
157+
104158
if (mode !== 'check') {
105159
return;
106160
}
107161

108162
if (differentCount > 0) {
109-
const files = differentCount === 1 ? 'file' : 'files';
110-
const count = color.bold(color.red(differentCount));
111-
const writeCommand = color.cyan('rs fmt --write');
112-
logger.error(`Code style issues found in ${count} ${files}. Run ${writeCommand} to fix.`);
163+
const differentFiles = getFileLabel(differentCount);
164+
const matchedFiles = getFileLabel(matchedFileCount);
165+
const count = color.red(formatFileCount(differentCount));
166+
const matchedCount = formatFileCount(matchedFileCount);
167+
const checkOption = color.cyan('--check');
168+
logger.error(
169+
`Formatting issues found in ${count} ${differentFiles}. Run without ${checkOption} to fix.`,
170+
);
171+
logger.info(`Checked ${matchedCount} ${matchedFiles} in ${prettyTime(durationSeconds)}.`);
113172
} else if (errorCount === 0) {
114-
logger.success('All matched files are correctly formatted.');
173+
const files = getFileLabel(matchedFileCount);
174+
logger.success(
175+
`Checked ${formatFileCount(matchedFileCount)} ${files} in ${prettyTime(durationSeconds)}. No issues found.`,
176+
);
115177
}
116178
};
117179

@@ -123,6 +185,7 @@ const runFmtCLI = async (args: string[]): Promise<void> => {
123185
}
124186

125187
const cwd = process.cwd();
188+
const startTime = performance.now();
126189

127190
try {
128191
const { configs, filePath } = await loadRstackConfig();
@@ -133,6 +196,11 @@ const runFmtCLI = async (args: string[]): Promise<void> => {
133196
});
134197
const files = await discoverFmtFiles({ cwd, patterns, config });
135198

199+
if (files.length === 0 && mode !== 'list-different') {
200+
logger.info('No files matched.');
201+
return;
202+
}
203+
136204
if (mode === 'check') {
137205
logger.start('Checking formatting...');
138206
}
@@ -143,13 +211,14 @@ const runFmtCLI = async (args: string[]): Promise<void> => {
143211
maxWorkers,
144212
});
145213

146-
logFmtResult(result, mode, cwd);
214+
const durationSeconds = (performance.now() - startTime) / 1000;
215+
logFmtResult(result, mode, cwd, files.length, durationSeconds);
147216
process.exitCode = result.exitCode;
148217
} catch (error) {
149218
logger.error(error);
150219
process.exitCode = 2;
151220
}
152221
};
153222

154-
export { fmtHelpMessage, parseFmtCLIArgs, runFmtCLI };
223+
export { fmtHelpMessage, parseFmtCLIArgs, prettyTime, runFmtCLI };
155224
export type { ParsedFmtCLIArgs };

‎packages/rstack/tests/cli/fmt/index.test.ts‎

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,21 @@ const runCLI = (args: string[]) => {
4646

4747
const runFmt = (args: string[] = []) => runCLI(['fmt', ...args]);
4848

49+
const normalizeDuration = (output: string): string =>
50+
output.replace(/\d+m(?: \d+(?:\.\d+)?s)?|\d+(?:\.\d+)?s/g, '<duration>');
51+
52+
const expectWriteSummary = (
53+
output: string,
54+
matchedFileCount: number,
55+
writtenCount: number,
56+
): void => {
57+
const files = matchedFileCount === 1 ? 'file' : 'files';
58+
const message = writtenCount
59+
? `Formatted ${writtenCount} of ${matchedFileCount} ${files} in <duration>.`
60+
: `Checked ${matchedFileCount} ${files} in <duration>. No changes needed.`;
61+
expect(normalizeDuration(output)).toBe(`success ${message}\n`);
62+
};
63+
4964
beforeEach(() => {
5065
projectPath = mkdtempSync(path.join(import.meta.dirname, 'test-temp-fmt-'));
5166
// Prevent repository-level ignore rules from affecting the fixture.
@@ -76,7 +91,7 @@ test('supports format as an alias for fmt', () => {
7691
const result = runCLI(['format', 'index.ts']);
7792

7893
expect(result.status).toBe(0);
79-
expect(result.stdout).toBe('success index.ts\n');
94+
expectWriteSummary(result.stdout, 1, 1);
8095
expect(result.stderr).toBe('');
8196
expect(readProjectFile('index.ts')).toBe('const message = "hello";\n');
8297
});
@@ -97,11 +112,21 @@ test('formats the current directory with Prettier defaults', () => {
97112
const result = runFmt();
98113

99114
expect(result.status).toBe(0);
100-
expect(result.stdout).toBe('success index.ts\n');
115+
expectWriteSummary(result.stdout, 2, 1);
101116
expect(result.stderr).toBe('');
102117
expect(readProjectFile('index.ts')).toBe('const message = "hello";\n');
103118
});
104119

120+
test('summarizes write mode when no files change', () => {
121+
writeProjectFile('index.ts', 'const message = "hello";\n');
122+
123+
const result = runFmt(['index.ts']);
124+
125+
expect(result.status).toBe(0);
126+
expectWriteSummary(result.stdout, 1, 0);
127+
expect(result.stderr).toBe('');
128+
});
129+
105130
test('does not sort package.json by default', () => {
106131
writeProjectFile('package.json', packageJsonSource);
107132

@@ -139,7 +164,7 @@ test('supports configuring the worker count', () => {
139164
const result = runFmt(['--parallel-workers', '1', 'first.ts', 'second.ts']);
140165

141166
expect(result.status).toBe(0);
142-
expect(result.stdout).toBe('success first.ts\nsuccess second.ts\n');
167+
expectWriteSummary(result.stdout, 2, 2);
143168
expect(result.stderr).toBe('');
144169
expect(readProjectFile('first.ts')).toBe('const first = "first";\n');
145170
expect(readProjectFile('second.ts')).toBe('const second = "second";\n');
@@ -154,7 +179,7 @@ test('does not load Prettier config or ignore files', () => {
154179
const result = runFmt(['index.ts']);
155180

156181
expect(result.status).toBe(0);
157-
expect(result.stdout).toBe('success index.ts\n');
182+
expectWriteSummary(result.stdout, 1, 1);
158183
expect(result.stderr).toBe('');
159184
expect(readProjectFile('index.ts')).toBe('function getMessage() {\n return "hello";\n}\n');
160185
});
@@ -186,7 +211,7 @@ define.fmt({
186211
const result = runFmt(['--write', 'src/**/*.ts']);
187212

188213
expect(result.status).toBe(0);
189-
expect(result.stdout).toBe('success src/index.test.ts\nsuccess src/index.ts\n');
214+
expectWriteSummary(result.stdout, 2, 2);
190215
expect(result.stderr).toBe('');
191216
expect(readProjectFile('src/index.ts')).toBe("const message = 'hello';\n");
192217
expect(readProjectFile('src/index.test.ts')).toBe("const test = 'test'\n");
@@ -209,7 +234,7 @@ define.fmt({
209234
const result = runFmt(['index.ts', '--config', 'custom.config.ts']);
210235

211236
expect(result.status).toBe(0);
212-
expect(result.stdout).toBe('success index.ts\n');
237+
expectWriteSummary(result.stdout, 1, 1);
213238
expect(result.stderr).toBe('');
214239
expect(readProjectFile('index.ts')).toBe("const message = 'hello';\n");
215240
});
@@ -221,19 +246,21 @@ test('checks formatting without writing files', () => {
221246
const result = runFmt(['--check', 'index.ts']);
222247

223248
expect(result.status).toBe(1);
224-
expect(result.stdout).toBe('start Checking formatting...\n');
249+
expect(normalizeDuration(result.stdout)).toBe(
250+
'start Checking formatting...\ninfo Checked 1 file in <duration>.\n',
251+
);
225252
expect(result.stderr).toContain('error index.ts');
226-
expect(result.stderr).toContain(
227-
'error Code style issues found in 1 file. Run rs fmt --write to fix.',
253+
expect(normalizeDuration(result.stderr)).toContain(
254+
'error Formatting issues found in 1 file. Run without --check to fix.',
228255
);
229256
expect(readProjectFile('index.ts')).toBe(source);
230257

231258
writeProjectFile('index.ts', 'const message = "hello";\n');
232259
const formattedResult = runFmt(['--check', 'index.ts']);
233260

234261
expect(formattedResult.status).toBe(0);
235-
expect(formattedResult.stdout).toBe(
236-
'start Checking formatting...\nsuccess All matched files are correctly formatted.\n',
262+
expect(normalizeDuration(formattedResult.stdout)).toBe(
263+
'start Checking formatting...\nsuccess Checked 1 file in <duration>. No issues found.\n',
237264
);
238265
expect(formattedResult.stderr).toBe('');
239266
});
@@ -278,7 +305,7 @@ define.fmt({
278305
const result = runFmt(['*.fixture']);
279306

280307
expect(result.status).toBe(0);
281-
expect(result.stdout).toBe('success first.fixture\nsuccess second.fixture\n');
308+
expectWriteSummary(result.stdout, 2, 2);
282309
expect(result.stderr).toBe('');
283310
expect(readProjectFile('first.fixture')).toBe('{ "first": true }\n');
284311
expect(readProjectFile('second.fixture')).toBe('{ "second": true }\n');
@@ -306,7 +333,7 @@ define.fmt({
306333
const result = runFmt(['data.fixture', 'index.ts']);
307334

308335
expect(result.status).toBe(0);
309-
expect(result.stdout).toBe('success data.fixture\nsuccess index.ts\n');
336+
expectWriteSummary(result.stdout, 2, 2);
310337
expect(result.stderr).toBe('');
311338
expect(readProjectFile('data.fixture')).toBe('{ "value": true }\n');
312339
expect(readProjectFile('index.ts')).toBe('const value = true;\n');
@@ -343,10 +370,16 @@ test('returns exit code 2 for formatting errors', () => {
343370
expect(result.stderr).toContain('error index.ts: SyntaxError:');
344371
});
345372

346-
test('succeeds when no files can be formatted', () => {
347-
const result = runFmt(['missing/**/*.ts']);
373+
test('reports when no files match', () => {
374+
const writeResult = runFmt(['missing/**/*.ts']);
348375

349-
expect(result.status).toBe(0);
350-
expect(result.stdout).toBe('');
351-
expect(result.stderr).toBe('');
376+
expect(writeResult.status).toBe(0);
377+
expect(writeResult.stdout).toBe('info No files matched.\n');
378+
expect(writeResult.stderr).toBe('');
379+
380+
const checkResult = runFmt(['--check', 'missing/**/*.ts']);
381+
382+
expect(checkResult.status).toBe(0);
383+
expect(checkResult.stdout).toBe('info No files matched.\n');
384+
expect(checkResult.stderr).toBe('');
352385
});

‎packages/rstack/tests/fmt/cli.test.ts‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
1+
import { stripVTControlCharacters } from 'node:util';
12
import { expect, test } from 'rstack/test';
2-
import { fmtHelpMessage, parseFmtCLIArgs } from '../../src/fmt/cli.ts';
3+
import { fmtHelpMessage, parseFmtCLIArgs, prettyTime } from '../../src/fmt/cli.ts';
4+
5+
test.each([
6+
[0, '0.000s'],
7+
[0.009, '0.009s'],
8+
[0.01, '0.01s'],
9+
[9.876, '9.88s'],
10+
[10, '10.0s'],
11+
[59.9, '59.9s'],
12+
[60, '1m'],
13+
[61, '1m 1s'],
14+
[61.25, '1m 1.3s'],
15+
[125.25, '2m 5.3s'],
16+
] as const)('formats %s seconds as %s', (seconds, expected) => {
17+
expect(stripVTControlCharacters(prettyTime(seconds))).toBe(expected);
18+
});
319

420
test('uses write mode by default', () => {
521
expect(parseFmtCLIArgs([])).toEqual({

0 commit comments

Comments
 (0)