UNPKG

ruch

Version:

Revolutionary React TypeScript CLI with hexagonal architecture & AI-powered development assistance. Create maintainable, scalable applications with domain-driven design and integrated AI tooling.

447 lines (394 loc) 14.2 kB
import { describe, it, expect, vi, beforeEach } from 'vitest'; import path from 'path'; import { extractImportStatements, extractDomainFromImportPath, scanDomainFiles, analyzeDomainDependencies, detectCircularDependencies, generateGraphData, type DomainNode } from '../src/utils/dependency-analyzer'; import type { FileSystem } from '../src/utils/file-operations'; // Mock FileSystem for tests const createMockFileSystem = (files: Record<string, string> = {}, directories: string[] = []): FileSystem => ({ writeFile: vi.fn() as any, readFile: vi.fn().mockImplementation(async (filePath: string) => { if (files[filePath]) { return files[filePath]; } throw new Error(`File not found: ${filePath}`); }) as any, readdir: vi.fn().mockImplementation(async (dirPath: string) => { const dirEntries = Object.keys(files) .filter(file => path.dirname(file) === dirPath) .map(file => path.basename(file)); const subDirs = directories .filter(dir => path.dirname(dir) === dirPath && dir !== dirPath) .map(dir => path.basename(dir)); return [...new Set([...dirEntries, ...subDirs])]; }) as any, stat: vi.fn().mockImplementation(async (filePath: string) => ({ isDirectory: () => directories.includes(filePath), isFile: () => files[filePath] !== undefined })) as any, remove: vi.fn() as any, mkdir: vi.fn() as any, exists: vi.fn().mockImplementation(async (filePath: string) => { return files[filePath] !== undefined || directories.includes(filePath); }), pathExists: vi.fn(), ensureDir: vi.fn() }); describe('Dependency Analyzer', () => { describe('extractImportStatements', () => { it('should extract import statements', () => { const content = ` import React from 'react'; import { useState, useEffect } from 'react'; import type { User } from '../types'; import * as utils from '../utils'; import { ApiService } from '../../api/ApiService'; export { UserService } from '../services/UserService'; export * from './types'; const someCode = "import fake from 'fake'"; `; const imports = extractImportStatements(content); expect(imports).toHaveLength(7); expect(imports[0]).toEqual({ statement: `import React from 'react';`, line: 2, from: 'react', type: 'import' }); expect(imports[1]).toEqual({ statement: `import { useState, useEffect } from 'react';`, line: 3, from: 'react', type: 'import' }); expect(imports[2]).toEqual({ statement: `import type { User } from '../types';`, line: 4, from: '../types', type: 'import' }); expect(imports[3]).toEqual({ statement: `import * as utils from '../utils';`, line: 5, from: '../utils', type: 'import' }); expect(imports[4]).toEqual({ statement: `import { ApiService } from '../../api/ApiService';`, line: 6, from: '../../api/ApiService', type: 'import' }); expect(imports[5]).toEqual({ statement: `export { UserService } from '../services/UserService';`, line: 7, from: '../services/UserService', type: 'export' }); }); it('should handle empty content', () => { const imports = extractImportStatements(''); expect(imports).toHaveLength(0); }); it('should handle content without imports', () => { const content = ` const x = 1; function test() { return "hello"; } `; const imports = extractImportStatements(content); expect(imports).toHaveLength(0); }); }); describe('extractDomainFromImportPath', () => { it('should extract domain from cross-domain import', () => { const currentDomainPath = '/project/src/domains/user/services/UserService.ts'; const importPath = '../../product/entities/Product'; const domain = extractDomainFromImportPath(importPath, currentDomainPath); expect(domain).toBe('product'); }); it('should return null for same-domain import', () => { const currentDomainPath = '/project/src/domains/user/services/UserService.ts'; const importPath = '../entities/User'; const domain = extractDomainFromImportPath(importPath, currentDomainPath); expect(domain).toBeNull(); }); it('should return null for external imports', () => { const currentDomainPath = '/project/src/domains/user/services/UserService.ts'; const importPath = 'react'; const domain = extractDomainFromImportPath(importPath, currentDomainPath); expect(domain).toBeNull(); }); it('should return null for non-domain imports', () => { const currentDomainPath = '/project/src/domains/user/services/UserService.ts'; const importPath = '../../../utils/helpers'; const domain = extractDomainFromImportPath(importPath, currentDomainPath); expect(domain).toBeNull(); }); }); describe('scanDomainFiles', () => { it('should scan and return TypeScript files in a domain', async () => { const files = { '/project/src/domains/user/entities/User.ts': 'content', '/project/src/domains/user/services/UserService.ts': 'content', '/project/src/domains/user/adapters/UserAdapter.tsx': 'content', '/project/src/domains/user/README.md': 'content', // Should be ignored '/project/src/domains/user/package.json': 'content' // Should be ignored }; const directories = [ '/project/src/domains/user', '/project/src/domains/user/entities', '/project/src/domains/user/services', '/project/src/domains/user/adapters' ]; const fileSystem = createMockFileSystem(files, directories); const result = await scanDomainFiles(fileSystem, '/project/src/domains/user'); expect(result).toHaveLength(3); expect(result).toContain('/project/src/domains/user/entities/User.ts'); expect(result).toContain('/project/src/domains/user/services/UserService.ts'); expect(result).toContain('/project/src/domains/user/adapters/UserAdapter.tsx'); }); it('should handle empty domain directory', async () => { const fileSystem = createMockFileSystem({}, ['/project/src/domains/empty']); const result = await scanDomainFiles(fileSystem, '/project/src/domains/empty'); expect(result).toHaveLength(0); }); it('should handle non-existent domain directory', async () => { const fileSystem = createMockFileSystem(); const result = await scanDomainFiles(fileSystem, '/project/src/domains/nonexistent'); expect(result).toHaveLength(0); }); }); describe('analyzeDomainDependencies', () => { it('should analyze dependencies correctly', async () => { const files = { '/project/src/domains/user/services/UserService.ts': ` import { Product } from '../../product/entities/Product'; import { ApiService } from '../../shared/api/ApiService'; import { UserEntity } from '../entities/User'; `, '/project/src/domains/user/entities/User.ts': ` import { Order } from '../../order/entities/Order'; ` }; const directories = [ '/project/src/domains/user', '/project/src/domains/user/services', '/project/src/domains/user/entities' ]; const fileSystem = createMockFileSystem(files, directories); const result = await analyzeDomainDependencies( fileSystem, '/project/src/domains/user', 'user' ); expect(result.dependencies).toHaveLength(3); expect(result.dependencies).toContain('product'); expect(result.dependencies).toContain('shared'); expect(result.dependencies).toContain('order'); expect(result.files).toHaveLength(2); }); it('should handle domain with no dependencies', async () => { const files = { '/project/src/domains/isolated/entities/Entity.ts': ` export interface Entity { id: string; } ` }; const directories = [ '/project/src/domains/isolated', '/project/src/domains/isolated/entities' ]; const fileSystem = createMockFileSystem(files, directories); const result = await analyzeDomainDependencies( fileSystem, '/project/src/domains/isolated', 'isolated' ); expect(result.dependencies).toHaveLength(0); expect(result.files).toHaveLength(1); }); }); describe('detectCircularDependencies', () => { it('should detect simple circular dependency', () => { const nodes: DomainNode[] = [ { id: 'user', name: 'user', type: 'feature', fileCount: 3, dependencies: ['product'], dependents: ['product'], files: [] }, { id: 'product', name: 'product', type: 'feature', fileCount: 2, dependencies: ['user'], dependents: ['user'], files: [] } ]; const cycles = detectCircularDependencies(nodes); expect(cycles).toHaveLength(1); expect(cycles[0]).toEqual(['user', 'product']); }); it('should detect complex circular dependency', () => { const nodes: DomainNode[] = [ { id: 'user', name: 'user', type: 'feature', fileCount: 3, dependencies: ['product'], dependents: ['order'], files: [] }, { id: 'product', name: 'product', type: 'feature', fileCount: 2, dependencies: ['order'], dependents: ['user'], files: [] }, { id: 'order', name: 'order', type: 'feature', fileCount: 4, dependencies: ['user'], dependents: ['product'], files: [] } ]; const cycles = detectCircularDependencies(nodes); expect(cycles).toHaveLength(1); expect(cycles[0]).toEqual(['user', 'product', 'order']); }); it('should return empty array when no cycles exist', () => { const nodes: DomainNode[] = [ { id: 'user', name: 'user', type: 'feature', fileCount: 3, dependencies: ['shared'], dependents: [], files: [] }, { id: 'product', name: 'product', type: 'feature', fileCount: 2, dependencies: ['shared'], dependents: [], files: [] }, { id: 'shared', name: 'shared', type: 'shared', fileCount: 1, dependencies: [], dependents: ['user', 'product'], files: [] } ]; const cycles = detectCircularDependencies(nodes); expect(cycles).toHaveLength(0); }); }); describe('generateGraphData', () => { it('should generate complete graph data', async () => { const files = { '/project/src/domains/user/entities/User.ts': ` import { Product } from '../../product/entities/Product'; `, '/project/src/domains/product/entities/Product.ts': ` export interface Product { id: string; } `, '/project/src/domains/shared/utils/helpers.ts': ` export const helper = () => {}; ` }; const directories = [ '/project/src', '/project/src/domains', '/project/src/domains/user', '/project/src/domains/user/entities', '/project/src/domains/product', '/project/src/domains/product/entities', '/project/src/domains/shared', '/project/src/domains/shared/utils' ]; const fileSystem = createMockFileSystem(files, directories); const result = await generateGraphData('/project', fileSystem); expect(result.nodes).toHaveLength(3); expect(result.edges).toHaveLength(1); expect(result.stats.totalDomains).toBe(3); expect(result.stats.totalDependencies).toBe(1); expect(result.stats.circularDependencyCount).toBe(0); const userNode = result.nodes.find(n => n.name === 'user'); expect(userNode).toBeDefined(); expect(userNode?.dependencies).toContain('product'); expect(userNode?.fileCount).toBe(1); const productNode = result.nodes.find(n => n.name === 'product'); expect(productNode).toBeDefined(); expect(productNode?.dependents).toContain('user'); const edge = result.edges[0]; expect(edge).toBeDefined(); if (edge) { expect(edge.from).toBe('user'); expect(edge.to).toBe('product'); expect(edge.type).toBe('direct-import'); } }); it('should handle project with no domains', async () => { const directories = ['/project/src']; const fileSystem = createMockFileSystem({}, directories); const result = await generateGraphData('/project', fileSystem); expect(result.nodes).toHaveLength(0); expect(result.edges).toHaveLength(0); expect(result.stats.totalDomains).toBe(0); expect(result.stats.totalDependencies).toBe(0); expect(result.stats.circularDependencyCount).toBe(0); }); it('should detect and count circular dependencies', async () => { const files = { '/project/src/domains/user/entities/User.ts': ` import { Product } from '../../product/entities/Product'; `, '/project/src/domains/product/entities/Product.ts': ` import { User } from '../../user/entities/User'; ` }; const directories = [ '/project/src', '/project/src/domains', '/project/src/domains/user', '/project/src/domains/user/entities', '/project/src/domains/product', '/project/src/domains/product/entities' ]; const fileSystem = createMockFileSystem(files, directories); const result = await generateGraphData('/project', fileSystem); expect(result.stats.circularDependencyCount).toBe(1); expect(result.circularDependencies).toHaveLength(1); }); }); });