@terrymooreii/sia 2.2.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/README.md +46 -0
- package/docs/creating-plugins.md +509 -0
- package/docs/plugins.md +320 -0
- package/lib/build.js +70 -4
- package/lib/collections.js +9 -3
- package/lib/config.js +7 -0
- package/lib/content.js +49 -23
- package/lib/hooks.js +188 -0
- package/lib/index.js +19 -0
- package/lib/plugins.js +350 -0
- package/lib/server.js +2 -1
- package/lib/templates.js +7 -1
- package/package.json +1 -1
package/lib/hooks.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook system for plugins
|
|
3
|
+
* Manages hook registration and execution
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class HookRegistry {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.hooks = new Map();
|
|
9
|
+
this.pluginHooks = new Map(); // Track which plugin registered which hooks
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Register hooks from a plugin
|
|
14
|
+
*/
|
|
15
|
+
registerPluginHooks(plugin) {
|
|
16
|
+
if (!plugin.hooks || typeof plugin.hooks !== 'object') {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const pluginName = plugin.name;
|
|
21
|
+
|
|
22
|
+
for (const [hookName, hookFunction] of Object.entries(plugin.hooks)) {
|
|
23
|
+
if (typeof hookFunction !== 'function') {
|
|
24
|
+
console.warn(`⚠️ Plugin ${pluginName}: Hook "${hookName}" is not a function, skipping`);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!this.hooks.has(hookName)) {
|
|
29
|
+
this.hooks.set(hookName, []);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.hooks.get(hookName).push({
|
|
33
|
+
plugin: pluginName,
|
|
34
|
+
fn: hookFunction
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Track plugin hooks
|
|
38
|
+
if (!this.pluginHooks.has(pluginName)) {
|
|
39
|
+
this.pluginHooks.set(pluginName, []);
|
|
40
|
+
}
|
|
41
|
+
this.pluginHooks.get(pluginName).push(hookName);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Execute a hook with given arguments
|
|
47
|
+
*/
|
|
48
|
+
async executeHook(hookName, ...args) {
|
|
49
|
+
const hooks = this.hooks.get(hookName) || [];
|
|
50
|
+
|
|
51
|
+
if (hooks.length === 0) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const results = [];
|
|
56
|
+
|
|
57
|
+
for (const { plugin, fn } of hooks) {
|
|
58
|
+
try {
|
|
59
|
+
const result = await fn(...args);
|
|
60
|
+
results.push({ plugin, result, error: null });
|
|
61
|
+
} catch (err) {
|
|
62
|
+
results.push({ plugin, result: null, error: err });
|
|
63
|
+
console.error(`❌ Plugin ${plugin} hook "${hookName}" failed:`, err.message);
|
|
64
|
+
if (process.env.VERBOSE || process.env.DEBUG) {
|
|
65
|
+
console.error(err.stack);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return results;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Execute a hook and return the last result (for hooks that modify data)
|
|
75
|
+
*/
|
|
76
|
+
async executeHookWithResult(hookName, initialValue, ...args) {
|
|
77
|
+
const hooks = this.hooks.get(hookName) || [];
|
|
78
|
+
|
|
79
|
+
let value = initialValue;
|
|
80
|
+
|
|
81
|
+
for (const { plugin, fn } of hooks) {
|
|
82
|
+
try {
|
|
83
|
+
const result = await fn(value, ...args);
|
|
84
|
+
// If hook returns a value, use it as the new value
|
|
85
|
+
if (result !== undefined && result !== null) {
|
|
86
|
+
value = result;
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.error(`❌ Plugin ${plugin} hook "${hookName}" failed:`, err.message);
|
|
90
|
+
if (process.env.VERBOSE || process.env.DEBUG) {
|
|
91
|
+
console.error(err.stack);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get all registered hooks for a plugin
|
|
101
|
+
*/
|
|
102
|
+
getPluginHooks(pluginName) {
|
|
103
|
+
return this.pluginHooks.get(pluginName) || [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Clear all hooks
|
|
108
|
+
*/
|
|
109
|
+
clear() {
|
|
110
|
+
this.hooks.clear();
|
|
111
|
+
this.pluginHooks.clear();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Global hook registry instance
|
|
116
|
+
let hookRegistry = null;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Initialize the hook system
|
|
120
|
+
*/
|
|
121
|
+
export function initializeHooks() {
|
|
122
|
+
hookRegistry = new HookRegistry();
|
|
123
|
+
return hookRegistry;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get the hook registry instance
|
|
128
|
+
*/
|
|
129
|
+
export function getHookRegistry() {
|
|
130
|
+
if (!hookRegistry) {
|
|
131
|
+
hookRegistry = new HookRegistry();
|
|
132
|
+
}
|
|
133
|
+
return hookRegistry;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Register hooks from all plugins
|
|
138
|
+
*/
|
|
139
|
+
export function registerPluginHooks(plugins) {
|
|
140
|
+
const registry = getHookRegistry();
|
|
141
|
+
|
|
142
|
+
for (const plugin of plugins) {
|
|
143
|
+
registry.registerPluginHooks(plugin);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Execute a hook
|
|
149
|
+
*/
|
|
150
|
+
export async function executeHook(hookName, ...args) {
|
|
151
|
+
const registry = getHookRegistry();
|
|
152
|
+
return registry.executeHook(hookName, ...args);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Execute a hook and return the result (for data transformation hooks)
|
|
157
|
+
*/
|
|
158
|
+
export async function executeHookWithResult(hookName, initialValue, ...args) {
|
|
159
|
+
const registry = getHookRegistry();
|
|
160
|
+
return registry.executeHookWithResult(hookName, initialValue, ...args);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Check if a hook has any registered handlers
|
|
165
|
+
*/
|
|
166
|
+
export function hasHook(hookName) {
|
|
167
|
+
const registry = getHookRegistry();
|
|
168
|
+
return registry.hooks.has(hookName) && registry.hooks.get(hookName).length > 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Clear all hooks (useful for testing)
|
|
173
|
+
*/
|
|
174
|
+
export function clearHooks() {
|
|
175
|
+
const registry = getHookRegistry();
|
|
176
|
+
registry.clear();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export default {
|
|
180
|
+
initializeHooks,
|
|
181
|
+
getHookRegistry,
|
|
182
|
+
registerPluginHooks,
|
|
183
|
+
executeHook,
|
|
184
|
+
executeHookWithResult,
|
|
185
|
+
hasHook,
|
|
186
|
+
clearHooks
|
|
187
|
+
};
|
|
188
|
+
|
package/lib/index.js
CHANGED
|
@@ -34,6 +34,25 @@ export { build, buildCommand } from './build.js';
|
|
|
34
34
|
export { startServer, devCommand } from './server.js';
|
|
35
35
|
export { newCommand } from './new.js';
|
|
36
36
|
export { initSite, initCommand } from './init.js';
|
|
37
|
+
export {
|
|
38
|
+
discoverPlugins,
|
|
39
|
+
loadPlugins,
|
|
40
|
+
loadPlugin,
|
|
41
|
+
validatePlugin,
|
|
42
|
+
discoverLocalPlugins,
|
|
43
|
+
discoverNpmPlugins,
|
|
44
|
+
orderPlugins
|
|
45
|
+
} from './plugins.js';
|
|
46
|
+
export {
|
|
47
|
+
initializeHooks,
|
|
48
|
+
getHookRegistry,
|
|
49
|
+
registerPluginHooks,
|
|
50
|
+
executeHook,
|
|
51
|
+
executeHookWithResult,
|
|
52
|
+
hasHook,
|
|
53
|
+
clearHooks
|
|
54
|
+
} from './hooks.js';
|
|
55
|
+
export { addMarkedExtension } from './content.js';
|
|
37
56
|
|
|
38
57
|
// Default export with version
|
|
39
58
|
import { readFileSync } from 'fs';
|
package/lib/plugins.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { readdirSync, statSync, existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validate plugin structure
|
|
10
|
+
*/
|
|
11
|
+
export function validatePlugin(plugin, pluginPath) {
|
|
12
|
+
if (!plugin) {
|
|
13
|
+
throw new Error('Plugin is null or undefined');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!plugin.name || typeof plugin.name !== 'string') {
|
|
17
|
+
throw new Error('Plugin must have a "name" property (string)');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!plugin.version || typeof plugin.version !== 'string') {
|
|
21
|
+
throw new Error('Plugin must have a "version" property (string)');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Validate hooks if present
|
|
25
|
+
if (plugin.hooks && typeof plugin.hooks !== 'object') {
|
|
26
|
+
throw new Error('Plugin "hooks" must be an object');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Validate configSchema if present
|
|
30
|
+
if (plugin.configSchema && typeof plugin.configSchema !== 'object') {
|
|
31
|
+
throw new Error('Plugin "configSchema" must be an object');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Validate dependencies if present
|
|
35
|
+
if (plugin.dependencies && !Array.isArray(plugin.dependencies)) {
|
|
36
|
+
throw new Error('Plugin "dependencies" must be an array');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Load a plugin from a file path
|
|
44
|
+
*/
|
|
45
|
+
export async function loadPlugin(pluginPath, config) {
|
|
46
|
+
try {
|
|
47
|
+
// Support both .js and .mjs files
|
|
48
|
+
// Add cache busting query parameter to ensure fresh reloads during dev
|
|
49
|
+
// This prevents Node.js from using cached versions of plugins during hot reload
|
|
50
|
+
const cacheBuster = `?t=${Date.now()}`;
|
|
51
|
+
let module;
|
|
52
|
+
|
|
53
|
+
// Normalize path to use file:// protocol for consistent cache busting
|
|
54
|
+
// All plugin paths from discovery are absolute, so we can safely use file://
|
|
55
|
+
const normalizedPath = pluginPath.startsWith('file://')
|
|
56
|
+
? pluginPath
|
|
57
|
+
: pluginPath.startsWith('/') || /^[A-Za-z]:/.test(pluginPath) // Unix absolute or Windows drive
|
|
58
|
+
? `file://${pluginPath}`
|
|
59
|
+
: pluginPath; // Relative path (shouldn't happen, but handle it)
|
|
60
|
+
|
|
61
|
+
// Import with cache busting to ensure fresh reloads during dev
|
|
62
|
+
module = await import(`${normalizedPath}${cacheBuster}`);
|
|
63
|
+
|
|
64
|
+
// Support both default export and named export
|
|
65
|
+
const plugin = module.default || module;
|
|
66
|
+
|
|
67
|
+
if (!plugin) {
|
|
68
|
+
throw new Error('Plugin file does not export a plugin object');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Validate plugin structure
|
|
72
|
+
validatePlugin(plugin, pluginPath);
|
|
73
|
+
|
|
74
|
+
// Add metadata
|
|
75
|
+
plugin._path = pluginPath;
|
|
76
|
+
plugin._loaded = true;
|
|
77
|
+
|
|
78
|
+
return plugin;
|
|
79
|
+
} catch (err) {
|
|
80
|
+
throw new Error(`Failed to load plugin from ${pluginPath}: ${err.message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Discover local plugins from _plugins directory
|
|
86
|
+
*/
|
|
87
|
+
export function discoverLocalPlugins(rootDir) {
|
|
88
|
+
const pluginsDir = join(rootDir, '_plugins');
|
|
89
|
+
const plugins = [];
|
|
90
|
+
|
|
91
|
+
if (!existsSync(pluginsDir)) {
|
|
92
|
+
return plugins;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const items = readdirSync(pluginsDir);
|
|
97
|
+
console.log(`🔍 Scanning _plugins directory: found ${items.length} item(s)`);
|
|
98
|
+
|
|
99
|
+
for (const item of items) {
|
|
100
|
+
const itemPath = join(pluginsDir, item);
|
|
101
|
+
const stat = statSync(itemPath);
|
|
102
|
+
|
|
103
|
+
// Check extension case-insensitively
|
|
104
|
+
const lowerItem = item.toLowerCase();
|
|
105
|
+
const isJsFile = lowerItem.endsWith('.js') || lowerItem.endsWith('.mjs');
|
|
106
|
+
|
|
107
|
+
// Debug: log what we found
|
|
108
|
+
if (stat.isFile()) {
|
|
109
|
+
console.log(` 📄 Found file: ${item} (${isJsFile ? 'plugin candidate' : 'skipped - not .js/.mjs'})`);
|
|
110
|
+
} else if (stat.isDirectory()) {
|
|
111
|
+
console.log(` 📁 Found directory: ${item} (skipped - plugins must be files)`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Only process .js and .mjs files (case-insensitive)
|
|
115
|
+
if (stat.isFile() && isJsFile) {
|
|
116
|
+
plugins.push({
|
|
117
|
+
type: 'local',
|
|
118
|
+
path: itemPath,
|
|
119
|
+
name: item.replace(/\.(js|mjs)$/i, '') // Case-insensitive replacement
|
|
120
|
+
});
|
|
121
|
+
console.log(` ✓ Added plugin: ${item}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.warn(`⚠️ Error reading _plugins directory: ${err.message}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return plugins;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Discover npm plugins from node_modules
|
|
133
|
+
*/
|
|
134
|
+
export function discoverNpmPlugins(rootDir) {
|
|
135
|
+
const nodeModulesDir = join(rootDir, 'node_modules');
|
|
136
|
+
const plugins = [];
|
|
137
|
+
|
|
138
|
+
if (!existsSync(nodeModulesDir)) {
|
|
139
|
+
return plugins;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const packages = readdirSync(nodeModulesDir);
|
|
144
|
+
|
|
145
|
+
for (const pkg of packages) {
|
|
146
|
+
// Look for sia-plugin-* packages
|
|
147
|
+
if (pkg.startsWith('sia-plugin-')) {
|
|
148
|
+
const pkgPath = join(nodeModulesDir, pkg);
|
|
149
|
+
const pkgJsonPath = join(pkgPath, 'package.json');
|
|
150
|
+
|
|
151
|
+
if (existsSync(pkgJsonPath)) {
|
|
152
|
+
try {
|
|
153
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
154
|
+
const mainFile = pkgJson.main || 'index.js';
|
|
155
|
+
const pluginPath = join(pkgPath, mainFile);
|
|
156
|
+
|
|
157
|
+
if (existsSync(pluginPath)) {
|
|
158
|
+
plugins.push({
|
|
159
|
+
type: 'npm',
|
|
160
|
+
path: pluginPath,
|
|
161
|
+
name: pkg,
|
|
162
|
+
packageName: pkg,
|
|
163
|
+
version: pkgJson.version
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
console.warn(`⚠️ Error reading package.json for ${pkg}: ${err.message}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.warn(`⚠️ Error reading node_modules: ${err.message}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return plugins;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Resolve plugin dependencies and order
|
|
181
|
+
*/
|
|
182
|
+
export function orderPlugins(plugins, config) {
|
|
183
|
+
// If explicit order is provided in config, use it
|
|
184
|
+
const explicitOrder = config.plugins?.order;
|
|
185
|
+
if (explicitOrder && Array.isArray(explicitOrder)) {
|
|
186
|
+
const ordered = [];
|
|
187
|
+
const pluginMap = new Map(plugins.map(p => [p.name, p]));
|
|
188
|
+
const added = new Set();
|
|
189
|
+
|
|
190
|
+
// Add plugins in explicit order
|
|
191
|
+
for (const name of explicitOrder) {
|
|
192
|
+
const plugin = pluginMap.get(name);
|
|
193
|
+
if (plugin) {
|
|
194
|
+
ordered.push(plugin);
|
|
195
|
+
added.add(name);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Add remaining plugins
|
|
200
|
+
for (const plugin of plugins) {
|
|
201
|
+
if (!added.has(plugin.name)) {
|
|
202
|
+
ordered.push(plugin);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return ordered;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Otherwise, resolve dependencies
|
|
210
|
+
const pluginMap = new Map(plugins.map(p => [p.name, p]));
|
|
211
|
+
const ordered = [];
|
|
212
|
+
const added = new Set();
|
|
213
|
+
const visiting = new Set();
|
|
214
|
+
|
|
215
|
+
function visit(plugin) {
|
|
216
|
+
if (added.has(plugin.name)) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (visiting.has(plugin.name)) {
|
|
221
|
+
console.warn(`⚠️ Circular dependency detected involving plugin: ${plugin.name}`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
visiting.add(plugin.name);
|
|
226
|
+
|
|
227
|
+
// Visit dependencies first
|
|
228
|
+
if (plugin.dependencies && Array.isArray(plugin.dependencies)) {
|
|
229
|
+
for (const depName of plugin.dependencies) {
|
|
230
|
+
const dep = pluginMap.get(depName);
|
|
231
|
+
if (dep) {
|
|
232
|
+
visit(dep);
|
|
233
|
+
} else {
|
|
234
|
+
console.warn(`⚠️ Plugin ${plugin.name} depends on ${depName}, but it's not found`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
visiting.delete(plugin.name);
|
|
240
|
+
ordered.push(plugin);
|
|
241
|
+
added.add(plugin.name);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Visit all plugins
|
|
245
|
+
for (const plugin of plugins) {
|
|
246
|
+
visit(plugin);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return ordered;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Discover and load all plugins
|
|
254
|
+
*/
|
|
255
|
+
export async function discoverPlugins(config) {
|
|
256
|
+
// Check if plugins are enabled
|
|
257
|
+
if (config.plugins?.enabled === false) {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const rootDir = config.rootDir || process.cwd();
|
|
262
|
+
const discovered = [];
|
|
263
|
+
|
|
264
|
+
// Discover local plugins
|
|
265
|
+
const localPlugins = discoverLocalPlugins(rootDir);
|
|
266
|
+
discovered.push(...localPlugins);
|
|
267
|
+
|
|
268
|
+
// Discover npm plugins
|
|
269
|
+
const npmPlugins = discoverNpmPlugins(rootDir);
|
|
270
|
+
discovered.push(...npmPlugins);
|
|
271
|
+
|
|
272
|
+
// Filter by explicit plugin list if provided
|
|
273
|
+
const explicitPlugins = config.plugins?.plugins;
|
|
274
|
+
if (explicitPlugins && Array.isArray(explicitPlugins) && explicitPlugins.length > 0) {
|
|
275
|
+
console.log(`🔍 Filtering plugins: only loading ${explicitPlugins.join(', ')}`);
|
|
276
|
+
const explicitSet = new Set(explicitPlugins);
|
|
277
|
+
const filtered = discovered.filter(p => explicitSet.has(p.name));
|
|
278
|
+
const filteredOut = discovered.filter(p => !explicitSet.has(p.name));
|
|
279
|
+
if (filteredOut.length > 0) {
|
|
280
|
+
console.log(` ⚠️ Filtered out ${filteredOut.length} plugin(s): ${filteredOut.map(p => p.name).join(', ')}`);
|
|
281
|
+
}
|
|
282
|
+
return filtered;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return discovered;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Load all discovered plugins
|
|
290
|
+
*/
|
|
291
|
+
export async function loadPlugins(config) {
|
|
292
|
+
// Check if plugins are disabled
|
|
293
|
+
if (config.plugins?.enabled === false) {
|
|
294
|
+
console.log('🔌 Plugins are disabled in config');
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const rootDir = config.rootDir || process.cwd();
|
|
299
|
+
const discovered = await discoverPlugins(config);
|
|
300
|
+
|
|
301
|
+
if (discovered.length === 0) {
|
|
302
|
+
// Log that we checked but found nothing (helps with debugging)
|
|
303
|
+
const pluginsDir = join(rootDir, '_plugins');
|
|
304
|
+
const hasPluginsDir = existsSync(pluginsDir);
|
|
305
|
+
if (hasPluginsDir) {
|
|
306
|
+
console.log('🔌 No plugins discovered (directory exists but no valid plugins found)');
|
|
307
|
+
} else {
|
|
308
|
+
console.log('🔌 No plugins directory found, skipping plugin discovery');
|
|
309
|
+
}
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
console.log(`🔌 Found ${discovered.length} plugin(s)`);
|
|
314
|
+
|
|
315
|
+
const loaded = [];
|
|
316
|
+
const errors = [];
|
|
317
|
+
|
|
318
|
+
for (const pluginInfo of discovered) {
|
|
319
|
+
try {
|
|
320
|
+
const plugin = await loadPlugin(pluginInfo.path, config);
|
|
321
|
+
plugin._type = pluginInfo.type;
|
|
322
|
+
plugin._packageName = pluginInfo.packageName;
|
|
323
|
+
loaded.push(plugin);
|
|
324
|
+
console.log(` ✓ Loaded ${plugin.name}@${plugin.version} (${pluginInfo.type})`);
|
|
325
|
+
} catch (err) {
|
|
326
|
+
errors.push({ name: pluginInfo.name, error: err.message });
|
|
327
|
+
console.warn(` ✗ Failed to load ${pluginInfo.name}: ${err.message}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (errors.length > 0 && config.plugins?.strictMode) {
|
|
332
|
+
throw new Error(`Plugin loading failed in strict mode. ${errors.length} plugin(s) failed to load.`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Order plugins
|
|
336
|
+
const ordered = orderPlugins(loaded, config);
|
|
337
|
+
|
|
338
|
+
return ordered;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export default {
|
|
342
|
+
discoverPlugins,
|
|
343
|
+
loadPlugins,
|
|
344
|
+
loadPlugin,
|
|
345
|
+
validatePlugin,
|
|
346
|
+
discoverLocalPlugins,
|
|
347
|
+
discoverNpmPlugins,
|
|
348
|
+
orderPlugins
|
|
349
|
+
};
|
|
350
|
+
|
package/lib/server.js
CHANGED
|
@@ -166,7 +166,8 @@ function setupWatcher(config, wss) {
|
|
|
166
166
|
config.includesDir,
|
|
167
167
|
join(config.rootDir, '_config.yml'),
|
|
168
168
|
join(config.rootDir, '_config.json'),
|
|
169
|
-
join(config.rootDir, 'styles')
|
|
169
|
+
join(config.rootDir, 'styles'),
|
|
170
|
+
join(config.rootDir, '_plugins')
|
|
170
171
|
].filter(p => existsSync(p));
|
|
171
172
|
|
|
172
173
|
const watcher = chokidar.watch(watchPaths, {
|
package/lib/templates.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join, dirname } from 'path';
|
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { resolveTheme, getBuiltInThemesDir } from './theme-resolver.js';
|
|
6
|
+
import { executeHook } from './hooks.js';
|
|
6
7
|
|
|
7
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
9
|
const __dirname = dirname(__filename);
|
|
@@ -195,7 +196,7 @@ function createUrlFilter(basePath) {
|
|
|
195
196
|
* @param {object} config - Site configuration
|
|
196
197
|
* @param {object} [resolvedTheme] - Pre-resolved theme info from resolveTheme()
|
|
197
198
|
*/
|
|
198
|
-
export function createTemplateEngine(config, resolvedTheme = null) {
|
|
199
|
+
export async function createTemplateEngine(config, resolvedTheme = null) {
|
|
199
200
|
// Set up template paths - user layouts first, then defaults
|
|
200
201
|
const templatePaths = [];
|
|
201
202
|
|
|
@@ -247,6 +248,11 @@ export function createTemplateEngine(config, resolvedTheme = null) {
|
|
|
247
248
|
// Add URL filter with basePath support
|
|
248
249
|
env.addFilter('url', createUrlFilter(config.site.basePath));
|
|
249
250
|
|
|
251
|
+
// Execute addTemplateFilter and addTemplateFunction hooks
|
|
252
|
+
// These hooks allow plugins to register custom filters and functions
|
|
253
|
+
const filterResults = await executeHook('addTemplateFilter', env, config);
|
|
254
|
+
const functionResults = await executeHook('addTemplateFunction', env, config);
|
|
255
|
+
|
|
250
256
|
return env;
|
|
251
257
|
}
|
|
252
258
|
|