UNPKG

filetree-pro

Version:

A powerful file tree generator for VS Code and Cursor. Generate beautiful file trees in multiple formats with smart exclusions and custom configurations.

773 lines (627 loc) • 19.2 kB
# šŸ›ļø FileTree Pro - Architecture Improvement Proposal **Version:** 1.0 **Date:** October 17, 2025 **Status:** Proposed --- ## šŸ“Œ Overview This document proposes architectural improvements to make FileTree Pro more scalable, maintainable, and production-ready. --- ## šŸŽÆ Current vs. Proposed Architecture ### Current Architecture (As-Is) ```mermaid flowchart TB subgraph "Current State" EXT[Extension<br/>Partially initialized] CMD[Commands<br/>1100+ lines] subgraph "Services" FS[FileSystemService<br/>No cache mgmt] CP[CopilotService<br/>No rate limit] AN[AnalyticsService] end FTP[FileTreeProvider<br/>Not registered] end EXT -.->|Missing| FTP CMD -->|Duplicate logic| FS style EXT fill:#f44336,color:#fff style CMD fill:#ff9800,color:#000 style FS fill:#ff9800,color:#000 style CP fill:#ff9800,color:#000 style FTP fill:#f44336,color:#fff ``` ### Proposed Architecture (To-Be) ```mermaid flowchart TB subgraph "VS Code Extension" EXT[Extension Entry<br/>Properly initialized] subgraph "Command Layer" GEN[GenerateTreeCommand] CONV[ConvertTextCommand] EXP[ExportCommand] end subgraph "Service Layer" FS[FileSystemService<br/>+ Cache Management<br/>+ Path Validation] CP[CopilotService<br/>+ Rate Limiting<br/>+ Size Validation] AN[AnalyticsService<br/>+ Performance Metrics] SEC[SecurityService<br/>NEW] end subgraph "Provider Layer" FTP[FileTreeProvider<br/>+ Lazy Loading<br/>Registered] end subgraph "Formatter Layer" MD[MarkdownFormatter] JSON[JSONFormatter] SVG[SVGFormatter] ASCII[ASCIIFormatter] end subgraph "Utility Layer" FU[FileUtils<br/>Single icon map] ERR[ErrorHandler<br/>NEW] CACHE[CacheManager<br/>NEW] end end EXT --> GEN EXT --> CONV EXT --> EXP EXT --> FTP GEN --> FS GEN --> MD GEN --> JSON GEN --> SVG GEN --> ASCII FTP --> FS FTP --> CP FTP --> AN FS --> SEC FS --> CACHE FS --> FU CP --> SEC CP --> CACHE GEN -.->|Uses| ERR FS -.->|Uses| ERR CP -.->|Uses| ERR style EXT fill:#4CAF50,color:#fff style GEN fill:#2196F3,color:#fff style FS fill:#4CAF50,color:#fff style CP fill:#4CAF50,color:#fff style FTP fill:#4CAF50,color:#fff style SEC fill:#4CAF50,color:#fff style ERR fill:#4CAF50,color:#fff style CACHE fill:#4CAF50,color:#fff ``` --- ## šŸ“¦ Proposed Module Structure ### Before (Current) ``` src/ ā”œā”€ā”€ extension.ts (45 lines) ā”œā”€ā”€ extension-simple.ts (13 lines) āŒ Unused ā”œā”€ā”€ types.ts (165 lines) ā”œā”€ā”€ commands/ │ └── commands.ts (1106 lines) āš ļø Too large! ā”œā”€ā”€ providers/ │ └── fileTreeProvider.ts (227 lines) ā”œā”€ā”€ services/ │ ā”œā”€ā”€ fileSystemService.ts (242 lines) │ ā”œā”€ā”€ copilotService.ts (239 lines) │ └── analyticsService.ts (276 lines) └── utils/ └── fileUtils.ts (287 lines) ``` ### After (Proposed) ``` src/ ā”œā”€ā”€ extension.ts (100 lines) āœ… Properly initialized ā”œā”€ā”€ types/ │ ā”œā”€ā”€ index.ts (Export all types) │ ā”œā”€ā”€ fileTree.types.ts │ ā”œā”€ā”€ service.types.ts │ └── config.types.ts ā”œā”€ā”€ commands/ │ ā”œā”€ā”€ index.ts (Export all commands) │ ā”œā”€ā”€ generateTreeCommand.ts (150 lines) │ ā”œā”€ā”€ convertTextCommand.ts (80 lines) │ └── exportCommand.ts (100 lines) ā”œā”€ā”€ formatters/ │ ā”œā”€ā”€ index.ts (Export all formatters) │ ā”œā”€ā”€ markdownFormatter.ts (200 lines) │ ā”œā”€ā”€ jsonFormatter.ts (150 lines) │ ā”œā”€ā”€ svgFormatter.ts (300 lines) │ └── asciiFormatter.ts (150 lines) ā”œā”€ā”€ providers/ │ └── fileTreeProvider.ts (250 lines) āœ… Enhanced ā”œā”€ā”€ services/ │ ā”œā”€ā”€ fileSystemService.ts (300 lines) āœ… Enhanced │ ā”œā”€ā”€ copilotService.ts (280 lines) āœ… Enhanced │ ā”œā”€ā”€ analyticsService.ts (280 lines) │ └── securityService.ts (150 lines) ✨ NEW ā”œā”€ā”€ utils/ │ ā”œā”€ā”€ fileUtils.ts (300 lines) │ ā”œā”€ā”€ errorHandler.ts (100 lines) ✨ NEW │ ā”œā”€ā”€ cacheManager.ts (150 lines) ✨ NEW │ └── securityUtils.ts (100 lines) ✨ NEW └── __tests__/ ā”œā”€ā”€ unit/ │ ā”œā”€ā”€ services.test.ts │ ā”œā”€ā”€ commands.test.ts │ └── utils.test.ts ā”œā”€ā”€ integration/ │ └── extension.test.ts └── fixtures/ └── sample-projects/ ``` --- ## šŸ”„ Data Flow Improvements ### Current Data Flow (Problematic) ```mermaid sequenceDiagram participant User participant Extension participant Commands participant FileSystem participant VSCode User->>Extension: Right-click folder Extension->>Commands: Execute command Commands->>FileSystem: Read directory (sync) FileSystem->>VSCode: fs.readFileSync() āŒ VSCode-->>FileSystem: File content FileSystem-->>Commands: Tree data Commands-->>User: Display tree Note over Commands,FileSystem: āš ļø No caching Note over FileSystem: āš ļø No validation Note over Commands: āš ļø Duplicate logic ``` ### Proposed Data Flow (Improved) ```mermaid sequenceDiagram participant User participant Extension participant Command participant Security participant Cache participant FileSystem participant Formatter participant VSCode User->>Extension: Right-click folder Extension->>Command: Execute generateTree Command->>Security: Validate path Security-->>Command: āœ… Valid Command->>Cache: Check cache alt Cache Hit Cache-->>Command: Cached data else Cache Miss Command->>FileSystem: Read directory (async) FileSystem->>VSCode: workspace.fs.readDirectory() VSCode-->>FileSystem: Entries FileSystem->>Cache: Store result Cache-->>Command: Fresh data end Command->>Formatter: Format tree Formatter-->>Command: Formatted output Command-->>User: Display tree Note over Security: āœ… Path validation Note over Cache: āœ… TTL + LRU Note over FileSystem: āœ… Async operations ``` --- ## šŸ›”ļø Security Layer Architecture ```mermaid flowchart TB subgraph "Security Service" VAL[Input Validator] RATE[Rate Limiter] SIZE[Size Checker] PATH[Path Validator] end subgraph "Protected Operations" READ[File Reading] COPILOT[Copilot Calls] PATTERN[Pattern Matching] end VAL --> READ RATE --> COPILOT SIZE --> READ PATH --> READ PATH --> PATTERN style VAL fill:#4CAF50 style RATE fill:#4CAF50 style SIZE fill:#4CAF50 style PATH fill:#4CAF50 ``` **Implementation:** ```typescript // src/services/securityService.ts export class SecurityService { private rateLimiter = new RateLimiter(); async validateOperation( operation: 'read' | 'analyze' | 'pattern', context: OperationContext ): Promise<ValidationResult> { switch (operation) { case 'read': return this.validateFileRead(context); case 'analyze': return this.validateAnalysis(context); case 'pattern': return this.validatePattern(context); } } private validateFileRead(context: OperationContext): ValidationResult { // 1. Check path is in workspace if (!this.isPathInWorkspace(context.uri)) { return { valid: false, reason: 'Path outside workspace' }; } // 2. Check file size if (context.size && context.size > this.MAX_FILE_SIZE) { return { valid: false, reason: 'File too large' }; } return { valid: true }; } private validateAnalysis(context: OperationContext): ValidationResult { // 1. Check rate limit if (!this.rateLimiter.checkLimit('copilot')) { return { valid: false, reason: 'Rate limit exceeded' }; } // 2. Check file size if (context.size && context.size > this.MAX_ANALYSIS_SIZE) { return { valid: false, reason: 'File too large for analysis' }; } this.rateLimiter.recordCall('copilot'); return { valid: true }; } } ``` --- ## šŸ’¾ Cache Management Architecture ```mermaid classDiagram class CacheManager { -cache: Map~string, CacheEntry~ -maxSize: number -ttl: number +get(key: string): T | null +set(key: string, value: T): void +invalidate(key: string): void +clear(): void +cleanup(): void } class CacheEntry~T~ { +data: T +timestamp: number +hits: number +size: number } class CacheStrategy { <<interface>> +shouldEvict(entry: CacheEntry): boolean } class LRUStrategy { +shouldEvict(entry: CacheEntry): boolean } class TTLStrategy { +shouldEvict(entry: CacheEntry): boolean } CacheManager --> CacheEntry CacheManager --> CacheStrategy CacheStrategy <|.. LRUStrategy CacheStrategy <|.. TTLStrategy ``` **Implementation:** ```typescript // src/utils/cacheManager.ts export class CacheManager<T> { private cache = new Map<string, CacheEntry<T>>(); private readonly maxSize: number; private readonly ttl: number; constructor(config: CacheConfig) { this.maxSize = config.maxSize || 1000; this.ttl = config.ttl || 5 * 60 * 1000; // 5 minutes // Auto-cleanup every minute setInterval(() => this.cleanup(), 60000); } get(key: string): T | null { const entry = this.cache.get(key); if (!entry) { return null; } // Check TTL if (Date.now() - entry.timestamp > this.ttl) { this.cache.delete(key); return null; } // Update hit count entry.hits++; return entry.data; } set(key: string, value: T): void { // Enforce size limit if (this.cache.size >= this.maxSize) { this.evictLRU(); } this.cache.set(key, { data: value, timestamp: Date.now(), hits: 0, size: this.estimateSize(value), }); } private evictLRU(): void { // Find entry with lowest hit count let minHits = Infinity; let keyToEvict: string | null = null; for (const [key, entry] of this.cache.entries()) { if (entry.hits < minHits) { minHits = entry.hits; keyToEvict = key; } } if (keyToEvict) { this.cache.delete(keyToEvict); } } cleanup(): void { const now = Date.now(); for (const [key, entry] of this.cache.entries()) { if (now - entry.timestamp > this.ttl) { this.cache.delete(key); } } } getStats(): CacheStats { let totalSize = 0; let totalHits = 0; for (const entry of this.cache.values()) { totalSize += entry.size; totalHits += entry.hits; } return { entries: this.cache.size, totalSize, totalHits, hitRate: totalHits / Math.max(1, this.cache.size), }; } } ``` --- ## šŸŽØ Formatter Pattern Implementation ```mermaid classDiagram class TreeFormatter { <<interface>> +format(tree: FileTreeItem[]): string +getExtension(): string } class MarkdownFormatter { +format(tree: FileTreeItem[]): string +getExtension(): string -generateTreeLines(items, level): string } class JSONFormatter { +format(tree: FileTreeItem[]): string +getExtension(): string -buildTreeData(items): object } class SVGFormatter { +format(tree: FileTreeItem[]): string +getExtension(): string -renderNode(node, x, y): string } class ASCIIFormatter { +format(tree: FileTreeItem[]): string +getExtension(): string -generateAsciiLines(items, prefix): string } class FormatterFactory { +createFormatter(type: string): TreeFormatter } TreeFormatter <|.. MarkdownFormatter TreeFormatter <|.. JSONFormatter TreeFormatter <|.. SVGFormatter TreeFormatter <|.. ASCIIFormatter FormatterFactory --> TreeFormatter ``` **Implementation:** ```typescript // src/formatters/treeFormatter.interface.ts export interface TreeFormatter { format(tree: FileTreeItem[], options: FormatOptions): string; getExtension(): string; getLanguageId(): string; } // src/formatters/formatterFactory.ts export class FormatterFactory { private formatters = new Map<string, TreeFormatter>(); constructor() { this.formatters.set('markdown', new MarkdownFormatter()); this.formatters.set('json', new JSONFormatter()); this.formatters.set('svg', new SVGFormatter()); this.formatters.set('ascii', new ASCIIFormatter()); } createFormatter(type: string): TreeFormatter { const formatter = this.formatters.get(type.toLowerCase()); if (!formatter) { throw new Error(`Unknown formatter type: ${type}`); } return formatter; } getAvailableFormats(): string[] { return Array.from(this.formatters.keys()); } } // Usage in command const factory = new FormatterFactory(); const formatter = factory.createFormatter(formatChoice.value); const output = formatter.format(tree, options); ``` --- ## šŸ”Œ Dependency Injection Container ```mermaid classDiagram class ServiceContainer { -services: Map~string, any~ +register~T~(name: string, factory: Factory~T~): void +resolve~T~(name: string): T +singleton~T~(name: string, factory: Factory~T~): void } class Extension { -container: ServiceContainer +activate(context): void } class FileTreeProvider { -fileSystemService: FileSystemService -copilotService: CopilotService -analyticsService: AnalyticsService +constructor(services: Services) } Extension --> ServiceContainer ServiceContainer --> FileTreeProvider ServiceContainer --> FileSystemService ServiceContainer --> CopilotService ``` **Implementation:** ```typescript // src/core/serviceContainer.ts export class ServiceContainer { private services = new Map<string, any>(); private singletons = new Map<string, any>(); register<T>(name: string, factory: () => T): void { this.services.set(name, factory); } singleton<T>(name: string, factory: () => T): void { this.singletons.set(name, factory); } resolve<T>(name: string): T { // Check singleton first if (this.singletons.has(name)) { const factory = this.singletons.get(name); if (!this.services.has(`singleton:${name}`)) { this.services.set(`singleton:${name}`, factory()); } return this.services.get(`singleton:${name}`); } // Create new instance const factory = this.services.get(name); if (!factory) { throw new Error(`Service not registered: ${name}`); } return factory(); } } // Usage in extension.ts export function activate(context: vscode.ExtensionContext) { const container = new ServiceContainer(); // Register services as singletons container.singleton('fileSystemService', () => new FileSystemService()); container.singleton('copilotService', () => new CopilotService()); container.singleton('analyticsService', () => new AnalyticsService()); container.singleton('securityService', () => new SecurityService()); container.singleton('cacheManager', () => new CacheManager({ maxSize: 1000, ttl: 300000 })); // Register provider container.register('fileTreeProvider', () => { return new FileTreeProvider( container.resolve('fileSystemService'), container.resolve('copilotService'), container.resolve('analyticsService') ); }); // Create and register Tree View const provider = container.resolve<FileTreeProvider>('fileTreeProvider'); const treeView = vscode.window.createTreeView('fileTreeView', { treeDataProvider: provider, }); context.subscriptions.push(treeView); } ``` --- ## šŸ“Š Performance Monitoring ```mermaid flowchart LR subgraph "Performance Monitoring" TIMER[Operation Timer] MEM[Memory Monitor] CACHE[Cache Stats] METRICS[Metrics Collector] end subgraph "Operations" READ[File Read] GEN[Tree Generation] FORMAT[Formatting] RENDER[Rendering] end READ --> TIMER GEN --> TIMER FORMAT --> TIMER RENDER --> TIMER TIMER --> METRICS MEM --> METRICS CACHE --> METRICS METRICS --> OUTPUT[Output Channel] METRICS --> TELEMETRY[Telemetry] ``` **Implementation:** ```typescript // src/utils/performanceMonitor.ts export class PerformanceMonitor { private metrics = new Map<string, OperationMetrics>(); startOperation(name: string): PerformanceTimer { return new PerformanceTimer(name, this); } recordOperation(name: string, duration: number, memoryDelta: number): void { const existing = this.metrics.get(name) || { count: 0, totalDuration: 0, avgDuration: 0, minDuration: Infinity, maxDuration: 0, totalMemory: 0, }; existing.count++; existing.totalDuration += duration; existing.avgDuration = existing.totalDuration / existing.count; existing.minDuration = Math.min(existing.minDuration, duration); existing.maxDuration = Math.max(existing.maxDuration, duration); existing.totalMemory += memoryDelta; this.metrics.set(name, existing); } getReport(): PerformanceReport { const report: PerformanceReport = { operations: [], totalOperations: 0, totalTime: 0, }; for (const [name, metrics] of this.metrics.entries()) { report.operations.push({ name, ...metrics }); report.totalOperations += metrics.count; report.totalTime += metrics.totalDuration; } return report; } } class PerformanceTimer { private startTime: number; private startMemory: number; constructor( private name: string, private monitor: PerformanceMonitor ) { this.startTime = performance.now(); this.startMemory = process.memoryUsage().heapUsed; } end(): void { const duration = performance.now() - this.startTime; const memoryDelta = process.memoryUsage().heapUsed - this.startMemory; this.monitor.recordOperation(this.name, duration, memoryDelta); } } // Usage const monitor = new PerformanceMonitor(); async function generateTree() { const timer = monitor.startOperation('generateTree'); try { // ... tree generation logic } finally { timer.end(); } } ``` ---