Files
haopengzhan 8c0b04db0e feat: initial release of pengzhan.dev personal editorial blog platform
- 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
2026-09-16 23:08:53 -07:00

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();