Skip to content

Commit 5c5770f

Browse files
feat: implement discoverFiles function with tests for glob pattern matching and ignoring specified files
1 parent ffad47d commit 5c5770f

File tree

6 files changed

+97
-19
lines changed

6 files changed

+97
-19
lines changed

docs/tasks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ Use Context7 MCP for up to date documentation.
109109

110110
## 3) Repo scanning and matching
111111

112-
10. [ ] **Glob discovery**
112+
10. [x] **Glob discovery**
113113
Lib: `fast-glob`. Ignore `node_modules`, `.git`, `dist`.
114114
Verify: Unit test ensures correct file set.
115115

package-lock.json

Lines changed: 1 addition & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"dependencies": {
4141
"@actions/core": "^1.10.1",
4242
"cheerio": "^1.1.2",
43+
"fast-glob": "^3.3.3",
4344
"semver": "^7.7.3",
4445
"undici": "^7.16.0",
4546
"zod": "^4.1.12"

src/scanning/glob-discovery.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import fg from 'fast-glob';
2+
3+
interface DiscoverFilesOptions {
4+
/** Absolute path to the root directory where globbing should occur. */
5+
root: string;
6+
/** Glob patterns to match relative to the root. */
7+
patterns: string[];
8+
/** Additional ignore globs to apply on top of the defaults. */
9+
ignore?: string[];
10+
/** Whether to follow symbolic links. Defaults to false. */
11+
followSymbolicLinks?: boolean;
12+
}
13+
14+
const DEFAULT_IGNORES = ['**/node_modules/**', '**/.git/**', '**/dist/**'];
15+
16+
export async function discoverFiles(options: DiscoverFilesOptions): Promise<string[]> {
17+
const { root, patterns, ignore = [], followSymbolicLinks = false } = options;
18+
19+
if (!Array.isArray(patterns) || patterns.length === 0) {
20+
throw new Error('discoverFiles requires at least one glob pattern.');
21+
}
22+
23+
const entries = await fg(patterns, {
24+
cwd: root,
25+
ignore: [...DEFAULT_IGNORES, ...ignore],
26+
followSymbolicLinks,
27+
dot: true,
28+
onlyFiles: true,
29+
unique: true,
30+
});
31+
32+
return [...entries].sort();
33+
}

src/scanning/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { discoverFiles } from './glob-discovery';

tests/glob-discovery.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3+
import { tmpdir } from 'node:os';
4+
import path from 'node:path';
5+
6+
import { discoverFiles } from '../src/scanning/glob-discovery';
7+
8+
let tempDir: string;
9+
10+
const touch = async (filePath: string): Promise<void> => {
11+
await mkdir(path.dirname(filePath), { recursive: true });
12+
await writeFile(filePath, 'placeholder');
13+
};
14+
15+
beforeEach(async () => {
16+
tempDir = await mkdtemp(path.join(tmpdir(), 'glob-discovery-'));
17+
});
18+
19+
afterEach(async () => {
20+
await rm(tempDir, { recursive: true, force: true });
21+
});
22+
23+
describe('discoverFiles', () => {
24+
it('returns matched files while respecting default ignores', async () => {
25+
await touch(path.join(tempDir, 'workflow/pipeline.yml'));
26+
await touch(path.join(tempDir, 'docs/readme.txt'));
27+
await touch(path.join(tempDir, 'node_modules/package/ignored.yml'));
28+
await touch(path.join(tempDir, '.git/config'));
29+
await touch(path.join(tempDir, 'dist/output.txt'));
30+
31+
const files = await discoverFiles({
32+
root: tempDir,
33+
patterns: ['**/*.yml', '**/*.txt'],
34+
});
35+
36+
expect(files).toEqual(['docs/readme.txt', 'workflow/pipeline.yml']);
37+
});
38+
39+
it('supports additional ignore patterns', async () => {
40+
await touch(path.join(tempDir, 'src/app.py'));
41+
await touch(path.join(tempDir, 'src/tests/app_test.py'));
42+
43+
const files = await discoverFiles({
44+
root: tempDir,
45+
patterns: ['**/*.py'],
46+
ignore: ['**/tests/**'],
47+
});
48+
49+
expect(files).toEqual(['src/app.py']);
50+
});
51+
52+
it('throws when no patterns are provided', async () => {
53+
await expect(
54+
discoverFiles({
55+
root: tempDir,
56+
patterns: [],
57+
}),
58+
).rejects.toThrow(/at least one glob pattern/);
59+
});
60+
});

0 commit comments

Comments
 (0)