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
12 changes: 9 additions & 3 deletions packages/rstack/src/fmt/discoverPaths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ interface DiscoverFmtPathsOptions {
/** Absolute directory used to resolve input paths. */
cwd: string;
patterns?: string[];
/** Returns whether a scanned directory can be pruned before traversal. */
isDirectoryIgnored?: (directoryPath: string) => boolean;
}

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

if (isDirectory) {
return gitIgnore.isIgnored(targetPath, true);
return gitIgnore.isIgnored(targetPath, true) || isDirectoryIgnored?.(targetPath) === true;
}

return (
Expand Down Expand Up @@ -343,6 +346,7 @@ const getTraversalRoots = (cwd: string, directories: string[], globs: string[]):
const discoverFmtPaths = async ({
cwd,
patterns: inputPatterns,
isDirectoryIgnored,
}: DiscoverFmtPathsOptions): Promise<string[]> => {
const patterns = inputPatterns?.length ? inputPatterns : ['.'];
const {
Expand All @@ -366,7 +370,7 @@ const discoverFmtPaths = async ({
}

await gitIgnore.loadThrough(rootPath);
if (gitIgnore.isIgnored(rootPath, true)) {
if (gitIgnore.isIgnored(rootPath, true) || isDirectoryIgnored?.(rootPath) === true) {
return [];
}

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

return (await readdir(rootPath, createTraversalOptions(gitIgnore, isIncluded))).files;
return (
await readdir(rootPath, createTraversalOptions(gitIgnore, isIncluded, isDirectoryIgnored))
).files;
}),
);

Expand Down
12 changes: 7 additions & 5 deletions packages/rstack/src/fmt/discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@ const createFileRequest = (filePath: string, config: ResolvedFmtConfig): FmtFile
options: resolveFmtOptions(filePath, config),
});

/** Discovers worker-ready files without reading Prettier config files or `.prettierignore`. */
/** Discovers worker-ready files without automatically reading Prettier config or ignore files. */
const discoverFmtFiles = async ({
cwd,
patterns,
ignorePaths,
config,
}: DiscoverFmtFilesOptions): Promise<FmtFileRequest[]> => {
const [candidates, isIgnored] = await Promise.all([
discoverFmtPaths({ cwd, patterns }),
createIgnoreMatcher({ config, cwd, ignorePaths }),
]);
const isIgnored = await createIgnoreMatcher({ config, cwd, ignorePaths });
const candidates = await discoverFmtPaths({
cwd,
patterns,
isDirectoryIgnored: (directoryPath) => isIgnored(directoryPath, true),
});
if (candidates.length === 0) {
return [];
}
Expand Down
10 changes: 6 additions & 4 deletions packages/rstack/src/fmt/ignore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { ResolvedFmtConfig } from './types.ts';
*/
const defaultIgnorePatterns = ['package-lock.json', 'pnpm-lock.yaml'];

type IgnoreMatcher = (filePath: string) => boolean;
type IgnoreMatcher = (filePath: string, isDirectory?: boolean) => boolean;

interface CreateIgnoreMatcherOptions {
config: ResolvedFmtConfig;
Expand All @@ -23,7 +23,8 @@ interface CreateIgnoreMatcherOptions {
const createPatternMatcher = (rootPath: string, patterns: string): IgnoreMatcher => {
const matches = fastIgnore(patterns);

return (filePath) => matches(path.relative(rootPath, filePath));
return (filePath, isDirectory = false) =>
matches(path.relative(rootPath, filePath), { isDirectory });
};

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

return (filePath) =>
configMatcher(filePath) || ignoreMatchers.some((matches) => matches(filePath));
return (filePath, isDirectory = false) =>
configMatcher(filePath, isDirectory) ||
ignoreMatchers.some((matches) => matches(filePath, isDirectory));
};

export { createIgnoreMatcher };
25 changes: 25 additions & 0 deletions packages/rstack/tests/fmt/discoverPaths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,31 @@ test('lets explicit files bypass gitignore', async () => {
});
});

test('prunes directories with an external ignore matcher', async () => {
await withTempProject(async (rootPath) => {
writeProjectFile(rootPath, 'generated/nested/output.ts');
writeProjectFile(rootPath, 'src/index.ts');
const checkedDirectories: string[] = [];
const generatedPath = path.join(rootPath, 'generated');
const isDirectoryIgnored = (directoryPath: string): boolean => {
checkedDirectories.push(path.relative(rootPath, directoryPath));
return directoryPath === generatedPath;
};

const files = await discoverFmtPaths({ cwd: rootPath, isDirectoryIgnored });
const ignoredRoot = await discoverFmtPaths({
cwd: rootPath,
patterns: ['generated'],
isDirectoryIgnored,
});

expect(relativePaths(rootPath, files)).toEqual([path.join('src', 'index.ts')]);
expect(ignoredRoot).toEqual([]);
expect(checkedDirectories).toContain('generated');
expect(checkedDirectories).not.toContain(path.join('generated', 'nested'));
});
});

test.runIf(process.platform !== 'win32')('does not follow file or directory symlinks', async () => {
await withTempProject(async (rootPath) => {
const targetPath = writeProjectFile(rootPath, 'target/index.ts');
Expand Down
21 changes: 21 additions & 0 deletions packages/rstack/tests/fmt/discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,27 @@ test('applies config ignore patterns outside the config root', async () => {
});
});

test('keeps files re-included by a CLI ignore file during directory traversal', async () => {
await withTempProject(async (rootPath) => {
writeProjectFile(rootPath, '.prettierignore', 'generated/*\n!generated/keep.ts\n');
writeProjectFile(rootPath, 'generated/drop.ts');
writeProjectFile(rootPath, 'generated/keep.ts');
writeProjectFile(rootPath, 'src/index.ts');

const files = await discoverFmtFiles({
cwd: rootPath,
patterns: ['**/*.ts'],
ignorePaths: ['.prettierignore'],
config: normalizeFmtConfig(undefined, rootPath),
});

expect(relativePaths(rootPath, files)).toEqual([
path.join('generated', 'keep.ts'),
path.join('src', 'index.ts'),
]);
});
});

test('defers parser inference to workers and preserves an explicit parser', async () => {
await withTempProject(async (rootPath) => {
writeProjectFile(rootPath, 'index.js');
Expand Down
9 changes: 9 additions & 0 deletions packages/rstack/tests/fmt/ignore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ test('matches gitignore patterns relative to the config root', async () => {
expect(isIgnored(path.join(rootPath, 'src/index.js'))).toBe(false);
});

test('distinguishes directory-only patterns from files', async () => {
const isIgnored = await createMatcher(['dist/']);
const directoryPath = path.join(rootPath, 'dist');

expect(isIgnored(directoryPath)).toBe(false);
expect(isIgnored(directoryPath, true)).toBe(true);
expect(isIgnored(path.join(directoryPath, 'index.js'))).toBe(true);
});

test('applies negated patterns in declaration order', async () => {
const isIgnored = await createMatcher(['*.js', '!src/keep.js']);
const isIgnoredAgain = await createMatcher(['*.js', '!src/keep.js', 'src/keep.js']);
Expand Down