import type { App } from 'vue' import type { Router } from 'vue-router' import type { ModulePlugin, ModuleConfig, ModuleRegistration } from './types' import { eventBus } from './event-bus' import { container } from './di-container' /** * Plugin Manager * Handles module registration, dependency resolution, and lifecycle management */ export class PluginManager { private modules = new Map() private app: App | null = null private router: Router | null = null private installOrder: string[] = [] /** * Initialize the plugin manager */ init(app: App, router: Router): void { this.app = app this.router = router // Register core services container.provide('app', app) container.provide('router', router) container.provide('eventBus', eventBus) console.log('🔧 Plugin Manager initialized') } /** * Register a module plugin */ async register(plugin: ModulePlugin, config: ModuleConfig): Promise { if (this.modules.has(plugin.name)) { throw new Error(`Module ${plugin.name} is already registered`) } // Validate dependencies const missingDeps = this.validateDependencies(plugin) if (missingDeps.length > 0) { throw new Error(`Module ${plugin.name} has missing dependencies: ${missingDeps.join(', ')}`) } // Register the module const registration: ModuleRegistration = { plugin, config, installed: false } this.modules.set(plugin.name, registration) console.log(`📦 Registered module: ${plugin.name} v${plugin.version}`) // Routes are now pre-registered during router creation // This registration step is kept for potential dynamic route additions in the future if (plugin.routes && this.router) { console.log(`🛤️ ${plugin.name} routes already pre-registered (${plugin.routes.length} routes)`) } // Auto-install if enabled and not lazy if (config.enabled && !config.lazy) { await this.install(plugin.name) } eventBus.emit('module:registered', { name: plugin.name, config }, 'plugin-manager') } /** * Install a module */ async install(moduleName: string): Promise { const registration = this.modules.get(moduleName) if (!registration) { throw new Error(`Module ${moduleName} is not registered`) } if (registration.installed) { console.warn(`Module ${moduleName} is already installed`) return } const { plugin, config } = registration // Install dependencies first if (plugin.dependencies) { for (const dep of plugin.dependencies) { const depRegistration = this.modules.get(dep) if (!depRegistration) { throw new Error(`Dependency ${dep} is not registered`) } if (!depRegistration.installed) { await this.install(dep) } } } try { // Install the module if (!this.app) { throw new Error('Plugin manager not initialized') } await plugin.install(this.app, { config: config.config }) // Routes are already registered during the register() phase // Register services in DI container if (plugin.services) { for (const [name, service] of Object.entries(plugin.services)) { container.provide(Symbol(name), service) } } // Mark as installed registration.installed = true registration.installTime = Date.now() this.installOrder.push(moduleName) console.log(`✅ Installed module: ${moduleName}`) eventBus.emit('module:installed', { name: moduleName, plugin, config }, 'plugin-manager') } catch (error) { console.error(`❌ Failed to install module ${moduleName}:`, error) eventBus.emit('module:install-failed', { name: moduleName, error }, 'plugin-manager') throw error } } /** * Uninstall a module */ async uninstall(moduleName: string): Promise { const registration = this.modules.get(moduleName) if (!registration) { throw new Error(`Module ${moduleName} is not registered`) } if (!registration.installed) { console.warn(`Module ${moduleName} is not installed`) return } // Check for dependents const dependents = this.findDependents(moduleName) if (dependents.length > 0) { throw new Error(`Cannot uninstall ${moduleName}: required by ${dependents.join(', ')}`) } try { // Call module's uninstall hook if (registration.plugin.uninstall) { await registration.plugin.uninstall() } // Remove routes if (registration.plugin.routes && this.router) { // Note: Vue Router doesn't have removeRoute, so we'd need to track and rebuild // For now, we'll just log this limitation console.warn(`Routes from ${moduleName} cannot be removed (Vue Router limitation)`) } // Remove services from DI container if (registration.plugin.services) { for (const name of Object.keys(registration.plugin.services)) { container.remove(Symbol(name)) } } // Mark as uninstalled registration.installed = false registration.installTime = undefined const orderIndex = this.installOrder.indexOf(moduleName) if (orderIndex !== -1) { this.installOrder.splice(orderIndex, 1) } console.log(`🗑️ Uninstalled module: ${moduleName}`) eventBus.emit('module:uninstalled', { name: moduleName }, 'plugin-manager') } catch (error) { console.error(`❌ Failed to uninstall module ${moduleName}:`, error) eventBus.emit('module:uninstall-failed', { name: moduleName, error }, 'plugin-manager') throw error } } /** * Get module registration info */ getModule(name: string): ModuleRegistration | undefined { return this.modules.get(name) } /** * Get all registered modules */ getModules(): Map { return new Map(this.modules) } /** * Get installed modules in installation order */ getInstalledModules(): string[] { return [...this.installOrder] } /** * Check if a module is installed */ isInstalled(name: string): boolean { return this.modules.get(name)?.installed || false } /** * Get module status */ getStatus(): { registered: number installed: number failed: number modules: Array<{ name: string; version: string; installed: boolean; dependencies?: string[] }> } { const modules = Array.from(this.modules.values()) return { registered: modules.length, installed: modules.filter(m => m.installed).length, failed: modules.filter(m => !m.installed && m.config.enabled).length, modules: modules.map(({ plugin, installed }) => ({ name: plugin.name, version: plugin.version, installed, dependencies: plugin.dependencies })) } } /** * Validate module dependencies */ private validateDependencies(plugin: ModulePlugin): string[] { if (!plugin.dependencies) { return [] } return plugin.dependencies.filter(dep => !this.modules.has(dep)) } /** * Find modules that depend on the given module */ private findDependents(moduleName: string): string[] { const dependents: string[] = [] for (const [name, registration] of this.modules) { if (registration.installed && registration.plugin.dependencies?.includes(moduleName)) { dependents.push(name) } } return dependents } /** * Install all enabled modules */ async installAll(): Promise { const enabledModules = Array.from(this.modules.entries()) .filter(([_, reg]) => reg.config.enabled && !reg.config.lazy) .map(([name, _]) => name) // Sort by dependencies to ensure correct installation order const sortedModules = this.topologicalSort(enabledModules) for (const moduleName of sortedModules) { if (!this.isInstalled(moduleName)) { await this.install(moduleName) } } } /** * Topological sort for dependency resolution */ private topologicalSort(moduleNames: string[]): string[] { const visited = new Set() const visiting = new Set() const result: string[] = [] const visit = (name: string) => { if (visiting.has(name)) { throw new Error(`Circular dependency detected involving module: ${name}`) } if (visited.has(name)) { return } visiting.add(name) const registration = this.modules.get(name) if (registration?.plugin.dependencies) { for (const dep of registration.plugin.dependencies) { if (moduleNames.includes(dep)) { visit(dep) } } } visiting.delete(name) visited.add(name) result.push(name) } for (const name of moduleNames) { if (!visited.has(name)) { visit(name) } } return result } } // Global plugin manager instance export const pluginManager = new PluginManager()