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
35 changes: 22 additions & 13 deletions packages/rstack/src/fmt/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,19 @@ const formatFileCount = (count: number, isError = false): string => {
return `${isError ? color.red(formattedCount) : formattedCount} ${count === 1 ? 'file' : 'files'}`;
};

const reportNoSupportedFiles = (patterns: string[]): void => {
const targets = (patterns.length ? patterns : ['.'])
.map((pattern) => color.cyan(JSON.stringify(pattern)))
.join(', ');
logger.error(`No supported files matched ${targets}, or all matching files were ignored.`);
process.exitCode = 2;
};

const logFmtResult = (
result: FmtRunResult,
mode: FmtMode,
cwd: string,
matchedFileCount: number,
processedFileCount: number,
durationSeconds: number,
): void => {
let writtenCount = 0;
Expand All @@ -177,12 +185,12 @@ const logFmtResult = (
return;
}

const matchedFiles = formatFileCount(matchedFileCount);
const processedFiles = formatFileCount(processedFileCount);
const time = prettyTime(durationSeconds);
const message =
writtenCount > 0
? `Formatted ${formatCount(writtenCount)} of ${matchedFiles} in ${time}.`
: `Checked ${matchedFiles} in ${time}. No changes needed.`;
? `Formatted ${formatCount(writtenCount)} of ${processedFiles} in ${time}.`
: `Checked ${processedFiles} in ${time}. No changes needed.`;
logger[result.exitCode === 0 ? 'success' : 'info'](message);
return;
}
Expand All @@ -193,15 +201,15 @@ const logFmtResult = (

if (differentCount > 0) {
const differentFiles = formatFileCount(differentCount, true);
const matchedFiles = formatFileCount(matchedFileCount);
const processedFiles = formatFileCount(processedFileCount);
const checkOption = color.cyan('--check');
logger.error(
`Formatting issues found in ${differentFiles}. Run without ${checkOption} to fix.`,
);
logger.info(`Checked ${matchedFiles} in ${prettyTime(durationSeconds)}.`);
logger.info(`Checked ${processedFiles} in ${prettyTime(durationSeconds)}.`);
} else if (result.exitCode === 0) {
logger.success(
`Checked ${formatFileCount(matchedFileCount)} in ${prettyTime(durationSeconds)}. No issues found.`,
`Checked ${formatFileCount(processedFileCount)} in ${prettyTime(durationSeconds)}. No issues found.`,
);
}
};
Expand Down Expand Up @@ -263,11 +271,7 @@ const runFmtCLI = async (args: string[]): Promise<void> => {
if (noErrorOnUnmatchedPattern) {
return;
}
const targets = (patterns.length ? patterns : ['.'])
.map((pattern) => color.cyan(JSON.stringify(pattern)))
.join(', ');
logger.error(`No supported files matched ${targets}, or all matching files were ignored.`);
process.exitCode = 2;
reportNoSupportedFiles(patterns);
return;
}

Expand All @@ -281,8 +285,13 @@ const runFmtCLI = async (args: string[]): Promise<void> => {
maxWorkers,
});

if (result.processedFileCount === 0) {
reportNoSupportedFiles(patterns);
return;
}

const durationSeconds = (performance.now() - startTime) / 1000;
logFmtResult(result, mode, cwd, files.length, durationSeconds);
logFmtResult(result, mode, cwd, result.processedFileCount, durationSeconds);
process.exitCode = result.exitCode;
} catch (error) {
logger.error(error);
Expand Down
41 changes: 32 additions & 9 deletions packages/rstack/src/fmt/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,23 @@ import type { FmtWorkerPool } from './workerPool.ts';

/** Formats one file and reports whether its contents differ. */
type FormatFile = FmtWorkerPool['formatFile'];
type FmtFileOutcome = FmtFileResult | 'unchanged' | 'unsupported';

interface FmtWorkerPoolResult {
files: FmtFileResult[];
processedFileCount: number;
}

/** Converts a formatter outcome into the shared per-file result. */
const runFmtFile = async (
file: FmtFileRequest,
shouldWrite: boolean,
formatFile: FormatFile,
): Promise<FmtFileResult | undefined> => {
): Promise<FmtFileOutcome> => {
try {
const result = await formatFile(file, shouldWrite);
if (result !== 'changed') {
return;
if (result === 'unchanged' || result === 'unsupported') {
return result;
}

return {
Expand All @@ -40,15 +46,29 @@ const runFmtFilesInWorkerPool = async (
files: FmtFileRequest[],
shouldWrite: boolean,
maxWorkers?: number,
): Promise<FmtFileResult[]> => {
): Promise<FmtWorkerPoolResult> => {
const { createFmtWorkerPool } = await import('./workerPool.ts');
const workerPool = await createFmtWorkerPool(files.length, maxWorkers);

try {
const results = await Promise.all(
files.map((file) => runFmtFile(file, shouldWrite, workerPool.formatFile)),
);
return results.filter((result): result is FmtFileResult => result !== undefined);
const processedFiles: FmtFileResult[] = [];
let processedFileCount = 0;

for (const result of results) {
if (result === 'unsupported') {
continue;
}

processedFileCount++;
if (result !== 'unchanged') {
processedFiles.push(result);
}
}

return { files: processedFiles, processedFileCount };
} finally {
await workerPool.terminate();
}
Expand Down Expand Up @@ -77,12 +97,15 @@ const runFmtFiles = async ({
maxWorkers,
}: RunFmtFilesOptions): Promise<FmtRunResult> => {
const shouldWrite = mode === 'write';
const results =
files.length === 0 ? [] : await runFmtFilesInWorkerPool(files, shouldWrite, maxWorkers);
const result =
files.length === 0
? { files: [], processedFileCount: 0 }
: await runFmtFilesInWorkerPool(files, shouldWrite, maxWorkers);

return {
files: results,
exitCode: getFmtExitCode(results),
...result,
exitCode:
files.length > 0 && result.processedFileCount === 0 ? 2 : getFmtExitCode(result.files),
};
};

Expand Down
2 changes: 2 additions & 0 deletions packages/rstack/src/fmt/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ type FmtFileResult = SuccessfulFmtFileResult | FailedFmtFileResult;

interface FmtRunResult {
files: FmtFileResult[];
/** Number of processed files, excluding files with no supported parser. */
processedFileCount: number;
/** Recommended CLI exit code. */
exitCode: FmtExitCode;
}
Expand Down
38 changes: 38 additions & 0 deletions packages/rstack/tests/cli/fmt/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,3 +610,41 @@ test.each(['--no-error-on-unmatched-pattern', '--noErrorOnUnmatchedPattern'])(
}
},
);

test('counts only supported files', () => {
writeProjectFile('index.ts', 'const value = 1;\n');
writeProjectFile('notes.unknown', 'plain text');

const result = runFmt(['--check', 'index.ts', 'notes.unknown']);

expect(result.status).toBe(0);
expect(normalizeDuration(result.stdout)).toBe(
'start Checking formatting...\nsuccess Checked 1 file in <duration>. No issues found.\n',
);
expect(result.stderr).toBe('');
});

test('returns exit code 2 when all matched files are unsupported', () => {
writeProjectFile('notes.unknown', 'plain text');

for (const modeArgs of [[], ['--check'], ['--list-different']]) {
const result = runFmt([...modeArgs, 'notes.unknown']);

expect(result.status).toBe(2);
expect(result.stdout).not.toContain('success');
expect(result.stderr).toContain(
'No supported files matched "notes.unknown", or all matching files were ignored.',
);
expect(result.stderr).not.toContain('\n at ');
}
});

test('does not treat unsupported files as unmatched patterns', () => {
writeProjectFile('notes.unknown', 'plain text');

const result = runFmt(['--no-error-on-unmatched-pattern', 'notes.unknown']);

expect(result.status).toBe(2);
expect(result.stdout).toBe('');
expect(result.stderr).toContain('No supported files matched "notes.unknown"');
});
6 changes: 5 additions & 1 deletion packages/rstack/tests/fmt/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ test('does not rewrite unchanged files', async () => {
expect(result).toMatchObject({
exitCode: 0,
files: [],
processedFileCount: 1,
});
expect(statSync(filePath).mtimeMs).toBe(mtimeMs);
});
Expand All @@ -46,6 +47,7 @@ test('writes changed files', async () => {
expect(result).toMatchObject({
exitCode: 0,
files: [{ path: filePath, status: 'written' }],
processedFileCount: 1,
});
expect(readFileSync(filePath, 'utf8')).toBe('const value = 1;\n');
});
Expand Down Expand Up @@ -75,6 +77,7 @@ for (const mode of ['check', 'list-different'] as const) {
expect(result).toMatchObject({
exitCode: 1,
files: [{ path: filePath, status: 'different' }],
processedFileCount: 1,
});
expect(readFileSync(filePath, 'utf8')).toBe(source);
});
Expand All @@ -96,6 +99,7 @@ test('continues after a file fails and gives errors exit-code precedence', async
{ path: invalidPath, status: 'error' },
{ path: validPath, status: 'different' },
],
processedFileCount: 2,
});
expect(readFileSync(validPath, 'utf8')).toBe('const value=1');
});
Expand All @@ -113,7 +117,7 @@ test('omits unsupported files from the result', async () => {
},
]);

expect(result).toMatchObject({ exitCode: 0, files: [] });
expect(result).toMatchObject({ exitCode: 2, files: [], processedFileCount: 0 });
expect(readFileSync(filePath, 'utf8')).toBe('plain text');
});
});
1 change: 1 addition & 0 deletions packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ test('does not start the worker pool when there are no files', async () => {
await expect(runFmtFiles({ files: [], mode: 'write' })).resolves.toMatchObject({
files: [],
exitCode: 0,
processedFileCount: 0,
});
expect(mocks.createFmtWorkerPoolCalls).toEqual([]);
});
1 change: 1 addition & 0 deletions packages/rstack/tests/fmt/runnerWriteFailure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ test('returns an error when a file write fails', async () => {
error: { message: 'file write failed' },
},
],
processedFileCount: 1,
});
expect(mocks.terminateCalls).toBe(1);
});