Implement ComponentAnalyzerService - Analyze component distribution across projects
Migrated from llm/agent-buildkit#5 on 2025-10-12T20:16:17.377Z Original author: @group_286_bot_4df652bbc58e62e505f1a777cd8f21b8 | Created: 2025-10-11T04:03:23.221Z
ComponentAnalyzerService Implementation
Parent Issue
Related to #1 (Project Health Auditor Epic)
Objective
Implement ComponentAnalyzerService that analyzes component distribution across projects and identifies components that should be moved to studio-ui for centralization.
Contract (Port Interface)
Implements: `IComponentAnalyzerPort` from `/src/services/ports/project-health.port.ts`
Required Methods
```typescript interface IComponentAnalyzerPort { analyzeComponents(projectPath: string): Promise; identifyStudioUICandidates(projectPath: string): Promise<string[]>; } ```
Responsibilities (SINGLE RESPONSIBILITY)
ONLY analyze component distribution. Does NOT:
- Perform actual component migration (CleanupService)
- Analyze project structure (StructureAnalyzerService)
- Calculate health scores (HealthScoringService)
Implementation Details
File Location
`/src/services/domain/project-health/ComponentAnalyzerService.ts`
Component Detection Patterns
Must find components in these locations:
```typescript const COMPONENT_PATTERNS = [ 'components//*.{tsx,jsx,ts,js}', 'src/components//.{tsx,jsx,ts,js}', 'frontend/src/components/**/.{tsx,jsx,ts,js}', 'backend/src/components//*.{tsx,jsx,ts,js}', 'ui/components//*.{tsx,jsx,ts,js}', ];
const COMPONENT_FILE_PATTERNS = [ /^[A-Z][a-zA-Z0-9].(tsx|jsx)/, // React components (PascalCase) /\.component\.(tsx|jsx|ts|js)/, // Angular-style components /use[A-Z][a-zA-Z0-9].(ts|js)$/, // React hooks ]; ```
Algorithm: analyzeComponents()
```typescript async analyzeComponents(projectPath: string): Promise { const componentFolders: ComponentFolder[] = [];
// 1. Find all directories named "components" or "component" const dirs = await this.findDirectories(projectPath, /^components?$/i);
// 2. For each directory, count components for (const dir of dirs) { const components = await this.findComponents(dir);
componentFolders.push({
path: dir,
componentCount: components.length,
components: components.map(c => basename(c))
});
}
// 3. Identify studio-ui candidates const candidates = await this.identifyStudioUICandidates(projectPath);
return { projectName: basename(projectPath), componentFolders, shouldBeInStudioUI: candidates }; } ```
Algorithm: identifyStudioUICandidates()
```typescript async identifyStudioUICandidates(projectPath: string): Promise<string[]> { const candidates: string[] = []; const projectName = basename(projectPath);
// studio-ui is the centralized component library // Components should NOT be in other projects if they're reusable if (projectName === 'studio-ui') { return []; // studio-ui itself is not a candidate }
// Find all component directories const componentDirs = await this.findDirectories(projectPath, /^components?$/i);
for (const dir of componentDirs) { const components = await this.findComponents(dir);
for (const component of components) {
// Check if component is reusable (heuristics)
if (await this.isReusableComponent(component)) {
candidates.push(component);
}
}
}
return candidates; } ```
Reusable Component Detection Heuristics
```typescript private async isReusableComponent(componentPath: string): Promise { const content = await this.fs.readFile(componentPath, 'utf-8'); const filename = basename(componentPath);
// Generic UI components (Button, Input, Card, etc.) should be in studio-ui const GENERIC_UI_NAMES = [ 'Button', 'Input', 'Card', 'Modal', 'Form', 'Table', 'List', 'Grid', 'Layout', 'Header', 'Footer', 'Sidebar', 'Alert', 'Badge', 'Tooltip', 'Dropdown', 'Menu', 'Nav', 'Spinner', 'Loader', 'Icon', 'Avatar', 'Breadcrumb' ];
for (const name of GENERIC_UI_NAMES) { if (filename.startsWith(name)) { return true; } }
// Check for UI library imports (indicates generic component) const hasUILibraryImports = /from\s+'"/g.test(content);
// Check for business logic (indicates NOT reusable) const hasBusinessLogic = /(useAuth|useUser|useProject|useAgent|useDeploy)/g.test(content);
// Check if component is pure (no side effects) const isPure = !/(useEffect|useState|useContext|fetch|axios)/g.test(content);
// Reusable if: // - Has UI library imports // - OR is pure component // - AND has no business logic return (hasUILibraryImports || isPure) && !hasBusinessLogic; } ```
Helper Methods
```typescript private async findDirectories( rootPath: string, pattern: RegExp ): Promise<string[]> { const results: string[] = [];
async function scan(dir: string) { const entries = await this.fs.readdir(dir);
for (const entry of entries) {
const fullPath = join(dir, entry);
const stat = await this.fs.stat(fullPath);
if (stat.isDirectory()) {
if (pattern.test(entry)) {
results.push(fullPath);
}
if (!SKIP_DIRS.has(entry)) {
await scan(fullPath);
}
}
}
}
await scan(rootPath); return results; }
private async findComponents(dir: string): Promise<string[]> { const components: string[] = []; const entries = await this.fs.readdir(dir);
for (const entry of entries) { const fullPath = join(dir, entry); const stat = await this.fs.stat(fullPath);
if (stat.isDirectory()) {
// Recurse into subdirectories
const subComponents = await this.findComponents(fullPath);
components.push(...subComponents);
} else {
// Check if file is a component
for (const pattern of COMPONENT_FILE_PATTERNS) {
if (pattern.test(entry)) {
components.push(fullPath);
break;
}
}
}
}
return components; } ```
Dependencies
- IFileSystemPort: For file operations (inject via constructor)
- glob or fast-glob: For pattern matching
- Zod schemas: ComponentAnalysisSchema
Acceptance Criteria
-
Implements all 2 methods from IComponentAnalyzerPort -
Constructor accepts IFileSystemPort for testability -
Finds all component directories recursively -
Counts components correctly (React, Vue, Angular patterns) -
Identifies reusable components using heuristics -
Excludes studio-ui from candidate recommendations -
Returns data matching Zod schemas -
Unit tests with mock filesystem (>90% coverage) -
Integration tests with real component directories
Testing Strategy
```typescript describe('ComponentAnalyzerService', () => { let service: ComponentAnalyzerService; let mockFs: MockFileSystem;
beforeEach(() => { mockFs = new MockFileSystem(); service = new ComponentAnalyzerService(mockFs); });
it('finds all component directories', async () => { mockFs.setupDirectory('/project', { 'components/': { 'Button.tsx': 'export const Button = () => ', 'Input.tsx': 'export const Input = () => ' }, 'src/': { 'components/': { 'Modal.tsx': 'export const Modal = () =>
' } } });const result = await service.analyzeComponents('/project');
expect(result.componentFolders).toHaveLength(2);
expect(result.componentFolders[0].componentCount).toBe(2);
expect(result.componentFolders[1].componentCount).toBe(1);
});
it('identifies generic UI components as studio-ui candidates', async () => { mockFs.addFile('/project/components/Button.tsx', `import { Button as ChakraButton } from '@chakra-ui/react';\n` + `export const Button = () => ;` );
const candidates = await service.identifyStudioUICandidates('/project');
expect(candidates).toContain('/project/components/Button.tsx');
});
it('excludes business logic components from studio-ui candidates', async () => { mockFs.addFile('/project/components/UserProfile.tsx', `import { useAuth } from '../hooks';\n` + `export const UserProfile = () => { const user = useAuth(); }` );
const candidates = await service.identifyStudioUICandidates('/project');
expect(candidates).not.toContain('/project/components/UserProfile.tsx');
});
it('does not recommend studio-ui for studio-ui project itself', async () => { mockFs.addFile('/studio-ui/components/Button.tsx', 'export const Button = () => '); const candidates = await service.identifyStudioUICandidates('/studio-ui'); expect(candidates).toEqual([]); }); }); ```
Example Output
```json { "projectName": "agent-router", "componentFolders": [ { "path": "/agent-router/src/components", "componentCount": 3, "components": ["Button.tsx", "Card.tsx", "AgentList.tsx"] } ], "shouldBeInStudioUI": [ "/agent-router/src/components/Button.tsx", "/agent-router/src/components/Card.tsx" ] } ```
Estimated Effort
10 hours
- Implementation: 5 hours
- Tests: 4 hours
- Documentation: 1 hour
References
- Port: `/src/services/ports/project-health.port.ts:IComponentAnalyzerPort`
- Reference: `/src/services/domain/project-health/ProjectDiscoveryService.ts`
- Schemas: `/src/types/dto/project-health.schemas.ts` (ComponentAnalysisSchema)
- Context: studio-ui is the centralized component library for all UI components
Labels
`enhancement`, `service-implementation`, `components`, `health-monitoring`