const fs = require('fs'); const path = require('path'); const express = require('express'); class PluginManager { constructor() { this.plugins = new Map(); this.pluginStaticRouters = []; } loadPlugins(contentDir, app, siteConfig) { const pluginsDir = path.join(contentDir, 'plugins'); this.plugins.clear(); if (!fs.existsSync(pluginsDir)) { return; } const entries = fs.readdirSync(pluginsDir, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; const pluginName = entry.name; const pluginPath = path.join(pluginsDir, pluginName); const manifestPath = path.join(pluginPath, 'plugin.json'); const entryScriptPath = path.join(pluginPath, 'index.js'); if (!fs.existsSync(entryScriptPath)) { continue; } try { let manifest = { name: pluginName, enabled: true }; if (fs.existsSync(manifestPath)) { manifest = { ...manifest, ...JSON.parse(fs.readFileSync(manifestPath, 'utf8')) }; } // Check if enabled in site config (default true if not explicitly false) const configEntry = siteConfig.plugins && siteConfig.plugins[pluginName]; if (configEntry === false || (configEntry && configEntry.enabled === false)) { console.log(`[PluginManager] Plugin ${pluginName} is disabled in config`); continue; } // Clear module cache for hot reload delete require.cache[require.resolve(entryScriptPath)]; const pluginModule = require(entryScriptPath); const pluginInstance = { name: pluginName, manifest, module: pluginModule, config: (configEntry && typeof configEntry === 'object') ? configEntry : {} }; // Mount public assets if directory exists const publicDir = path.join(pluginPath, 'public'); if (fs.existsSync(publicDir) && app) { app.use(`/plugins/${pluginName}`, express.static(publicDir)); } // Run onInit hook if (typeof pluginModule.onInit === 'function' && app) { try { pluginModule.onInit({ app, config: siteConfig, pluginConfig: pluginInstance.config }); } catch (initErr) { console.error(`[PluginManager] Error in onInit for plugin ${pluginName}:`, initErr); } } this.plugins.set(pluginName, pluginInstance); console.log(`[PluginManager] Loaded plugin: ${pluginName}`); } catch (err) { console.error(`[PluginManager] Failed to load plugin ${pluginName}:`, err); } } } applyMarkdownHooks(markdownIt) { for (const [name, plugin] of this.plugins) { if (typeof plugin.module.onMarkdownInit === 'function') { try { plugin.module.onMarkdownInit({ markdownIt, pluginConfig: plugin.config }); } catch (err) { console.error(`[PluginManager] Error in onMarkdownInit for ${name}:`, err); } } } } applyPostHooks(post) { for (const [name, plugin] of this.plugins) { if (typeof plugin.module.onPostLoad === 'function') { try { plugin.module.onPostLoad({ post, pluginConfig: plugin.config }); } catch (err) { console.error(`[PluginManager] Error in onPostLoad for ${name}:`, err); } } } } async triggerCommentCreated(comment, post) { for (const [name, plugin] of this.plugins) { if (typeof plugin.module.onCommentCreated === 'function') { try { await plugin.module.onCommentCreated({ comment, post, pluginConfig: plugin.config }); } catch (err) { console.error(`[PluginManager] Error in onCommentCreated for ${name}:`, err); } } } } renderHook(hookName, context) { let output = ''; for (const [name, plugin] of this.plugins) { if (typeof plugin.module[hookName] === 'function') { try { const res = plugin.module[hookName]({ ...context, pluginConfig: plugin.config }); if (res && typeof res === 'string') { output += '\n' + res; } } catch (err) { console.error(`[PluginManager] Error in ${hookName} for ${name}:`, err); } } } return output; } } module.exports = new PluginManager();