Skip to content

Commit 2b6b85a

Browse files
committed
feat(fmt): support --ignore-path
1 parent 6494ba2 commit 2b6b85a

13 files changed

Lines changed: 255 additions & 59 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ doc_build
1212

1313
# Temp files
1414
test-temp-*
15+
TODO.md
16+
TODO-*.md
1517

1618
# IDE
1719
.vscode/*

packages/rstack/src/fmt/cli.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { FmtMode, FmtRunResult, ResolvedFmtConfig } from './types.ts';
1111
interface ParsedFmtCLIArgs {
1212
mode: FmtMode;
1313
patterns: string[];
14+
ignorePaths: string[];
1415
maxWorkers?: number;
1516
help: boolean;
1617
/** Path the stdin content is formatted as; it need not exist on disk. */
@@ -28,6 +29,7 @@ ${color.cyan('Options')}:
2829
--write Write formatted files in place (default)
2930
--check Check whether files are formatted
3031
--list-different Print paths of unformatted files
32+
--ignore-path <path> Path to an additional ignore file (repeatable)
3133
--parallel-workers <count> Number of parallel workers
3234
--stdin-filepath <path> Format stdin as if it were saved at <path>
3335
-h, --help Display this help message`;
@@ -57,6 +59,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => {
5759
check: { type: 'boolean' },
5860
'list-different': { type: 'boolean' },
5961
listDifferent: { type: 'boolean' },
62+
'ignore-path': { type: 'string', multiple: true },
6063
'parallel-workers': { type: 'string' },
6164
parallelWorkers: { type: 'string' },
6265
'stdin-filepath': { type: 'string' },
@@ -92,6 +95,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => {
9295
return {
9396
mode,
9497
patterns: positionals,
98+
ignorePaths: values['ignore-path'] ?? [],
9599
maxWorkers,
96100
help: values.help ?? false,
97101
stdinFilepath,
@@ -210,7 +214,7 @@ const runFmtCLI = async (args: string[]): Promise<void> => {
210214
// Argument errors are reported like every other failure so that a single
211215
// exit code identifies "rs fmt refused to run".
212216
try {
213-
const { help, maxWorkers, mode, patterns, stdinFilepath } = parseFmtCLIArgs(args);
217+
const { help, ignorePaths, maxWorkers, mode, patterns, stdinFilepath } = parseFmtCLIArgs(args);
214218
if (help) {
215219
logger.log(fmtHelpMessage);
216220
return;
@@ -221,12 +225,22 @@ const runFmtCLI = async (args: string[]): Promise<void> => {
221225
/* rspackChunkName: 'fmtStdin' */
222226
'./stdin.ts'
223227
);
224-
await runFmtStdin({ filepath: stdinFilepath, cwd, loadConfig: () => loadFmtConfig(cwd) });
228+
await runFmtStdin({
229+
filepath: stdinFilepath,
230+
cwd,
231+
ignorePaths,
232+
loadConfig: () => loadFmtConfig(cwd),
233+
});
225234
return;
226235
}
227236

228237
const config = await loadFmtConfig(cwd);
229-
const files = await discoverFmtFiles({ cwd, patterns, config });
238+
const files = await discoverFmtFiles({
239+
cwd,
240+
patterns,
241+
config,
242+
ignorePaths,
243+
});
230244

231245
if (files.length === 0) {
232246
if (mode !== 'list-different') {

packages/rstack/src/fmt/discovery.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { resolveFmtOptions } from './config.ts';
22
import { discoverFmtPaths } from './discoverPaths.ts';
3-
import { createFmtIgnoreMatcher } from './ignore.ts';
3+
import { createIgnoreMatcher } from './ignore.ts';
44
import type { DiscoverFmtFilesOptions, FmtFileRequest, ResolvedFmtConfig } from './types.ts';
55

66
const createFileRequest = (filePath: string, config: ResolvedFmtConfig): FmtFileRequest => ({
@@ -12,15 +12,18 @@ const createFileRequest = (filePath: string, config: ResolvedFmtConfig): FmtFile
1212
const discoverFmtFiles = async ({
1313
cwd,
1414
patterns,
15+
ignorePaths,
1516
config,
1617
}: DiscoverFmtFilesOptions): Promise<FmtFileRequest[]> => {
17-
const candidates = await discoverFmtPaths({ cwd, patterns });
18+
const [candidates, isIgnored] = await Promise.all([
19+
discoverFmtPaths({ cwd, patterns }),
20+
createIgnoreMatcher({ config, cwd, ignorePaths }),
21+
]);
1822
if (candidates.length === 0) {
1923
return [];
2024
}
2125

22-
const isFmtIgnored = createFmtIgnoreMatcher(config);
23-
const filePaths = candidates.filter((filePath) => !isFmtIgnored(filePath));
26+
const filePaths = candidates.filter((filePath) => !isIgnored(filePath));
2427
const files = filePaths.map((filePath) => createFileRequest(filePath, config));
2528
if (!files.some((file) => file.options.plugins?.length)) {
2629
return files;
@@ -32,7 +35,10 @@ const discoverFmtFiles = async ({
3235
);
3336
const resolvePlugins = createFmtPluginResolver(config.rootPath);
3437

35-
return files.map((file) => ({ ...file, options: resolvePlugins(file.options) }));
38+
return files.map((file) => ({
39+
...file,
40+
options: resolvePlugins(file.options),
41+
}));
3642
};
3743

3844
export { createFileRequest, discoverFmtFiles };

packages/rstack/src/fmt/ignore.ts

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { relative } from 'node:path';
1+
import { readFile } from 'node:fs/promises';
2+
import path from 'node:path';
23
import fastIgnore from 'fast-ignore';
34
import type { ResolvedFmtConfig } from './types.ts';
45

@@ -10,11 +11,52 @@ import type { ResolvedFmtConfig } from './types.ts';
1011
*/
1112
const defaultIgnorePatterns = ['package-lock.json', 'pnpm-lock.yaml'];
1213

13-
/** Creates a reusable matcher for default and config-level ignore patterns. */
14-
const createFmtIgnoreMatcher = (config: ResolvedFmtConfig): ((filePath: string) => boolean) => {
15-
const matches = fastIgnore([...defaultIgnorePatterns, ...config.ignorePatterns].join('\n'));
14+
type IgnoreMatcher = (filePath: string) => boolean;
1615

17-
return (filePath) => matches(relative(config.rootPath, filePath));
16+
interface CreateIgnoreMatcherOptions {
17+
config: ResolvedFmtConfig;
18+
/** Base directory for relative ignore paths. */
19+
cwd: string;
20+
ignorePaths?: string[];
21+
}
22+
23+
const createPatternMatcher = (rootPath: string, patterns: string): IgnoreMatcher => {
24+
const matches = fastIgnore(patterns);
25+
26+
return (filePath) => matches(path.relative(rootPath, filePath));
27+
};
28+
29+
const loadIgnoreMatcher = async (cwd: string, ignorePath: string): Promise<IgnoreMatcher> => {
30+
const filePath = path.resolve(cwd, ignorePath);
31+
let patterns: string;
32+
33+
try {
34+
patterns = await readFile(filePath, 'utf8');
35+
} catch (error) {
36+
throw new Error(`Failed to read ignore file "${ignorePath}".`, {
37+
cause: error,
38+
});
39+
}
40+
41+
return createPatternMatcher(path.dirname(filePath), patterns);
42+
};
43+
44+
/** Creates a reusable matcher for default, config-level, and CLI-provided ignore patterns. */
45+
const createIgnoreMatcher = async ({
46+
config,
47+
cwd,
48+
ignorePaths = [],
49+
}: CreateIgnoreMatcherOptions): Promise<IgnoreMatcher> => {
50+
const configMatcher = createPatternMatcher(
51+
config.rootPath,
52+
[...defaultIgnorePatterns, ...config.ignorePatterns].join('\n'),
53+
);
54+
const ignoreMatchers = await Promise.all(
55+
ignorePaths.map((ignorePath) => loadIgnoreMatcher(cwd, ignorePath)),
56+
);
57+
58+
return (filePath) =>
59+
configMatcher(filePath) || ignoreMatchers.some((matches) => matches(filePath));
1860
};
1961

20-
export { createFmtIgnoreMatcher };
62+
export { createIgnoreMatcher };

packages/rstack/src/fmt/stdin.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { resolve } from 'node:path';
22
import { createFileRequest } from './discovery.ts';
33
import { formatFmtSource } from './format.ts';
4-
import { createFmtIgnoreMatcher } from './ignore.ts';
4+
import { createIgnoreMatcher } from './ignore.ts';
55
import type { ResolvedFmtConfig } from './types.ts';
66

77
interface RunFmtStdinOptions {
88
/** Path used for per-file options and parser inference; it need not exist on disk. */
99
filepath: string;
1010
/** Absolute directory used to resolve the path. */
1111
cwd: string;
12+
/** Ignore files resolved from `cwd`. */
13+
ignorePaths?: string[];
1214
/** Loads the project config; its failures surface only after stdin is drained. */
1315
loadConfig: () => Promise<ResolvedFmtConfig>;
1416
}
@@ -45,7 +47,12 @@ const writeStdout = (output: string): Promise<void> =>
4547
* Formats stdin on the main thread and writes the result to stdout.
4648
* Nothing but the formatted output may reach stdout in this mode.
4749
*/
48-
const runFmtStdin = async ({ filepath, cwd, loadConfig }: RunFmtStdinOptions): Promise<void> => {
50+
const runFmtStdin = async ({
51+
filepath,
52+
cwd,
53+
ignorePaths,
54+
loadConfig,
55+
}: RunFmtStdinOptions): Promise<void> => {
4956
const configPromise = loadConfig();
5057
// Drain stdin before surfacing any failure, otherwise a writer that already
5158
// queued more than the pipe buffer sees EPIPE instead of the real error.
@@ -54,7 +61,12 @@ const runFmtStdin = async ({ filepath, cwd, loadConfig }: RunFmtStdinOptions): P
5461
const config = await configPromise;
5562

5663
const absolutePath = resolve(cwd, filepath);
57-
if (createFmtIgnoreMatcher(config)(absolutePath)) {
64+
const isIgnored = await createIgnoreMatcher({
65+
config,
66+
cwd,
67+
ignorePaths,
68+
});
69+
if (isIgnored(absolutePath)) {
5870
await writeStdout(source);
5971
return;
6072
}
@@ -69,7 +81,10 @@ const runFmtStdin = async ({ filepath, cwd, loadConfig }: RunFmtStdinOptions): P
6981
/* rspackChunkName: 'fmtPlugins' */
7082
'./plugins.ts'
7183
);
72-
file = { ...file, options: createFmtPluginResolver(config.rootPath)(file.options) };
84+
file = {
85+
...file,
86+
options: createFmtPluginResolver(config.rootPath)(file.options),
87+
};
7388
}
7489

7590
const result = await formatFmtSource(file, () => source);

packages/rstack/src/fmt/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ interface DiscoverFmtFilesOptions {
5656
cwd: string;
5757
/** Files, directories, and positive or negative globs. Defaults to the current directory. */
5858
patterns?: string[];
59+
/** Ignore files resolved from `cwd`; each file's patterns are relative to its own directory. */
60+
ignorePaths?: string[];
5961
/** Resolved project config applied to discovered files. */
6062
config: ResolvedFmtConfig;
6163
}

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,41 @@ test('does not load Prettier config or ignore files', () => {
195195
expect(readProjectFile('index.ts')).toBe('function getMessage() {\n return "hello";\n}\n');
196196
});
197197

198+
test('applies repeated ignore paths to explicit files', () => {
199+
writeProjectFile('.prettierignore', 'src/ignored-by-root.ts\n');
200+
writeProjectFile('config/extra.ignore', '../src/ignored-by-extra.ts\n');
201+
writeProjectFile('src/ignored-by-root.ts', 'const root="ignored"');
202+
writeProjectFile('src/ignored-by-extra.ts', 'const extra="ignored"');
203+
writeProjectFile('src/index.ts', 'const index="formatted"');
204+
205+
const result = runFmt([
206+
'--ignore-path',
207+
'.prettierignore',
208+
'--ignore-path=config/extra.ignore',
209+
'src/ignored-by-root.ts',
210+
'src/ignored-by-extra.ts',
211+
'src/index.ts',
212+
]);
213+
214+
expect(result.status).toBe(0);
215+
expectWriteSummary(result.stdout, 1, 1);
216+
expect(result.stderr).toBe('');
217+
expect(readProjectFile('src/ignored-by-root.ts')).toBe('const root="ignored"');
218+
expect(readProjectFile('src/ignored-by-extra.ts')).toBe('const extra="ignored"');
219+
expect(readProjectFile('src/index.ts')).toBe('const index = "formatted";\n');
220+
});
221+
222+
test('returns exit code 2 for an unreadable ignore path', () => {
223+
writeProjectFile('index.ts', 'const value=true');
224+
225+
const result = runFmt(['--ignore-path', 'missing.ignore', 'index.ts']);
226+
227+
expect(result.status).toBe(2);
228+
expect(result.stdout).toBe('');
229+
expect(result.stderr).toContain('Failed to read ignore file "missing.ignore".');
230+
expect(readProjectFile('index.ts')).toBe('const value=true');
231+
});
232+
198233
test('applies define.fmt options, overrides, ignore patterns, and globs', () => {
199234
writeProjectFile(
200235
'rstack.config.ts',
@@ -469,6 +504,20 @@ define.fmt({ ignorePatterns: ['src/ignored.ts'] });
469504
expect(result.stderr).toBe('');
470505
});
471506

507+
test('echoes stdin paths ignored by --ignore-path', () => {
508+
writeProjectFile('.prettierignore', 'src/ignored.ts\n');
509+
510+
const source = 'const ignored="ignored"';
511+
const result = runFmtStdin(
512+
['--ignore-path', '.prettierignore', '--stdin-filepath', 'src/ignored.ts'],
513+
source,
514+
);
515+
516+
expect(result.status).toBe(0);
517+
expect(result.stdout).toBe(source);
518+
expect(result.stderr).toBe('');
519+
});
520+
472521
test('echoes stdin for default ignored lock files', () => {
473522
const source = 'lockfileVersion: "9.0"\n';
474523
const result = runFmtStdin(['--stdin-filepath', 'pnpm-lock.yaml'], source);

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ test('uses write mode by default', () => {
2121
expect(parseFmtCLIArgs([])).toEqual({
2222
mode: 'write',
2323
patterns: [],
24+
ignorePaths: [],
2425
maxWorkers: undefined,
2526
help: false,
2627
});
@@ -35,6 +36,7 @@ test.each([
3536
expect(parseFmtCLIArgs([option])).toEqual({
3637
mode,
3738
patterns: [],
39+
ignorePaths: [],
3840
maxWorkers: undefined,
3941
help: false,
4042
});
@@ -46,6 +48,7 @@ test.each(['--parallel-workers', '--parallelWorkers'])(
4648
expect(parseFmtCLIArgs([option, '3'])).toEqual({
4749
mode: 'write',
4850
patterns: [],
51+
ignorePaths: [],
4952
maxWorkers: 3,
5053
help: false,
5154
});
@@ -71,6 +74,7 @@ test('preserves file paths and globs', () => {
7174
expect(parseFmtCLIArgs([patterns[0], '--check', ...patterns.slice(1)])).toEqual({
7275
mode: 'check',
7376
patterns,
77+
ignorePaths: [],
7478
maxWorkers: undefined,
7579
help: false,
7680
});
@@ -80,6 +84,7 @@ test('treats arguments after the terminator as paths', () => {
8084
expect(parseFmtCLIArgs(['--check', '--', '--write', '--help'])).toEqual({
8185
mode: 'check',
8286
patterns: ['--write', '--help'],
87+
ignorePaths: [],
8388
maxWorkers: undefined,
8489
help: false,
8590
});
@@ -89,10 +94,18 @@ test.each(['--help', '-h'])('parses %s', (option) => {
8994
expect(parseFmtCLIArgs([option]).help).toBe(true);
9095
});
9196

97+
test('collects repeated ignore paths', () => {
98+
expect(
99+
parseFmtCLIArgs(['--ignore-path', '.prettierignore', '--ignore-path=config/format.ignore'])
100+
.ignorePaths,
101+
).toEqual(['.prettierignore', 'config/format.ignore']);
102+
});
103+
92104
test.each(['--stdin-filepath', '--stdinFilepath'])('parses %s', (option) => {
93105
expect(parseFmtCLIArgs([option, 'src/index.ts'])).toEqual({
94106
mode: 'write',
95107
patterns: [],
108+
ignorePaths: [],
96109
maxWorkers: undefined,
97110
help: false,
98111
stdinFilepath: 'src/index.ts',
@@ -103,6 +116,7 @@ test('accepts a worker count with --stdin-filepath', () => {
103116
expect(parseFmtCLIArgs(['--stdin-filepath', 'index.ts', '--parallel-workers', '2'])).toEqual({
104117
mode: 'write',
105118
patterns: [],
119+
ignorePaths: [],
106120
maxWorkers: 2,
107121
help: false,
108122
stdinFilepath: 'index.ts',
@@ -129,6 +143,7 @@ test('provides command help', () => {
129143
expect(fmtHelpMessage).toContain('--write');
130144
expect(fmtHelpMessage).toContain('--check');
131145
expect(fmtHelpMessage).toContain('--list-different');
146+
expect(fmtHelpMessage).toContain('--ignore-path <path>');
132147
expect(fmtHelpMessage).toContain('--parallel-workers <count>');
133148
expect(fmtHelpMessage).toContain('--stdin-filepath <path>');
134149
expect(fmtHelpMessage).toContain('-h, --help');

0 commit comments

Comments
 (0)