Skip to content

Commit 1f15c86

Browse files
authored
test: split formatter CLI tests (#297)
1 parent 95857d3 commit 1f15c86

7 files changed

Lines changed: 894 additions & 838 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { expect, test } from 'rstack/test';
2+
import { expectWriteSummary, normalizeDuration, setupFmtTest } from './helpers.ts';
3+
4+
const { projectFileExists, readProjectFile, resolveProjectPath, runFmt, writeProjectFile } =
5+
setupFmtTest();
6+
7+
test.each([
8+
['write', []],
9+
['check', ['--check']],
10+
['list-different', ['--list-different']],
11+
] as const)('uses the default cache in %s mode', (_, args) => {
12+
writeProjectFile('index.ts', 'const value = 1;\n');
13+
writeProjectFile('.rstack/cache/fmt-v1.json', 'legacy');
14+
15+
const result = runFmt([...args, 'index.ts']);
16+
17+
expect(result.status).toBe(0);
18+
expect(readProjectFile('.rstack/cache/.gitignore')).toBe('*\n');
19+
expect(JSON.parse(readProjectFile('.rstack/cache/fmt/v1.json'))).toMatchObject({
20+
version: 1,
21+
files: {
22+
'index.ts': [expect.any(String), expect.any(String), 'clean'],
23+
},
24+
});
25+
expect(readProjectFile('.rstack/cache/fmt-v1.json')).toBe('legacy');
26+
});
27+
28+
test('--no-cache bypasses cache reads and writes', () => {
29+
writeProjectFile('index.ts', 'const value=1');
30+
writeProjectFile('custom-cache/v1.json', '{"value":true}');
31+
32+
const first = runFmt([
33+
'--no-cache',
34+
'--cache-location',
35+
'custom-cache',
36+
'index.ts',
37+
'custom-cache/v1.json',
38+
]);
39+
40+
expect(first.status).toBe(0);
41+
expect(readProjectFile('custom-cache/v1.json')).toBe('{ "value": true }\n');
42+
expect(projectFileExists('.rstack')).toBe(false);
43+
44+
writeProjectFile('.rstack/cache/fmt-v1.json', 'stale');
45+
writeProjectFile('index.ts', 'const value=2');
46+
const second = runFmt(['--no-cache', 'index.ts']);
47+
48+
expect(second.status).toBe(0);
49+
expect(readProjectFile('index.ts')).toBe('const value = 2;\n');
50+
expect(readProjectFile('.rstack/cache/fmt-v1.json')).toBe('stale');
51+
expect(projectFileExists('.rstack/cache/.gitignore')).toBe(false);
52+
});
53+
54+
test.each(['relative', 'absolute'] as const)('uses a %s custom cache location', (kind) => {
55+
const cacheLocation = kind === 'relative' ? 'custom-cache' : resolveProjectPath('custom-cache');
56+
writeProjectFile('index.ts', 'const value = 1;\n');
57+
58+
const result = runFmt(['--cache-location', cacheLocation, 'index.ts']);
59+
60+
expect(result.status).toBe(0);
61+
expect(JSON.parse(readProjectFile('custom-cache/v1.json'))).toMatchObject({
62+
version: 1,
63+
files: {
64+
'index.ts': [expect.any(String), expect.any(String), 'clean'],
65+
},
66+
});
67+
expect(projectFileExists('custom-cache/.gitignore')).toBe(false);
68+
expect(projectFileExists('.rstack')).toBe(false);
69+
});
70+
71+
test.each(['.', '..'])('rejects a custom cache location at %s', (cacheLocation) => {
72+
const result = runFmt(['--cache-location', cacheLocation, '.']);
73+
74+
expect(result.status).toBe(2);
75+
expect(result.stdout).toBe('');
76+
expect(result.stderr).toContain(
77+
'The --cache-location directory cannot be the current working directory or an ancestor.',
78+
);
79+
});
80+
81+
test('excludes the custom cache directory from formatting', () => {
82+
const cacheLocation = 'custom-cache';
83+
writeProjectFile('index.ts', 'const value = 1;\n');
84+
writeProjectFile('custom-cache/nested/ignored.ts', 'const value=2');
85+
expect(runFmt(['--cache-location', cacheLocation, 'index.ts']).status).toBe(0);
86+
87+
const result = runFmt(['--cache-location', cacheLocation, '.']);
88+
89+
expect(result.status).toBe(0);
90+
expectWriteSummary(result.stdout, 2, 0);
91+
expect(readProjectFile('custom-cache/nested/ignored.ts')).toBe('const value=2');
92+
});
93+
94+
test('uses an explicit config root cache from a subdirectory', () => {
95+
const appPath = resolveProjectPath('packages/app');
96+
writeProjectFile('packages/app/index.ts', 'const value=1');
97+
98+
const result = runFmt(['index.ts', '--config', '../../rstack.config.ts'], appPath);
99+
100+
expect(result.status).toBe(0);
101+
expect(readProjectFile('packages/app/index.ts')).toBe('const value = 1;\n');
102+
expect(projectFileExists('.rstack/cache/fmt/v1.json')).toBe(true);
103+
expect(projectFileExists('packages/app/.rstack')).toBe(false);
104+
expect(JSON.parse(readProjectFile('.rstack/cache/fmt/v1.json'))).toMatchObject({
105+
files: {
106+
'packages/app/index.ts': [expect.any(String), expect.any(String), 'clean'],
107+
},
108+
});
109+
});
110+
111+
test('recovers from a corrupted cache', () => {
112+
writeProjectFile('index.ts', 'const value = 1;\n');
113+
const first = runFmt(['--check', 'index.ts']);
114+
writeProjectFile('.rstack/cache/fmt/v1.json', '{');
115+
116+
const second = runFmt(['--check', 'index.ts']);
117+
118+
expect(second.status).toBe(0);
119+
expect(normalizeDuration(second.stdout)).toBe(normalizeDuration(first.stdout));
120+
expect(second.stderr).toBe(first.stderr);
121+
expect(JSON.parse(readProjectFile('.rstack/cache/fmt/v1.json'))).toMatchObject({ version: 1 });
122+
});
123+
124+
test('formats without a writable cache directory', () => {
125+
writeProjectFile('.rstack', 'not a directory');
126+
writeProjectFile('index.ts', 'const value=1');
127+
128+
const result = runFmt(['index.ts']);
129+
130+
expect(result.status).toBe(0);
131+
expect(readProjectFile('index.ts')).toBe('const value = 1;\n');
132+
});
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import { expect, test } from 'rstack/test';
2+
import {
3+
expectWriteSummary,
4+
packageJsonSource,
5+
setupFmtTest,
6+
sortedPackageJson,
7+
} from './helpers.ts';
8+
9+
const { readProjectFile, runFmt, writeFixturePlugin, writeProjectFile } = setupFmtTest();
10+
11+
test('does not sort package.json by default', () => {
12+
writeProjectFile('package.json', packageJsonSource);
13+
14+
const result = runFmt(['package.json']);
15+
16+
expect(result.status).toBe(0);
17+
expect(readProjectFile('package.json')).toContain(
18+
'"dependencies": {\n "z": "1.0.0",\n "a": "1.0.0"',
19+
);
20+
});
21+
22+
test('sorts package.json with workers', () => {
23+
writeProjectFile(
24+
'rstack.config.ts',
25+
`import { define } from 'rstack';
26+
27+
define.fmt({ sortPackageJson: true });
28+
`,
29+
);
30+
writeProjectFile('package.json', packageJsonSource);
31+
writeProjectFile('packages/example/package.json', packageJsonSource);
32+
33+
const result = runFmt(['package.json', 'packages/example/package.json']);
34+
35+
expect(result.status).toBe(0);
36+
expect(result.stderr).toBe('');
37+
expect(readProjectFile('package.json')).toBe(sortedPackageJson);
38+
expect(readProjectFile('packages/example/package.json')).toBe(sortedPackageJson);
39+
});
40+
41+
test('supports configuring the worker count', () => {
42+
writeProjectFile('first.ts', 'const first="first"');
43+
writeProjectFile('second.ts', 'const second="second"');
44+
45+
const result = runFmt(['--parallel-workers', '1', 'first.ts', 'second.ts']);
46+
47+
expect(result.status).toBe(0);
48+
expectWriteSummary(result.stdout, 2, 2);
49+
expect(result.stderr).toBe('');
50+
expect(readProjectFile('first.ts')).toBe('const first = "first";\n');
51+
expect(readProjectFile('second.ts')).toBe('const second = "second";\n');
52+
});
53+
54+
test('does not load Prettier config or ignore files', () => {
55+
writeProjectFile('.prettierrc.json', '{ "singleQuote": true, "semi": false }\n');
56+
writeProjectFile('.prettierignore', 'index.ts\n');
57+
writeProjectFile('.editorconfig', 'root = true\n\n[*]\nindent_style = space\nindent_size = 8\n');
58+
writeProjectFile('index.ts', "function getMessage(){\n return 'hello'\n}");
59+
60+
const result = runFmt(['index.ts']);
61+
62+
expect(result.status).toBe(0);
63+
expectWriteSummary(result.stdout, 1, 1);
64+
expect(result.stderr).toBe('');
65+
expect(readProjectFile('index.ts')).toBe('function getMessage() {\n return "hello";\n}\n');
66+
});
67+
68+
test('applies repeated ignore paths', () => {
69+
writeProjectFile('.prettierignore', 'src/ignored-by-root.ts\n');
70+
writeProjectFile('config/extra.ignore', '../src/ignored-by-extra.ts\n');
71+
writeProjectFile('src/ignored-by-root.ts', 'const root="ignored"');
72+
writeProjectFile('src/ignored-by-extra.ts', 'const extra="ignored"');
73+
writeProjectFile('src/index.ts', 'const index="formatted"');
74+
75+
const result = runFmt([
76+
'--ignore-path',
77+
'.prettierignore',
78+
'--ignore-path=config/extra.ignore',
79+
'src/ignored-by-root.ts',
80+
'src/ignored-by-extra.ts',
81+
'src/index.ts',
82+
]);
83+
84+
expect(result.status).toBe(0);
85+
expectWriteSummary(result.stdout, 1, 1);
86+
expect(result.stderr).toBe('');
87+
expect(readProjectFile('src/ignored-by-root.ts')).toBe('const root="ignored"');
88+
expect(readProjectFile('src/ignored-by-extra.ts')).toBe('const extra="ignored"');
89+
expect(readProjectFile('src/index.ts')).toBe('const index = "formatted";\n');
90+
});
91+
92+
test('returns exit code 2 for an unreadable ignore path', () => {
93+
writeProjectFile('index.ts', 'const value=true');
94+
95+
const result = runFmt(['--ignore-path', 'missing.ignore', 'index.ts']);
96+
97+
expect(result.status).toBe(2);
98+
expect(result.stdout).toBe('');
99+
expect(result.stderr).toContain('Failed to read ignore file "missing.ignore".');
100+
expect(readProjectFile('index.ts')).toBe('const value=true');
101+
});
102+
103+
test('applies define.fmt options, overrides, ignore patterns, and globs', () => {
104+
writeProjectFile(
105+
'rstack.config.ts',
106+
`import { define } from 'rstack';
107+
108+
define.fmt({
109+
singleQuote: true,
110+
ignorePatterns: ['src/ignored.ts'],
111+
overrides: [
112+
{
113+
files: '*.test.ts',
114+
options: {
115+
semi: false,
116+
},
117+
},
118+
],
119+
});
120+
`,
121+
);
122+
writeProjectFile('src/index.ts', 'const message="hello"');
123+
writeProjectFile('src/index.test.ts', 'const test="test"');
124+
writeProjectFile('src/ignored.ts', 'const ignored="ignored"');
125+
writeProjectFile('src/index.js', 'const javascript="untouched"');
126+
127+
const result = runFmt(['--write', 'src/**/*.ts']);
128+
129+
expect(result.status).toBe(0);
130+
expectWriteSummary(result.stdout, 2, 2);
131+
expect(result.stderr).toBe('');
132+
expect(readProjectFile('src/index.ts')).toBe("const message = 'hello';\n");
133+
expect(readProjectFile('src/index.test.ts')).toBe("const test = 'test'\n");
134+
expect(readProjectFile('src/ignored.ts')).toBe('const ignored="ignored"');
135+
expect(readProjectFile('src/index.js')).toBe('const javascript="untouched"');
136+
});
137+
138+
test('uses an explicit Rstack config', () => {
139+
writeProjectFile(
140+
'custom.config.ts',
141+
`import { define } from 'rstack';
142+
143+
define.fmt({
144+
singleQuote: true,
145+
});
146+
`,
147+
);
148+
writeProjectFile('index.ts', 'const message="hello"');
149+
150+
const result = runFmt(['index.ts', '--config', 'custom.config.ts']);
151+
152+
expect(result.status).toBe(0);
153+
expectWriteSummary(result.stdout, 1, 1);
154+
expect(result.stderr).toBe('');
155+
expect(readProjectFile('index.ts')).toBe("const message = 'hello';\n");
156+
});
157+
158+
test('returns exit code 2 for config errors', () => {
159+
writeProjectFile('rstack.config.ts', 'throw new Error("invalid fmt config");\n');
160+
161+
const result = runFmt(['index.ts']);
162+
163+
expect(result.status).toBe(2);
164+
expect(result.stdout).toBe('');
165+
expect(result.stderr).toContain('invalid fmt config');
166+
});
167+
168+
test('formats with a project-local plugin in workers', () => {
169+
writeProjectFile(
170+
'rstack.config.ts',
171+
`import { define } from 'rstack';
172+
173+
define.fmt({
174+
plugins: ['prettier-plugin-fixture'],
175+
});
176+
`,
177+
);
178+
writeFixturePlugin();
179+
writeProjectFile('first.fixture', '{"first":true}');
180+
writeProjectFile('second.fixture', '{"second":true}');
181+
182+
const result = runFmt(['*.fixture']);
183+
184+
expect(result.status).toBe(0);
185+
expectWriteSummary(result.stdout, 2, 2);
186+
expect(result.stderr).toBe('');
187+
expect(readProjectFile('first.fixture')).toBe('{ "first": true }\n');
188+
expect(readProjectFile('second.fixture')).toBe('{ "second": true }\n');
189+
});
190+
191+
test('formats mixed plugin overrides in workers', () => {
192+
writeProjectFile(
193+
'rstack.config.ts',
194+
`import { define } from 'rstack';
195+
196+
define.fmt({
197+
overrides: [
198+
{
199+
files: '*.fixture',
200+
options: { plugins: ['prettier-plugin-fixture'] },
201+
},
202+
],
203+
});
204+
`,
205+
);
206+
writeFixturePlugin();
207+
writeProjectFile('data.fixture', '{"value":true}');
208+
writeProjectFile('index.ts', 'const value=true');
209+
210+
const result = runFmt(['data.fixture', 'index.ts']);
211+
212+
expect(result.status).toBe(0);
213+
expectWriteSummary(result.stdout, 2, 2);
214+
expect(result.stderr).toBe('');
215+
expect(readProjectFile('data.fixture')).toBe('{ "value": true }\n');
216+
expect(readProjectFile('index.ts')).toBe('const value = true;\n');
217+
});
218+
219+
test('returns exit code 2 for imported plugin objects', () => {
220+
writeProjectFile(
221+
'rstack.config.ts',
222+
`import { define } from 'rstack';
223+
224+
define.fmt({
225+
plugins: [{ languages: [] }],
226+
});
227+
`,
228+
);
229+
writeProjectFile('index.ts', 'const value=true');
230+
231+
const result = runFmt(['index.ts']);
232+
233+
expect(result.status).toBe(2);
234+
expect(result.stdout).toBe('');
235+
expect(result.stderr).toContain(
236+
'Prettier plugin objects are not supported. Use a package name, path, or URL instead.',
237+
);
238+
});

0 commit comments

Comments
 (0)