- Modify app configuration to use environment variables for base URL and API key, enhancing flexibility for different environments. - Refactor plugin installation logic in the PluginManager to ensure proper configuration object structure. - Update base module initialization to correctly access Nostr relay options from the configuration, improving reliability.
329 lines
No EOL
8.8 KiB
TypeScript
329 lines
No EOL
8.8 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}`)
|
|
|
|
// 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 })
|
|
|
|
// Register routes if provided
|
|
if (plugin.routes && this.router) {
|
|
for (const route of plugin.routes) {
|
|
this.router.addRoute(route)
|
|
}
|
|
}
|
|
|
|
// 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() |