Skip to content

Commit d3bdfb8

Browse files
authored
feat(fmt): add CLI argument parser (#122)
1 parent 6338a31 commit d3bdfb8

5 files changed

Lines changed: 128 additions & 9 deletions

File tree

packages/rstack/src/fmt/cli.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { parseArgs } from 'node:util';
2+
import { color } from 'rslog';
3+
import type { FmtMode } from './types.ts';
4+
5+
interface ParsedFmtCLIArgs {
6+
mode: FmtMode;
7+
patterns: string[];
8+
help: boolean;
9+
}
10+
11+
const fmtHelpMessage: string = `Rstack v${RSTACK_VERSION}
12+
13+
${color.cyan('Usage')}:
14+
${color.yellow(' $ rs fmt [options] [files/globs...]')}
15+
16+
Format files with Prettier.
17+
18+
${color.cyan('Options')}:
19+
--write Write formatted files in place (default)
20+
--check Check whether files are formatted
21+
--list-different Print paths of unformatted files
22+
-h, --help Display this help message`;
23+
24+
const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => {
25+
const { values, positionals } = parseArgs({
26+
args,
27+
options: {
28+
write: { type: 'boolean' },
29+
check: { type: 'boolean' },
30+
'list-different': { type: 'boolean' },
31+
listDifferent: { type: 'boolean' },
32+
help: { type: 'boolean', short: 'h' },
33+
},
34+
allowPositionals: true,
35+
strict: true,
36+
});
37+
38+
const listDifferent = values['list-different'] || values.listDifferent;
39+
const modes = [values.write, values.check, listDifferent].filter(Boolean);
40+
if (modes.length > 1) {
41+
throw new Error('The --write, --check, and --list-different options cannot be used together.');
42+
}
43+
44+
const mode = values.check ? 'check' : listDifferent ? 'list-different' : 'write';
45+
46+
return {
47+
mode,
48+
patterns: positionals,
49+
help: values.help ?? false,
50+
};
51+
};
52+
53+
export { fmtHelpMessage, parseFmtCLIArgs };
54+
export type { ParsedFmtCLIArgs };

packages/rstack/src/fmt/runner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { FmtExitCode, FmtFileResult, FmtRunResult, RunFmtFilesOptions } from './types.ts';
2-
import { formatFileSerial } from './vendor/prettier-cli/serial.ts';
2+
import { formatFileSerial } from './serial.ts';
33

44
const runFmtFiles = async ({ files, mode }: RunFmtFilesOptions): Promise<FmtRunResult> => {
55
const startTime = performance.now();

packages/rstack/src/fmt/vendor/prettier-cli/serial.ts renamed to packages/rstack/src/fmt/serial.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import { readFile, writeFile } from 'atomically';
88
import { format } from 'prettier';
9-
import type { FmtFileRequest } from '../../types.ts';
9+
import type { FmtFileRequest } from './types.ts';
1010

1111
const formatFileSerial = async (
1212
{ path, options }: FmtFileRequest,

packages/rstack/src/fmt/vendor/prettier-cli/LICENSE

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { expect, test } from 'rstack/test';
2+
import { fmtHelpMessage, parseFmtCLIArgs } from '../../src/fmt/cli.ts';
3+
4+
test('uses write mode by default', () => {
5+
expect(parseFmtCLIArgs([])).toEqual({
6+
mode: 'write',
7+
patterns: [],
8+
help: false,
9+
});
10+
});
11+
12+
test.each([
13+
['--write', 'write'],
14+
['--check', 'check'],
15+
['--list-different', 'list-different'],
16+
['--listDifferent', 'list-different'],
17+
] as const)('parses %s mode', (option, mode) => {
18+
expect(parseFmtCLIArgs([option])).toEqual({
19+
mode,
20+
patterns: [],
21+
help: false,
22+
});
23+
});
24+
25+
test('preserves file paths and globs', () => {
26+
const patterns = ['src/file with spaces.ts', 'src/**/*.{js,ts}', '!src/generated/**'];
27+
28+
expect(parseFmtCLIArgs([patterns[0], '--check', ...patterns.slice(1)])).toEqual({
29+
mode: 'check',
30+
patterns,
31+
help: false,
32+
});
33+
});
34+
35+
test('treats arguments after the terminator as paths', () => {
36+
expect(parseFmtCLIArgs(['--check', '--', '--write', '--help'])).toEqual({
37+
mode: 'check',
38+
patterns: ['--write', '--help'],
39+
help: false,
40+
});
41+
});
42+
43+
test.each(['--help', '-h'])('parses %s', (option) => {
44+
expect(parseFmtCLIArgs([option]).help).toBe(true);
45+
});
46+
47+
test('provides command help', () => {
48+
expect(fmtHelpMessage).toContain('Usage:\n $ rs fmt [options] [files/globs...]');
49+
expect(fmtHelpMessage).toContain('--write');
50+
expect(fmtHelpMessage).toContain('--check');
51+
expect(fmtHelpMessage).toContain('--list-different');
52+
expect(fmtHelpMessage).toContain('-h, --help');
53+
});
54+
55+
test.each([
56+
['--write', '--check'],
57+
['--write', '--list-different'],
58+
['--write', '--listDifferent'],
59+
['--check', '--list-different'],
60+
['--write', '--check', '--list-different'],
61+
])('rejects conflicting modes: %s', (...args) => {
62+
expect(() => parseFmtCLIArgs(args)).toThrow(
63+
'The --write, --check, and --list-different options cannot be used together.',
64+
);
65+
});
66+
67+
test.each(['--unknown', '--no-cache', '--no-parallel', '--parallel-workers'])(
68+
'rejects unsupported option %s',
69+
(option) => {
70+
expect(() => parseFmtCLIArgs([option])).toThrow();
71+
},
72+
);

0 commit comments

Comments
 (0)