- Editorial web platform built with Node.js, Express, EJS, and Vanilla CSS - Markdown content engine with YAML frontmatter, bilingual variants, and hot-reload watcher - Unified SQLite with FTS5 search indexing, query logging, and schema filters - Centered editorial post layout with margin-docked left TOC and mobile bottom sheet - WordPress-grade comment system with moderation dashboard and bulk operations under /guanli - Complete SEO suite (JSON-LD structured data, RSS/Atom feeds, XML sitemap, robots.txt) - CI/CD workflows for linting, security audits (Gitleaks), automated tests, and Docker releases
138 lines
4.3 KiB
JavaScript
138 lines
4.3 KiB
JavaScript
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();
|