UNPKG

@pulzar/core

Version:

Next-generation Node.js framework for ultra-fast web applications with zero-reflection DI, GraphQL, WebSockets, events, and edge runtime support

301 lines 9.36 kB
import { logger } from "../utils/logger"; import { EventEmitter } from "events"; export class PluginManager extends EventEmitter { plugins = new Map(); context; initialized = false; options; constructor(context, options) { super(); this.context = context; this.options = { enableHotReload: false, validateDependencies: true, ...options, }; } /** * Initialize all plugins */ async initialize() { if (this.initialized) { throw new Error("PluginManager already initialized"); } logger.info("Initializing plugin system", { pluginCount: this.options.plugins.length, }); // Load and register plugins for (const pluginOrFactory of this.options.plugins) { await this.loadPlugin(pluginOrFactory); } // Validate dependencies if (this.options.validateDependencies) { this.validatePluginDependencies(); } // Sort plugins by dependencies const sortedPlugins = this.sortPluginsByDependencies(); // Setup plugins in dependency order for (const plugin of sortedPlugins) { await this.setupPlugin(plugin); } this.initialized = true; this.emit("initialized"); logger.info("Plugin system initialized", { loadedPlugins: Array.from(this.plugins.keys()), }); } /** * Load a single plugin */ async loadPlugin(pluginOrFactory) { let plugin; if (typeof pluginOrFactory === "function") { plugin = pluginOrFactory(); } else { plugin = pluginOrFactory; } if (this.plugins.has(plugin.name)) { throw new Error(`Plugin "${plugin.name}" is already loaded`); } this.plugins.set(plugin.name, plugin); logger.debug("Plugin loaded", { name: plugin.name, version: plugin.version, dependencies: plugin.dependencies, }); this.emit("pluginLoaded", plugin); } /** * Setup a plugin */ async setupPlugin(plugin) { try { logger.debug("Setting up plugin", { name: plugin.name }); if (plugin.setup) { await plugin.setup(this.context); } this.emit("pluginSetup", plugin); } catch (error) { logger.error("Failed to setup plugin", { plugin: plugin.name, error: error instanceof Error ? error.message : String(error), }); throw error; } } /** * Call a specific hook on all plugins */ async callHook(hookName, ...args) { for (const plugin of this.plugins.values()) { const hook = plugin[hookName]; if (hook && typeof hook === "function") { try { await hook(...args); } catch (error) { logger.error("Plugin hook failed", { plugin: plugin.name, hook: hookName, error: error instanceof Error ? error.message : String(error), }); throw error; } } } } /** * Get a plugin by name */ getPlugin(name) { return this.plugins.get(name); } /** * Get all loaded plugins */ getPlugins() { return Array.from(this.plugins.values()); } /** * Check if a plugin is loaded */ hasPlugin(name) { return this.plugins.has(name); } /** * Validate plugin dependencies */ validatePluginDependencies() { for (const plugin of this.plugins.values()) { if (plugin.dependencies) { for (const dependency of plugin.dependencies) { if (!this.plugins.has(dependency)) { throw new Error(`Plugin "${plugin.name}" requires dependency "${dependency}" which is not loaded`); } } } } } /** * Sort plugins by dependencies using topological sort */ sortPluginsByDependencies() { const plugins = Array.from(this.plugins.values()); const sorted = []; const visited = new Set(); const visiting = new Set(); const visit = (plugin) => { if (visited.has(plugin.name)) return; if (visiting.has(plugin.name)) { throw new Error(`Circular dependency detected involving plugin "${plugin.name}"`); } visiting.add(plugin.name); // Visit dependencies first if (plugin.dependencies) { for (const depName of plugin.dependencies) { const dep = this.plugins.get(depName); if (dep) { visit(dep); } } } visiting.delete(plugin.name); visited.add(plugin.name); sorted.push(plugin); }; for (const plugin of plugins) { visit(plugin); } return sorted; } /** * Shutdown all plugins */ async shutdown() { logger.info("Shutting down plugin system"); const plugins = Array.from(this.plugins.values()).reverse(); // Reverse order for (const plugin of plugins) { try { if (plugin.teardown) { await plugin.teardown(this.context); } } catch (error) { logger.error("Failed to teardown plugin", { plugin: plugin.name, error: error instanceof Error ? error.message : String(error), }); } } this.plugins.clear(); this.initialized = false; this.emit("shutdown"); } /** * Reload a plugin (for hot reload) */ async reloadPlugin(name) { if (!this.options.enableHotReload) { throw new Error("Hot reload is not enabled"); } const plugin = this.plugins.get(name); if (!plugin) { throw new Error(`Plugin "${name}" is not loaded`); } logger.info("Reloading plugin", { name }); // Teardown if (plugin.teardown) { await plugin.teardown(this.context); } // Setup again if (plugin.setup) { await plugin.setup(this.context); } this.emit("pluginReloaded", plugin); } } /** * Create a plugin context */ export function createPluginContext(app, config) { return { app, config, env: process.env, isDevelopment: process.env.NODE_ENV === "development", isProduction: process.env.NODE_ENV === "production", }; } export function definePlugin(pluginOrFactory) { return pluginOrFactory; } /** * Create a plugin with options */ export function createPlugin(name, factory) { return (options) => factory(options); } /** * Built-in plugins */ export const builtinPlugins = { /** * Development tools plugin */ devtools: createPlugin("devtools", (options = {}) => ({ name: "devtools", description: "Development tools and hot reload", async configureServer(app, context) { if (!context.isDevelopment || !options.enabled) return; // Add development routes app.get("/_dev/health", async () => ({ status: "ok", plugins: [], // Plugin info would need to be passed from plugin manager memory: process.memoryUsage(), uptime: process.uptime(), })); logger.info("DevTools plugin enabled"); }, })), /** * Metrics plugin */ metrics: createPlugin("metrics", (options = {}) => ({ name: "metrics", description: "Prometheus metrics endpoint", async configureServer(app, context) { if (!options.enabled) return; const metricsPath = options.path || "/metrics"; app.get(metricsPath, async () => { // Return Prometheus metrics return "# Placeholder for Prometheus metrics\n"; }); logger.info("Metrics plugin enabled", { path: metricsPath }); }, })), /** * Health check plugin */ health: createPlugin("health", (options = {}) => ({ name: "health", description: "Health check endpoints", async configureServer(app, context) { const healthPath = options.path || "/health"; app.get(healthPath, async () => ({ status: "ok", timestamp: new Date().toISOString(), uptime: process.uptime(), memory: process.memoryUsage(), checks: options.checks || [], })); logger.info("Health check plugin enabled", { path: healthPath }); }, })), }; export default PluginManager; //# sourceMappingURL=plugin-manager.js.map