diff options
Diffstat (limited to 'packages/core/src/services')
| -rw-r--r-- | packages/core/src/services/fileDiscoveryService.test.ts | 253 | ||||
| -rw-r--r-- | packages/core/src/services/fileDiscoveryService.ts | 87 |
2 files changed, 340 insertions, 0 deletions
diff --git a/packages/core/src/services/fileDiscoveryService.test.ts b/packages/core/src/services/fileDiscoveryService.test.ts new file mode 100644 index 00000000..2ef83bfa --- /dev/null +++ b/packages/core/src/services/fileDiscoveryService.test.ts @@ -0,0 +1,253 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; +import type { Mocked } from 'vitest'; +import { FileDiscoveryService } from './fileDiscoveryService.js'; +import { GitIgnoreParser } from '../utils/gitIgnoreParser.js'; + +// Mock the GitIgnoreParser +vi.mock('../utils/gitIgnoreParser.js'); + +// Mock gitUtils module +vi.mock('../utils/gitUtils.js', () => ({ + isGitRepository: vi.fn(() => true), + findGitRoot: vi.fn(() => '/test/project'), +})); + +describe('FileDiscoveryService', () => { + let service: FileDiscoveryService; + let mockGitIgnoreParser: Mocked<GitIgnoreParser>; + const mockProjectRoot = '/test/project'; + + beforeEach(() => { + service = new FileDiscoveryService(mockProjectRoot); + + mockGitIgnoreParser = { + initialize: vi.fn(), + isIgnored: vi.fn(), + getIgnoredPatterns: vi.fn(() => ['.git/**', 'node_modules/**']), + parseGitIgnoreContent: vi.fn(), + } as unknown as Mocked<GitIgnoreParser>; + + vi.mocked(GitIgnoreParser).mockImplementation(() => mockGitIgnoreParser); + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('initialization', () => { + it('should initialize git ignore parser by default', async () => { + await service.initialize(); + + expect(GitIgnoreParser).toHaveBeenCalledWith(mockProjectRoot); + expect(mockGitIgnoreParser.initialize).toHaveBeenCalled(); + }); + + it('should not initialize git ignore parser when respectGitIgnore is false', async () => { + await service.initialize({ respectGitIgnore: false }); + + expect(GitIgnoreParser).not.toHaveBeenCalled(); + expect(mockGitIgnoreParser.initialize).not.toHaveBeenCalled(); + }); + + it('should handle initialization errors gracefully', async () => { + mockGitIgnoreParser.initialize.mockRejectedValue( + new Error('Init failed'), + ); + + await expect(service.initialize()).rejects.toThrow('Init failed'); + }); + }); + + describe('filterFiles', () => { + beforeEach(async () => { + mockGitIgnoreParser.isIgnored.mockImplementation( + (path: string) => + path.includes('node_modules') || path.includes('.git'), + ); + await service.initialize(); + }); + + it('should filter out git-ignored files by default', () => { + const files = [ + 'src/index.ts', + 'node_modules/package/index.js', + 'README.md', + '.git/config', + 'dist/bundle.js', + ]; + + const filtered = service.filterFiles(files); + + expect(filtered).toEqual(['src/index.ts', 'README.md', 'dist/bundle.js']); + }); + + it('should not filter files when respectGitIgnore is false', () => { + const files = [ + 'src/index.ts', + 'node_modules/package/index.js', + '.git/config', + ]; + + const filtered = service.filterFiles(files, { respectGitIgnore: false }); + + expect(filtered).toEqual(files); + }); + + it('should handle empty file list', () => { + const filtered = service.filterFiles([]); + expect(filtered).toEqual([]); + }); + }); + + describe('shouldIgnoreFile', () => { + beforeEach(async () => { + mockGitIgnoreParser.isIgnored.mockImplementation((path: string) => + path.includes('node_modules'), + ); + await service.initialize(); + }); + + it('should return true for git-ignored files', () => { + expect(service.shouldIgnoreFile('node_modules/package/index.js')).toBe( + true, + ); + }); + + it('should return false for non-ignored files', () => { + expect(service.shouldIgnoreFile('src/index.ts')).toBe(false); + }); + + it('should return false when respectGitIgnore is false', () => { + expect( + service.shouldIgnoreFile('node_modules/package/index.js', { + respectGitIgnore: false, + }), + ).toBe(false); + }); + + it('should return false when git ignore parser is not initialized', async () => { + const uninitializedService = new FileDiscoveryService(mockProjectRoot); + expect( + uninitializedService.shouldIgnoreFile('node_modules/package/index.js'), + ).toBe(false); + }); + }); + + describe('getIgnoreInfo', () => { + beforeEach(async () => { + await service.initialize(); + }); + + it('should return git ignored patterns', () => { + const info = service.getIgnoreInfo(); + + expect(info.gitIgnored).toEqual(['.git/**', 'node_modules/**']); + }); + + it('should return empty arrays when git ignore parser is not initialized', async () => { + const uninitializedService = new FileDiscoveryService(mockProjectRoot); + const info = uninitializedService.getIgnoreInfo(); + + expect(info.gitIgnored).toEqual([]); + }); + + it('should handle git ignore parser returning null patterns', async () => { + mockGitIgnoreParser.getIgnoredPatterns.mockReturnValue([] as string[]); + + const info = service.getIgnoreInfo(); + + expect(info.gitIgnored).toEqual([]); + }); + }); + + describe('isGitRepository', () => { + it('should return true when isGitRepo is explicitly set to true in options', () => { + const result = service.isGitRepository({ isGitRepo: true }); + expect(result).toBe(true); + }); + + it('should return false when isGitRepo is explicitly set to false in options', () => { + const result = service.isGitRepository({ isGitRepo: false }); + expect(result).toBe(false); + }); + + it('should use git utility function when isGitRepo is not specified', () => { + const result = service.isGitRepository(); + expect(result).toBe(true); // mocked to return true + }); + + it('should use git utility function when options are undefined', () => { + const result = service.isGitRepository(undefined); + expect(result).toBe(true); // mocked to return true + }); + }); + + describe('initialization with isGitRepo config', () => { + it('should initialize git ignore parser when isGitRepo is true in options', async () => { + await service.initialize({ isGitRepo: true }); + + expect(GitIgnoreParser).toHaveBeenCalledWith(mockProjectRoot); + expect(mockGitIgnoreParser.initialize).toHaveBeenCalled(); + }); + + it('should not initialize git ignore parser when isGitRepo is false in options', async () => { + await service.initialize({ isGitRepo: false }); + + expect(GitIgnoreParser).not.toHaveBeenCalled(); + expect(mockGitIgnoreParser.initialize).not.toHaveBeenCalled(); + }); + + it('should initialize git ignore parser when isGitRepo is not specified but respectGitIgnore is true', async () => { + await service.initialize({ respectGitIgnore: true }); + + expect(GitIgnoreParser).toHaveBeenCalledWith(mockProjectRoot); + expect(mockGitIgnoreParser.initialize).toHaveBeenCalled(); + }); + }); + + describe('shouldIgnoreFile with isGitRepo config', () => { + it('should respect isGitRepo option when checking if file should be ignored', async () => { + mockGitIgnoreParser.isIgnored.mockImplementation((path: string) => + path.includes('node_modules'), + ); + await service.initialize({ isGitRepo: true }); + + expect( + service.shouldIgnoreFile('node_modules/package/index.js', { + isGitRepo: true, + }), + ).toBe(true); + expect( + service.shouldIgnoreFile('node_modules/package/index.js', { + isGitRepo: false, + }), + ).toBe(false); + }); + }); + + describe('edge cases', () => { + it('should handle relative project root paths', () => { + const relativeService = new FileDiscoveryService('./relative/path'); + expect(relativeService).toBeInstanceOf(FileDiscoveryService); + }); + + it('should handle undefined options', async () => { + await service.initialize(undefined); + expect(GitIgnoreParser).toHaveBeenCalled(); + }); + + it('should handle filterFiles with undefined options', async () => { + await service.initialize(); + const files = ['src/index.ts']; + const filtered = service.filterFiles(files, undefined); + expect(filtered).toEqual(files); + }); + }); +}); diff --git a/packages/core/src/services/fileDiscoveryService.ts b/packages/core/src/services/fileDiscoveryService.ts new file mode 100644 index 00000000..5329507e --- /dev/null +++ b/packages/core/src/services/fileDiscoveryService.ts @@ -0,0 +1,87 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { GitIgnoreParser, GitIgnoreFilter } from '../utils/gitIgnoreParser.js'; +import { isGitRepository } from '../utils/gitUtils.js'; +import * as path from 'path'; + +export interface FileDiscoveryOptions { + respectGitIgnore?: boolean; + includeBuildArtifacts?: boolean; + isGitRepo?: boolean; +} + +export class FileDiscoveryService { + private gitIgnoreFilter: GitIgnoreFilter | null = null; + private projectRoot: string; + + constructor(projectRoot: string) { + this.projectRoot = path.resolve(projectRoot); + } + + async initialize(options: FileDiscoveryOptions = {}): Promise<void> { + const isGitRepo = options.isGitRepo ?? isGitRepository(this.projectRoot); + + if (options.respectGitIgnore !== false && isGitRepo) { + const parser = new GitIgnoreParser(this.projectRoot); + await parser.initialize(); + this.gitIgnoreFilter = parser; + } + } + + /** + * Filters a list of file paths based on git ignore rules + */ + filterFiles( + filePaths: string[], + options: FileDiscoveryOptions = {}, + ): string[] { + return filePaths.filter((filePath) => { + // Always respect git ignore unless explicitly disabled + if (options.respectGitIgnore !== false && this.gitIgnoreFilter) { + if (this.gitIgnoreFilter.isIgnored(filePath)) { + return false; + } + } + + return true; + }); + } + + /** + * Gets patterns that would be ignored for debugging/transparency + */ + getIgnoreInfo(): { gitIgnored: string[] } { + return { + gitIgnored: this.gitIgnoreFilter?.getIgnoredPatterns() || [], + }; + } + + /** + * Checks if a single file should be ignored + */ + shouldIgnoreFile( + filePath: string, + options: FileDiscoveryOptions = {}, + ): boolean { + const isGitRepo = options.isGitRepo ?? isGitRepository(this.projectRoot); + if ( + options.respectGitIgnore !== false && + isGitRepo && + this.gitIgnoreFilter + ) { + return this.gitIgnoreFilter.isIgnored(filePath); + } + return false; + } + + /** + * Returns whether the project is a git repository + */ + isGitRepository(options: FileDiscoveryOptions = {}): boolean { + return options.isGitRepo ?? isGitRepository(this.projectRoot); + } +} |
