@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
JavaScript
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