Skip to content

Commit 9ef70a9

Browse files
authored
feat(fmt): improve formatting summary output (#157)
1 parent 6dba9ff commit 9ef70a9

3 files changed

Lines changed: 150 additions & 34 deletions

File tree

packages/rstack/src/fmt/cli.ts

Lines changed: 82 additions & 15 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,35 +84,92 @@ 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 formatCount = (count: number): string => color.bold(count);
113+
const formatFileCount = (count: number, isError = false): string => {
114+
const formattedCount = formatCount(count);
115+
return `${isError ? color.red(formattedCount) : formattedCount} ${count === 1 ? 'file' : 'files'}`;
116+
};
117+
118+
const logFmtResult = (
119+
result: FmtRunResult,
120+
mode: FmtMode,
121+
cwd: string,
122+
matchedFileCount: number,
123+
durationSeconds: number,
124+
): void => {
87125
let differentCount = 0;
88-
let errorCount = 0;
89126

90127
for (const file of result.files) {
91-
const displayPath = getDisplayPath(cwd, file.path);
92-
93128
if (file.status === 'written') {
94-
logger.success(displayPath);
95-
} else if (file.status === 'different') {
129+
continue;
130+
}
131+
132+
const displayPath = getDisplayPath(cwd, file.path);
133+
if (file.status === 'different') {
96134
differentCount++;
97135
logger[mode === 'check' ? 'error' : 'log'](displayPath);
98136
} else if (file.status === 'error') {
99-
errorCount++;
100137
logger.error(`${displayPath}: ${String(file.error)}`);
101138
}
102139
}
103140

141+
if (mode === 'write') {
142+
if (result.exitCode !== 0) {
143+
return;
144+
}
145+
146+
const writtenCount = result.files.length;
147+
const matchedFiles = formatFileCount(matchedFileCount);
148+
const time = prettyTime(durationSeconds);
149+
if (writtenCount > 0) {
150+
logger.success(`Formatted ${formatCount(writtenCount)} of ${matchedFiles} in ${time}.`);
151+
} else {
152+
logger.success(`Checked ${matchedFiles} in ${time}. No changes needed.`);
153+
}
154+
return;
155+
}
156+
104157
if (mode !== 'check') {
105158
return;
106159
}
107160

108161
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.`);
113-
} else if (errorCount === 0) {
114-
logger.success('All matched files are correctly formatted.');
162+
const differentFiles = formatFileCount(differentCount, true);
163+
const matchedFiles = formatFileCount(matchedFileCount);
164+
const checkOption = color.cyan('--check');
165+
logger.error(
166+
`Formatting issues found in ${differentFiles}. Run without ${checkOption} to fix.`,
167+
);
168+
logger.info(`Checked ${matchedFiles} in ${prettyTime(durationSeconds)}.`);
169+
} else if (result.exitCode === 0) {
170+
logger.success(
171+
`Checked ${formatFileCount(matchedFileCount)} in ${prettyTime(durationSeconds)}. No issues found.`,
172+
);
115173
}
116174
};
117175

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

125183
const cwd = process.cwd();
184+
const startTime = performance.now();
126185

127186
try {
128187
const { configs, filePath } = await loadRstackConfig();
@@ -133,6 +192,13 @@ const runFmtCLI = async (args: string[]): Promise<void> => {
133192
});
134193
const files = await discoverFmtFiles({ cwd, patterns, config });
135194

195+
if (files.length === 0) {
196+
if (mode !== 'list-different') {
197+
logger.info('No files matched.');
198+
}
199+
return;
200+
}
201+
136202
if (mode === 'check') {
137203
logger.start('Checking formatting...');
138204
}
@@ -143,13 +209,14 @@ const runFmtCLI = async (args: string[]): Promise<void> => {
143209
maxWorkers,
144210
});
145211

146-
logFmtResult(result, mode, cwd);
212+
const durationSeconds = (performance.now() - startTime) / 1000;
213+
logFmtResult(result, mode, cwd, files.length, durationSeconds);
147214
process.exitCode = result.exitCode;
148215
} catch (error) {
149216
logger.error(error);
150217
process.exitCode = 2;
151218
}
152219
};
153220

154-
export { fmtHelpMessage, parseFmtCLIArgs, runFmtCLI };
221+
export { fmtHelpMessage, parseFmtCLIArgs, prettyTime, runFmtCLI };
155222
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)