- Pre-register all module routes automatically from module definitions in router configuration - Add useModuleReady composable for clean reactive loading states during module initialization - Update ChatPage and EventsPage with proper loading/error states and computed service access - Remove duplicate route registration from plugin manager install phase - Maintain modular architecture while ensuring routes are available immediately on app startup Resolves blank pages and Vue Router warnings when refreshing on /chat, /events, /my-tickets routes. Users now see proper loading indicators instead of blank screens during module initialization. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
330 lines
No EOL
9 KiB
TypeScript
330 lines
No EOL
9 KiB
TypeScript
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<string, ModuleRegistration>()
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<string, ModuleRegistration> {
|
|
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<void> {
|
|
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<string>()
|
|
const visiting = new Set<string>()
|
|
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() |