Skip to content

Commit 7ee0d93

Browse files
committed
perf(fmt): prune ignored directories during discovery
1 parent 723dc5b commit 7ee0d93

6 files changed

Lines changed: 77 additions & 12 deletions

File tree

packages/rstack/src/fmt/discoverPaths.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ interface DiscoverFmtPathsOptions {
1111
/** Absolute directory used to resolve input paths. */
1212
cwd: string;
1313
patterns?: string[];
14+
/** Returns whether a scanned directory can be pruned before traversal. */
15+
isDirectoryIgnored?: (directoryPath: string) => boolean;
1416
}
1517

1618
const isErrnoException = (error: unknown): error is NodeJS.ErrnoException =>
@@ -201,6 +203,7 @@ class GitIgnoreMatcher {
201203
const createTraversalOptions = (
202204
gitIgnore: GitIgnoreMatcher,
203205
isIncluded?: (filePath: string) => boolean,
206+
isDirectoryIgnored?: (directoryPath: string) => boolean,
204207
) => {
205208
// tiny-readdir passes only a path to `ignore`, so retain the dirent type briefly.
206209
const directories = new Set<string>();
@@ -214,7 +217,7 @@ const createTraversalOptions = (
214217
}
215218

216219
if (isDirectory) {
217-
return gitIgnore.isIgnored(targetPath, true);
220+
return gitIgnore.isIgnored(targetPath, true) || isDirectoryIgnored?.(targetPath) === true;
218221
}
219222

220223
return (
@@ -343,6 +346,7 @@ const getTraversalRoots = (cwd: string, directories: string[], globs: string[]):
343346
const discoverFmtPaths = async ({
344347
cwd,
345348
patterns: inputPatterns,
349+
isDirectoryIgnored,
346350
}: DiscoverFmtPathsOptions): Promise<string[]> => {
347351
const patterns = inputPatterns?.length ? inputPatterns : ['.'];
348352
const {
@@ -366,7 +370,7 @@ const discoverFmtPaths = async ({
366370
}
367371

368372
await gitIgnore.loadThrough(rootPath);
369-
if (gitIgnore.isIgnored(rootPath, true)) {
373+
if (gitIgnore.isIgnored(rootPath, true) || isDirectoryIgnored?.(rootPath) === true) {
370374
return [];
371375
}
372376

@@ -384,7 +388,9 @@ const discoverFmtPaths = async ({
384388
return globMatchers.some((matches) => matches(relativePath));
385389
};
386390

387-
return (await readdir(rootPath, createTraversalOptions(gitIgnore, isIncluded))).files;
391+
return (
392+
await readdir(rootPath, createTraversalOptions(gitIgnore, isIncluded, isDirectoryIgnored))
393+
).files;
388394
}),
389395
);
390396

packages/rstack/src/fmt/discovery.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@ const createFileRequest = (filePath: string, config: ResolvedFmtConfig): FmtFile
88
options: resolveFmtOptions(filePath, config),
99
});
1010

11-
/** Discovers worker-ready files without reading Prettier config files or `.prettierignore`. */
11+
/** Discovers worker-ready files without automatically reading Prettier config or ignore files. */
1212
const discoverFmtFiles = async ({
1313
cwd,
1414
patterns,
1515
ignorePaths,
1616
config,
1717
}: DiscoverFmtFilesOptions): Promise<FmtFileRequest[]> => {
18-
const [candidates, isIgnored] = await Promise.all([
19-
discoverFmtPaths({ cwd, patterns }),
20-
createIgnoreMatcher({ config, cwd, ignorePaths }),
21-
]);
18+
const isIgnored = await createIgnoreMatcher({ config, cwd, ignorePaths });
19+
const candidates = await discoverFmtPaths({
20+
cwd,
21+
patterns,
22+
isDirectoryIgnored: (directoryPath) => isIgnored(directoryPath, true),
23+
});
2224
if (candidates.length === 0) {
2325
return [];
2426
}

packages/rstack/src/fmt/ignore.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { ResolvedFmtConfig } from './types.ts';
1111
*/
1212
const defaultIgnorePatterns = ['package-lock.json', 'pnpm-lock.yaml'];
1313

14-
type IgnoreMatcher = (filePath: string) => boolean;
14+
type IgnoreMatcher = (filePath: string, isDirectory?: boolean) => boolean;
1515

1616
interface CreateIgnoreMatcherOptions {
1717
config: ResolvedFmtConfig;
@@ -23,7 +23,8 @@ interface CreateIgnoreMatcherOptions {
2323
const createPatternMatcher = (rootPath: string, patterns: string): IgnoreMatcher => {
2424
const matches = fastIgnore(patterns);
2525

26-
return (filePath) => matches(path.relative(rootPath, filePath));
26+
return (filePath, isDirectory = false) =>
27+
matches(path.relative(rootPath, filePath), { isDirectory });
2728
};
2829

2930
const loadIgnoreMatcher = async (cwd: string, ignorePath: string): Promise<IgnoreMatcher> => {
@@ -55,8 +56,9 @@ const createIgnoreMatcher = async ({
5556
ignorePaths.map((ignorePath) => loadIgnoreMatcher(cwd, ignorePath)),
5657
);
5758

58-
return (filePath) =>
59-
configMatcher(filePath) || ignoreMatchers.some((matches) => matches(filePath));
59+
return (filePath, isDirectory = false) =>
60+
configMatcher(filePath, isDirectory) ||
61+
ignoreMatchers.some((matches) => matches(filePath, isDirectory));
6062
};
6163

6264
export { createIgnoreMatcher };

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,31 @@ test('lets explicit files bypass gitignore', async () => {
112112
});
113113
});
114114

115+
test('prunes directories with an external ignore matcher', async () => {
116+
await withTempProject(async (rootPath) => {
117+
writeProjectFile(rootPath, 'generated/nested/output.ts');
118+
writeProjectFile(rootPath, 'src/index.ts');
119+
const checkedDirectories: string[] = [];
120+
const generatedPath = path.join(rootPath, 'generated');
121+
const isDirectoryIgnored = (directoryPath: string): boolean => {
122+
checkedDirectories.push(path.relative(rootPath, directoryPath));
123+
return directoryPath === generatedPath;
124+
};
125+
126+
const files = await discoverFmtPaths({ cwd: rootPath, isDirectoryIgnored });
127+
const ignoredRoot = await discoverFmtPaths({
128+
cwd: rootPath,
129+
patterns: ['generated'],
130+
isDirectoryIgnored,
131+
});
132+
133+
expect(relativePaths(rootPath, files)).toEqual([path.join('src', 'index.ts')]);
134+
expect(ignoredRoot).toEqual([]);
135+
expect(checkedDirectories).toContain('generated');
136+
expect(checkedDirectories).not.toContain(path.join('generated', 'nested'));
137+
});
138+
});
139+
115140
test.runIf(process.platform !== 'win32')('does not follow file or directory symlinks', async () => {
116141
await withTempProject(async (rootPath) => {
117142
const targetPath = writeProjectFile(rootPath, 'target/index.ts');

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,27 @@ test('applies config ignore patterns outside the config root', async () => {
4747
});
4848
});
4949

50+
test('keeps files re-included by a CLI ignore file during directory traversal', async () => {
51+
await withTempProject(async (rootPath) => {
52+
writeProjectFile(rootPath, '.prettierignore', 'generated/*\n!generated/keep.ts\n');
53+
writeProjectFile(rootPath, 'generated/drop.ts');
54+
writeProjectFile(rootPath, 'generated/keep.ts');
55+
writeProjectFile(rootPath, 'src/index.ts');
56+
57+
const files = await discoverFmtFiles({
58+
cwd: rootPath,
59+
patterns: ['**/*.ts'],
60+
ignorePaths: ['.prettierignore'],
61+
config: normalizeFmtConfig(undefined, rootPath),
62+
});
63+
64+
expect(relativePaths(rootPath, files)).toEqual([
65+
path.join('generated', 'keep.ts'),
66+
path.join('src', 'index.ts'),
67+
]);
68+
});
69+
});
70+
5071
test('defers parser inference to workers and preserves an explicit parser', async () => {
5172
await withTempProject(async (rootPath) => {
5273
writeProjectFile(rootPath, 'index.js');

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ test('matches gitignore patterns relative to the config root', async () => {
2929
expect(isIgnored(path.join(rootPath, 'src/index.js'))).toBe(false);
3030
});
3131

32+
test('distinguishes directory-only patterns from files', async () => {
33+
const isIgnored = await createMatcher(['dist/']);
34+
const directoryPath = path.join(rootPath, 'dist');
35+
36+
expect(isIgnored(directoryPath)).toBe(false);
37+
expect(isIgnored(directoryPath, true)).toBe(true);
38+
expect(isIgnored(path.join(directoryPath, 'index.js'))).toBe(true);
39+
});
40+
3241
test('applies negated patterns in declaration order', async () => {
3342
const isIgnored = await createMatcher(['*.js', '!src/keep.js']);
3443
const isIgnoredAgain = await createMatcher(['*.js', '!src/keep.js', 'src/keep.js']);

0 commit comments

Comments
 (0)