vako 1.3.0
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/CHANGELOG.md +63 -0
- package/README.md +1944 -0
- package/bin/commands/quick-setup.js +111 -0
- package/bin/commands/setup-executor.js +203 -0
- package/bin/commands/setup.js +737 -0
- package/bin/create-veko-app.js +75 -0
- package/bin/veko-update.js +205 -0
- package/bin/veko.js +188 -0
- package/error/error.ejs +382 -0
- package/index.js +36 -0
- package/lib/adapters/nextjs-adapter.js +241 -0
- package/lib/app.js +749 -0
- package/lib/core/auth-manager.js +1353 -0
- package/lib/core/auto-updater.js +1118 -0
- package/lib/core/logger.js +97 -0
- package/lib/core/module-installer.js +86 -0
- package/lib/dev/dev-server.js +292 -0
- package/lib/layout/layout-manager.js +834 -0
- package/lib/plugin-manager.js +1795 -0
- package/lib/routing/route-manager.js +1000 -0
- package/package.json +231 -0
- package/templates/public/css/style.css +2 -0
- package/templates/public/js/main.js +1 -0
- package/tsconfig.json +50 -0
- package/types/index.d.ts +238 -0
|
@@ -0,0 +1,1795 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const EventEmitter = require('events');
|
|
4
|
+
|
|
5
|
+
// Couleurs pour les logs
|
|
6
|
+
const colors = {
|
|
7
|
+
reset: '\x1b[0m',
|
|
8
|
+
bright: '\x1b[1m',
|
|
9
|
+
red: '\x1b[31m',
|
|
10
|
+
green: '\x1b[32m',
|
|
11
|
+
yellow: '\x1b[33m',
|
|
12
|
+
blue: '\x1b[34m',
|
|
13
|
+
magenta: '\x1b[35m',
|
|
14
|
+
cyan: '\x1b[36m',
|
|
15
|
+
gray: '\x1b[90m',
|
|
16
|
+
bgGreen: '\x1b[42m',
|
|
17
|
+
bgBlue: '\x1b[44m',
|
|
18
|
+
bgMagenta: '\x1b[45m',
|
|
19
|
+
bgCyan: '\x1b[46m',
|
|
20
|
+
bgYellow: '\x1b[43m',
|
|
21
|
+
bgRed: '\x1b[41m',
|
|
22
|
+
white: '\x1b[37m'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
class PluginManager extends EventEmitter {
|
|
26
|
+
constructor(app, options = {}) {
|
|
27
|
+
super();
|
|
28
|
+
|
|
29
|
+
this.app = app;
|
|
30
|
+
this.options = {
|
|
31
|
+
pluginsDir: 'plugins',
|
|
32
|
+
autoLoad: true,
|
|
33
|
+
enableHooks: true,
|
|
34
|
+
enableAPI: true,
|
|
35
|
+
maxRetries: 3,
|
|
36
|
+
timeout: 30000,
|
|
37
|
+
supportTypeScript: true,
|
|
38
|
+
devMode: process.env.NODE_ENV === 'development',
|
|
39
|
+
watchMode: false,
|
|
40
|
+
enableMetrics: true,
|
|
41
|
+
enableValidation: true,
|
|
42
|
+
allowHotReload: true,
|
|
43
|
+
enableSandbox: false,
|
|
44
|
+
...options
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
this.plugins = new Map();
|
|
48
|
+
this.hooks = new Map();
|
|
49
|
+
this.middleware = [];
|
|
50
|
+
this.routes = [];
|
|
51
|
+
this.commands = new Map();
|
|
52
|
+
this.loadOrder = [];
|
|
53
|
+
this.loadingQueue = new Set();
|
|
54
|
+
this.errorCount = new Map();
|
|
55
|
+
this.metrics = new Map();
|
|
56
|
+
this.watchers = new Map();
|
|
57
|
+
this.schemas = new Map();
|
|
58
|
+
this.devTools = new Map();
|
|
59
|
+
|
|
60
|
+
// Support TypeScript
|
|
61
|
+
this.tsSupport = this.initTypeScriptSupport();
|
|
62
|
+
|
|
63
|
+
this.init();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============= SUPPORT TYPESCRIPT =============
|
|
67
|
+
|
|
68
|
+
initTypeScriptSupport() {
|
|
69
|
+
if (!this.options.supportTypeScript) return null;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
// Essayer de charger ts-node pour l'exécution directe de TS
|
|
73
|
+
const tsNode = require('ts-node');
|
|
74
|
+
tsNode.register({
|
|
75
|
+
transpileOnly: true,
|
|
76
|
+
compilerOptions: {
|
|
77
|
+
module: 'commonjs',
|
|
78
|
+
target: 'es2020',
|
|
79
|
+
esModuleInterop: true,
|
|
80
|
+
allowSyntheticDefaultImports: true,
|
|
81
|
+
experimentalDecorators: true,
|
|
82
|
+
emitDecoratorMetadata: true
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
this.log('info', 'Support TypeScript activé', '🔷 ts-node configuré');
|
|
87
|
+
return { enabled: true, runtime: 'ts-node' };
|
|
88
|
+
} catch (error) {
|
|
89
|
+
// Fallback: support de la compilation à la volée
|
|
90
|
+
this.log('warning', 'ts-node non trouvé', 'compilation à la volée activée');
|
|
91
|
+
return { enabled: true, runtime: 'compile' };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async loadTypeScriptPlugin(pluginPath) {
|
|
96
|
+
if (!this.tsSupport?.enabled) {
|
|
97
|
+
throw new Error('Support TypeScript non activé');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (this.tsSupport.runtime === 'ts-node') {
|
|
101
|
+
// Chargement direct avec ts-node
|
|
102
|
+
return require(pluginPath);
|
|
103
|
+
} else {
|
|
104
|
+
// Compilation à la volée
|
|
105
|
+
const typescript = require('typescript');
|
|
106
|
+
const tsContent = fs.readFileSync(pluginPath, 'utf8');
|
|
107
|
+
|
|
108
|
+
const result = typescript.transpile(tsContent, {
|
|
109
|
+
module: typescript.ModuleKind.CommonJS,
|
|
110
|
+
target: typescript.ScriptTarget.ES2020,
|
|
111
|
+
esModuleInterop: true,
|
|
112
|
+
allowSyntheticDefaultImports: true
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Écrire le fichier JS temporaire
|
|
116
|
+
const jsPath = pluginPath.replace('.ts', '.js');
|
|
117
|
+
fs.writeFileSync(jsPath, result);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const plugin = require(jsPath);
|
|
121
|
+
fs.unlinkSync(jsPath); // Nettoyer le fichier temporaire
|
|
122
|
+
return plugin;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (fs.existsSync(jsPath)) fs.unlinkSync(jsPath);
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============= INITIALISATION AVANCÉE =============
|
|
131
|
+
|
|
132
|
+
init() {
|
|
133
|
+
this.setupHooks();
|
|
134
|
+
this.setupDevTools();
|
|
135
|
+
this.setupMetrics();
|
|
136
|
+
this.setupWatcher();
|
|
137
|
+
|
|
138
|
+
if (this.options.autoLoad) {
|
|
139
|
+
this.loadAllPlugins().catch(error => {
|
|
140
|
+
this.log('error', 'Erreur lors du chargement automatique', error.message);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
setupHooks() {
|
|
146
|
+
const defaultHooks = [
|
|
147
|
+
'app:init', 'app:start', 'app:stop', 'app:restart',
|
|
148
|
+
'route:load', 'route:create', 'route:delete', 'route:update',
|
|
149
|
+
'request:start', 'request:end', 'request:error',
|
|
150
|
+
'response:start', 'response:end', 'response:error',
|
|
151
|
+
'middleware:add', 'middleware:remove',
|
|
152
|
+
'error:handle', 'error:critical',
|
|
153
|
+
'websocket:connect', 'websocket:disconnect', 'websocket:message',
|
|
154
|
+
'file:change', 'file:add', 'file:delete',
|
|
155
|
+
'plugin:load', 'plugin:unload', 'plugin:error', 'plugin:timeout',
|
|
156
|
+
'plugin:activate', 'plugin:deactivate', 'plugin:reload',
|
|
157
|
+
'config:change', 'config:validate',
|
|
158
|
+
'database:connect', 'database:disconnect', 'database:query',
|
|
159
|
+
'cache:set', 'cache:get', 'cache:delete', 'cache:clear',
|
|
160
|
+
'auth:login', 'auth:logout', 'auth:register',
|
|
161
|
+
'dev:hotreload', 'dev:debug', 'dev:profile'
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
defaultHooks.forEach(hookName => {
|
|
165
|
+
this.hooks.set(hookName, []);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
setupDevTools() {
|
|
170
|
+
if (!this.options.devMode) return;
|
|
171
|
+
|
|
172
|
+
this.devTools.set('profiler', {
|
|
173
|
+
start: (name) => {
|
|
174
|
+
const start = process.hrtime.bigint();
|
|
175
|
+
return {
|
|
176
|
+
end: () => {
|
|
177
|
+
const end = process.hrtime.bigint();
|
|
178
|
+
const duration = Number(end - start) / 1000000; // ms
|
|
179
|
+
this.log('debug', `Profil: ${name}`, `${duration.toFixed(2)}ms`);
|
|
180
|
+
return duration;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
this.devTools.set('debugger', {
|
|
187
|
+
breakpoint: (message, data = {}) => {
|
|
188
|
+
if (this.options.devMode) {
|
|
189
|
+
console.log(`🔴 BREAKPOINT: ${message}`, data);
|
|
190
|
+
debugger; // eslint-disable-line no-debugger
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
inspect: (obj, label = 'Object') => {
|
|
194
|
+
console.log(`🔍 ${label}:`, require('util').inspect(obj, { colors: true, depth: 3 }));
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
this.devTools.set('hotreload', {
|
|
199
|
+
enable: () => this.enableHotReload(),
|
|
200
|
+
disable: () => this.disableHotReload(),
|
|
201
|
+
trigger: (pluginName) => this.triggerHotReload(pluginName)
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
setupMetrics() {
|
|
206
|
+
if (!this.options.enableMetrics) return;
|
|
207
|
+
|
|
208
|
+
setInterval(() => {
|
|
209
|
+
this.collectMetrics();
|
|
210
|
+
}, 30000); // Collecte toutes les 30 secondes
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
setupWatcher() {
|
|
214
|
+
if (!this.options.watchMode && !this.options.allowHotReload) return;
|
|
215
|
+
|
|
216
|
+
const chokidar = require('chokidar');
|
|
217
|
+
const pluginsPath = path.join(process.cwd(), this.options.pluginsDir);
|
|
218
|
+
|
|
219
|
+
const watcher = chokidar.watch([
|
|
220
|
+
`${pluginsPath}/**/*.js`,
|
|
221
|
+
`${pluginsPath}/**/*.ts`,
|
|
222
|
+
`${pluginsPath}/**/package.json`
|
|
223
|
+
], {
|
|
224
|
+
ignored: /node_modules/,
|
|
225
|
+
persistent: true
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
watcher.on('change', async (filePath) => {
|
|
229
|
+
const pluginName = this.getPluginNameFromFile(filePath);
|
|
230
|
+
if (pluginName && this.plugins.has(pluginName)) {
|
|
231
|
+
this.log('info', 'Fichier modifié détecté', `${pluginName} → rechargement`);
|
|
232
|
+
try {
|
|
233
|
+
await this.reloadPlugin(pluginName);
|
|
234
|
+
this.emit('dev:hotreload', pluginName, filePath);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
this.log('error', 'Erreur hot reload', error.message);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
this.watchers.set('files', watcher);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ============= CHARGEMENT AVANCÉ DES PLUGINS =============
|
|
245
|
+
|
|
246
|
+
async loadPlugin(plugin, config = {}) {
|
|
247
|
+
let pluginName;
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
let pluginModule;
|
|
251
|
+
|
|
252
|
+
if (typeof plugin === 'string') {
|
|
253
|
+
pluginName = plugin;
|
|
254
|
+
|
|
255
|
+
if (this.loadingQueue.has(pluginName)) {
|
|
256
|
+
throw new Error(`Plugin "${pluginName}" déjà en cours de chargement`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
this.loadingQueue.add(pluginName);
|
|
260
|
+
pluginModule = await this.resolvePlugin(plugin);
|
|
261
|
+
} else {
|
|
262
|
+
pluginModule = plugin;
|
|
263
|
+
pluginName = plugin.name || 'anonymous';
|
|
264
|
+
this.loadingQueue.add(pluginName);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (this.plugins.has(pluginName)) {
|
|
268
|
+
this.log('warning', 'Plugin déjà chargé', pluginName);
|
|
269
|
+
this.loadingQueue.delete(pluginName);
|
|
270
|
+
return this;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Validation avancée
|
|
274
|
+
await this.validatePluginAdvanced(pluginModule, pluginName);
|
|
275
|
+
|
|
276
|
+
const pluginInstance = {
|
|
277
|
+
name: pluginName,
|
|
278
|
+
version: pluginModule.version || '1.0.0',
|
|
279
|
+
description: pluginModule.description || '',
|
|
280
|
+
author: pluginModule.author || '',
|
|
281
|
+
dependencies: pluginModule.dependencies || [],
|
|
282
|
+
peerDependencies: pluginModule.peerDependencies || [],
|
|
283
|
+
config: { ...pluginModule.defaultConfig, ...config },
|
|
284
|
+
module: pluginModule,
|
|
285
|
+
loaded: false,
|
|
286
|
+
active: false,
|
|
287
|
+
loadTime: Date.now(),
|
|
288
|
+
errorCount: 0,
|
|
289
|
+
metrics: {
|
|
290
|
+
loadTime: 0,
|
|
291
|
+
executeCount: 0,
|
|
292
|
+
errorCount: 0,
|
|
293
|
+
lastError: null,
|
|
294
|
+
performance: {}
|
|
295
|
+
},
|
|
296
|
+
type: this.detectPluginType(pluginModule),
|
|
297
|
+
priority: pluginModule.priority || 10,
|
|
298
|
+
sandbox: this.options.enableSandbox ? this.createSandbox(pluginName) : null
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
await this.checkDependenciesAdvanced(pluginInstance);
|
|
302
|
+
await this.executePluginLoadWithTimeout(pluginInstance);
|
|
303
|
+
|
|
304
|
+
this.plugins.set(pluginName, pluginInstance);
|
|
305
|
+
this.loadOrder.push(pluginName);
|
|
306
|
+
this.loadingQueue.delete(pluginName);
|
|
307
|
+
|
|
308
|
+
this.log('success', 'Plugin chargé', `${pluginName} v${pluginInstance.version}`);
|
|
309
|
+
this.emit('plugin:loaded', pluginName, pluginInstance);
|
|
310
|
+
await this.executeHook('plugin:load', pluginName, pluginInstance);
|
|
311
|
+
|
|
312
|
+
return this;
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (pluginName) {
|
|
315
|
+
this.loadingQueue.delete(pluginName);
|
|
316
|
+
this.errorCount.set(pluginName, (this.errorCount.get(pluginName) || 0) + 1);
|
|
317
|
+
}
|
|
318
|
+
this.log('error', 'Erreur lors du chargement du plugin', error.message);
|
|
319
|
+
this.emit('plugin:error', pluginName, error);
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async resolvePlugin(pluginName) {
|
|
325
|
+
const pluginsPath = path.join(process.cwd(), this.options.pluginsDir);
|
|
326
|
+
|
|
327
|
+
// Essayer différentes extensions et structures
|
|
328
|
+
const possiblePaths = [
|
|
329
|
+
path.join(pluginsPath, `${pluginName}.js`),
|
|
330
|
+
path.join(pluginsPath, `${pluginName}.ts`),
|
|
331
|
+
path.join(pluginsPath, pluginName, 'index.js'),
|
|
332
|
+
path.join(pluginsPath, pluginName, 'index.ts'),
|
|
333
|
+
path.join(pluginsPath, pluginName, 'main.js'),
|
|
334
|
+
path.join(pluginsPath, pluginName, 'main.ts'),
|
|
335
|
+
path.join(pluginsPath, pluginName, 'plugin.js'),
|
|
336
|
+
path.join(pluginsPath, pluginName, 'plugin.ts')
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
for (const pluginPath of possiblePaths) {
|
|
340
|
+
if (fs.existsSync(pluginPath)) {
|
|
341
|
+
if (pluginPath.endsWith('.ts')) {
|
|
342
|
+
return await this.loadTypeScriptPlugin(pluginPath);
|
|
343
|
+
} else {
|
|
344
|
+
return require(pluginPath);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Essayer depuis node_modules
|
|
350
|
+
try {
|
|
351
|
+
return require(pluginName);
|
|
352
|
+
} catch (e) {
|
|
353
|
+
throw new Error(`Plugin "${pluginName}" introuvable`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
detectPluginType(pluginModule) {
|
|
358
|
+
if (pluginModule.type) return pluginModule.type;
|
|
359
|
+
|
|
360
|
+
// Détecter automatiquement le type
|
|
361
|
+
if (pluginModule.middleware) return 'middleware';
|
|
362
|
+
if (pluginModule.routes) return 'router';
|
|
363
|
+
if (pluginModule.commands) return 'cli';
|
|
364
|
+
if (pluginModule.websocket) return 'websocket';
|
|
365
|
+
if (pluginModule.database) return 'database';
|
|
366
|
+
if (pluginModule.auth) return 'auth';
|
|
367
|
+
if (pluginModule.theme) return 'theme';
|
|
368
|
+
|
|
369
|
+
return 'generic';
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async validatePluginAdvanced(pluginModule, pluginName) {
|
|
373
|
+
if (!this.options.enableValidation) return;
|
|
374
|
+
|
|
375
|
+
// Validation de base
|
|
376
|
+
if (!pluginModule || typeof pluginModule !== 'object') {
|
|
377
|
+
throw new Error(`Plugin "${pluginName}" doit exporter un objet`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (!pluginModule.load || typeof pluginModule.load !== 'function') {
|
|
381
|
+
throw new Error(`Plugin "${pluginName}" doit avoir une méthode load()`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Validation des métadonnées
|
|
385
|
+
const requiredFields = ['name', 'version'];
|
|
386
|
+
const optionalFields = ['description', 'author', 'license', 'homepage', 'repository'];
|
|
387
|
+
|
|
388
|
+
for (const field of requiredFields) {
|
|
389
|
+
if (!pluginModule[field]) {
|
|
390
|
+
this.log('warning', `Plugin ${pluginName}`, `Champ requis manquant: ${field}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Validation sémantique de version
|
|
395
|
+
if (pluginModule.version && !this.isValidVersion(pluginModule.version)) {
|
|
396
|
+
throw new Error(`Plugin "${pluginName}": version invalide "${pluginModule.version}"`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Validation des dépendances
|
|
400
|
+
if (pluginModule.dependencies && !Array.isArray(pluginModule.dependencies)) {
|
|
401
|
+
throw new Error(`Plugin "${pluginName}": dependencies doit être un tableau`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Validation du schéma de configuration
|
|
405
|
+
if (pluginModule.configSchema) {
|
|
406
|
+
this.schemas.set(pluginName, pluginModule.configSchema);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Validation des hooks déclarés
|
|
410
|
+
if (pluginModule.hooks && Array.isArray(pluginModule.hooks)) {
|
|
411
|
+
for (const hookName of pluginModule.hooks) {
|
|
412
|
+
if (!this.hooks.has(hookName)) {
|
|
413
|
+
this.log('warning', `Plugin ${pluginName}`, `Hook inconnu: ${hookName}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Validation des permissions
|
|
419
|
+
if (pluginModule.permissions && Array.isArray(pluginModule.permissions)) {
|
|
420
|
+
await this.validatePermissions(pluginName, pluginModule.permissions);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async checkDependenciesAdvanced(plugin) {
|
|
425
|
+
// Vérifier les dépendances normales
|
|
426
|
+
if (plugin.dependencies && plugin.dependencies.length > 0) {
|
|
427
|
+
const missing = [];
|
|
428
|
+
for (const dep of plugin.dependencies) {
|
|
429
|
+
if (!this.plugins.has(dep)) {
|
|
430
|
+
missing.push(dep);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (missing.length > 0) {
|
|
435
|
+
throw new Error(`Plugin "${plugin.name}" nécessite: ${missing.join(', ')}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Vérifier les peer dependencies
|
|
440
|
+
if (plugin.peerDependencies && plugin.peerDependencies.length > 0) {
|
|
441
|
+
const missingPeers = [];
|
|
442
|
+
for (const peerDep of plugin.peerDependencies) {
|
|
443
|
+
try {
|
|
444
|
+
require.resolve(peerDep);
|
|
445
|
+
} catch (error) {
|
|
446
|
+
missingPeers.push(peerDep);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (missingPeers.length > 0) {
|
|
451
|
+
this.log('warning', `Plugin ${plugin.name}`, `Peer dependencies manquantes: ${missingPeers.join(', ')}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Vérifier les versions compatibles
|
|
456
|
+
if (plugin.module.engines) {
|
|
457
|
+
await this.checkEngineCompatibility(plugin.name, plugin.module.engines);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ============= FONCTIONS UTILITAIRES POUR DÉVELOPPEURS =============
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Crée un plugin de développement rapide
|
|
465
|
+
*/
|
|
466
|
+
createDevPlugin(name, options = {}) {
|
|
467
|
+
const plugin = {
|
|
468
|
+
name,
|
|
469
|
+
version: '1.0.0-dev',
|
|
470
|
+
description: `Plugin de développement: ${name}`,
|
|
471
|
+
author: 'Développeur',
|
|
472
|
+
type: 'dev',
|
|
473
|
+
load: async (app, config, context) => {
|
|
474
|
+
context.log('info', `Plugin de dev ${name} chargé`);
|
|
475
|
+
|
|
476
|
+
// Auto-setup des fonctionnalités courantes
|
|
477
|
+
if (options.routes) {
|
|
478
|
+
Object.entries(options.routes).forEach(([path, handler]) => {
|
|
479
|
+
context.addRoute('GET', path, handler);
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (options.middleware) {
|
|
484
|
+
options.middleware.forEach(mw => context.addMiddleware(mw));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (options.hooks) {
|
|
488
|
+
Object.entries(options.hooks).forEach(([hookName, callback]) => {
|
|
489
|
+
context.hook(hookName, callback);
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (options.commands) {
|
|
494
|
+
Object.entries(options.commands).forEach(([cmdName, cmd]) => {
|
|
495
|
+
context.addCommand(cmdName, cmd.handler, cmd.description);
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Exécuter le code personnalisé
|
|
500
|
+
if (options.load && typeof options.load === 'function') {
|
|
501
|
+
await options.load(app, config, context);
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
unload: options.unload,
|
|
505
|
+
...options
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
return plugin;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Plugin factory avec builder pattern
|
|
513
|
+
*/
|
|
514
|
+
createPluginBuilder() {
|
|
515
|
+
return new PluginBuilder();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Injecte du code dans un plugin existant (dev uniquement)
|
|
520
|
+
*/
|
|
521
|
+
async injectCode(pluginName, code, type = 'before-load') {
|
|
522
|
+
if (!this.options.devMode) {
|
|
523
|
+
throw new Error('Injection de code disponible uniquement en mode développement');
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const plugin = this.plugins.get(pluginName);
|
|
527
|
+
if (!plugin) {
|
|
528
|
+
throw new Error(`Plugin ${pluginName} non trouvé`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (!plugin.injections) plugin.injections = [];
|
|
532
|
+
|
|
533
|
+
plugin.injections.push({
|
|
534
|
+
code,
|
|
535
|
+
type,
|
|
536
|
+
timestamp: Date.now(),
|
|
537
|
+
active: true
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
this.log('debug', `Code injecté dans ${pluginName}`, `Type: ${type}`);
|
|
541
|
+
this.emit('dev:inject', pluginName, code, type);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Monitore les performances d'un plugin
|
|
546
|
+
*/
|
|
547
|
+
profilePlugin(pluginName, duration = 60000) {
|
|
548
|
+
const plugin = this.plugins.get(pluginName);
|
|
549
|
+
if (!plugin) return null;
|
|
550
|
+
|
|
551
|
+
const profiler = {
|
|
552
|
+
start: Date.now(),
|
|
553
|
+
end: Date.now() + duration,
|
|
554
|
+
data: {
|
|
555
|
+
hookCalls: 0,
|
|
556
|
+
executionTime: 0,
|
|
557
|
+
memoryUsage: process.memoryUsage(),
|
|
558
|
+
errors: 0
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// Wrapper les méthodes du plugin pour collecter les métriques
|
|
563
|
+
this.wrapPluginMethods(plugin, profiler);
|
|
564
|
+
|
|
565
|
+
setTimeout(() => {
|
|
566
|
+
this.log('info', `Profil de ${pluginName}`, JSON.stringify(profiler.data, null, 2));
|
|
567
|
+
this.emit('dev:profile', pluginName, profiler.data);
|
|
568
|
+
}, duration);
|
|
569
|
+
|
|
570
|
+
return profiler;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Débogueur interactif pour plugin
|
|
575
|
+
*/
|
|
576
|
+
debugPlugin(pluginName) {
|
|
577
|
+
const plugin = this.plugins.get(pluginName);
|
|
578
|
+
if (!plugin) return null;
|
|
579
|
+
|
|
580
|
+
const debugInterface = {
|
|
581
|
+
inspect: () => this.devTools.get('debugger').inspect(plugin, `Plugin ${pluginName}`),
|
|
582
|
+
config: () => console.log('Configuration:', plugin.config),
|
|
583
|
+
metrics: () => console.log('Métriques:', plugin.metrics),
|
|
584
|
+
hooks: () => this.listPluginHooks(pluginName),
|
|
585
|
+
reload: () => this.reloadPlugin(pluginName),
|
|
586
|
+
toggle: () => this.togglePlugin(pluginName),
|
|
587
|
+
breakpoint: (message) => this.devTools.get('debugger').breakpoint(`${pluginName}: ${message}`, plugin)
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
global[`debug_${pluginName}`] = debugInterface;
|
|
591
|
+
this.log('debug', `Interface de debug créée`, `Utilisez global.debug_${pluginName}`);
|
|
592
|
+
|
|
593
|
+
return debugInterface;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Teste un plugin avec différents scénarios
|
|
598
|
+
*/
|
|
599
|
+
async testPlugin(pluginName, tests = {}) {
|
|
600
|
+
const plugin = this.plugins.get(pluginName);
|
|
601
|
+
if (!plugin) throw new Error(`Plugin ${pluginName} non trouvé`);
|
|
602
|
+
|
|
603
|
+
const results = {
|
|
604
|
+
passed: 0,
|
|
605
|
+
failed: 0,
|
|
606
|
+
errors: [],
|
|
607
|
+
details: {}
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
// Tests par défaut
|
|
611
|
+
const defaultTests = {
|
|
612
|
+
'load': () => plugin.loaded === true,
|
|
613
|
+
'active': () => plugin.active === true,
|
|
614
|
+
'config': () => plugin.config !== null,
|
|
615
|
+
'version': () => plugin.version && this.isValidVersion(plugin.version)
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const allTests = { ...defaultTests, ...tests };
|
|
619
|
+
|
|
620
|
+
for (const [testName, testFn] of Object.entries(allTests)) {
|
|
621
|
+
try {
|
|
622
|
+
const result = await testFn(plugin);
|
|
623
|
+
if (result) {
|
|
624
|
+
results.passed++;
|
|
625
|
+
results.details[testName] = 'PASS';
|
|
626
|
+
} else {
|
|
627
|
+
results.failed++;
|
|
628
|
+
results.details[testName] = 'FAIL';
|
|
629
|
+
}
|
|
630
|
+
} catch (error) {
|
|
631
|
+
results.failed++;
|
|
632
|
+
results.errors.push({ test: testName, error: error.message });
|
|
633
|
+
results.details[testName] = `ERROR: ${error.message}`;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
this.log('info', `Tests pour ${pluginName}`, `${results.passed} réussis, ${results.failed} échoués`);
|
|
638
|
+
return results;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Générateur de plugin depuis template
|
|
643
|
+
*/
|
|
644
|
+
async generatePlugin(name, template = 'basic', options = {}) {
|
|
645
|
+
const templates = {
|
|
646
|
+
basic: this.getBasicTemplate(),
|
|
647
|
+
middleware: this.getMiddlewareTemplate(),
|
|
648
|
+
api: this.getApiTemplate(),
|
|
649
|
+
websocket: this.getWebSocketTemplate(),
|
|
650
|
+
database: this.getDatabaseTemplate(),
|
|
651
|
+
auth: this.getAuthTemplate()
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
const pluginTemplate = templates[template];
|
|
655
|
+
if (!pluginTemplate) {
|
|
656
|
+
throw new Error(`Template ${template} non trouvé`);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const pluginCode = this.renderTemplate(pluginTemplate, { name, ...options });
|
|
660
|
+
const pluginPath = path.join(process.cwd(), this.options.pluginsDir, `${name}.js`);
|
|
661
|
+
|
|
662
|
+
fs.writeFileSync(pluginPath, pluginCode);
|
|
663
|
+
this.log('success', 'Plugin généré', `${name} → ${pluginPath}`);
|
|
664
|
+
|
|
665
|
+
return pluginPath;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Backup et restore des plugins
|
|
670
|
+
*/
|
|
671
|
+
async backupPlugins(backupPath = null) {
|
|
672
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
673
|
+
const defaultPath = path.join(process.cwd(), 'backups', `plugins-${timestamp}.json`);
|
|
674
|
+
const outputPath = backupPath || defaultPath;
|
|
675
|
+
|
|
676
|
+
const backup = {
|
|
677
|
+
timestamp: Date.now(),
|
|
678
|
+
plugins: Array.from(this.plugins.entries()).map(([name, plugin]) => ({
|
|
679
|
+
name,
|
|
680
|
+
version: plugin.version,
|
|
681
|
+
config: plugin.config,
|
|
682
|
+
active: plugin.active,
|
|
683
|
+
loadOrder: this.loadOrder.indexOf(name)
|
|
684
|
+
})),
|
|
685
|
+
loadOrder: this.loadOrder,
|
|
686
|
+
options: this.options
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
const dir = path.dirname(outputPath);
|
|
690
|
+
if (!fs.existsSync(dir)) {
|
|
691
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
fs.writeFileSync(outputPath, JSON.stringify(backup, null, 2));
|
|
695
|
+
this.log('success', 'Plugins sauvegardés', outputPath);
|
|
696
|
+
|
|
697
|
+
return outputPath;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async restorePlugins(backupPath) {
|
|
701
|
+
if (!fs.existsSync(backupPath)) {
|
|
702
|
+
throw new Error(`Fichier de sauvegarde non trouvé: ${backupPath}`);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const backup = JSON.parse(fs.readFileSync(backupPath, 'utf8'));
|
|
706
|
+
|
|
707
|
+
// Décharger tous les plugins actuels
|
|
708
|
+
for (const pluginName of Array.from(this.plugins.keys())) {
|
|
709
|
+
await this.unloadPlugin(pluginName);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Charger les plugins dans l'ordre de la sauvegarde
|
|
713
|
+
const sortedPlugins = backup.plugins.sort((a, b) => a.loadOrder - b.loadOrder);
|
|
714
|
+
|
|
715
|
+
for (const pluginData of sortedPlugins) {
|
|
716
|
+
try {
|
|
717
|
+
await this.loadPlugin(pluginData.name, pluginData.config);
|
|
718
|
+
if (!pluginData.active) {
|
|
719
|
+
await this.togglePlugin(pluginData.name, false);
|
|
720
|
+
}
|
|
721
|
+
} catch (error) {
|
|
722
|
+
this.log('error', `Erreur restauration ${pluginData.name}`, error.message);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
this.log('success', 'Plugins restaurés', `${sortedPlugins.length} plugins`);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// ============= TEMPLATES DE PLUGINS =============
|
|
730
|
+
|
|
731
|
+
getBasicTemplate() {
|
|
732
|
+
return `
|
|
733
|
+
/**
|
|
734
|
+
* Plugin {{name}}
|
|
735
|
+
* Généré automatiquement par Veko.js PluginManager
|
|
736
|
+
*/
|
|
737
|
+
|
|
738
|
+
module.exports = {
|
|
739
|
+
name: '{{name}}',
|
|
740
|
+
version: '1.0.0',
|
|
741
|
+
description: 'Description du plugin {{name}}',
|
|
742
|
+
author: 'Votre nom',
|
|
743
|
+
|
|
744
|
+
// Configuration par défaut
|
|
745
|
+
defaultConfig: {
|
|
746
|
+
enabled: true
|
|
747
|
+
},
|
|
748
|
+
|
|
749
|
+
// Méthode de chargement (obligatoire)
|
|
750
|
+
async load(app, config, context) {
|
|
751
|
+
context.log('info', 'Plugin {{name}} chargé !');
|
|
752
|
+
|
|
753
|
+
// Votre code ici
|
|
754
|
+
},
|
|
755
|
+
|
|
756
|
+
// Méthode de déchargement (optionnelle)
|
|
757
|
+
async unload(app, config) {
|
|
758
|
+
console.log('Plugin {{name}} déchargé');
|
|
759
|
+
},
|
|
760
|
+
|
|
761
|
+
// Activation/désactivation (optionnelles)
|
|
762
|
+
async activate(app, config) {
|
|
763
|
+
console.log('Plugin {{name}} activé');
|
|
764
|
+
},
|
|
765
|
+
|
|
766
|
+
async deactivate(app, config) {
|
|
767
|
+
console.log('Plugin {{name}} désactivé');
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
`;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
getMiddlewareTemplate() {
|
|
774
|
+
return `
|
|
775
|
+
module.exports = {
|
|
776
|
+
name: '{{name}}',
|
|
777
|
+
version: '1.0.0',
|
|
778
|
+
description: 'Plugin middleware {{name}}',
|
|
779
|
+
type: 'middleware',
|
|
780
|
+
|
|
781
|
+
async load(app, config, context) {
|
|
782
|
+
// Middleware personnalisé
|
|
783
|
+
const middleware = (req, res, next) => {
|
|
784
|
+
context.log('info', 'Middleware {{name}} exécuté');
|
|
785
|
+
// Votre logique ici
|
|
786
|
+
next();
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
context.addMiddleware(middleware);
|
|
790
|
+
context.log('success', 'Middleware {{name}} ajouté');
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
`;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
getApiTemplate() {
|
|
797
|
+
return `
|
|
798
|
+
module.exports = {
|
|
799
|
+
name: '{{name}}',
|
|
800
|
+
version: '1.0.0',
|
|
801
|
+
description: 'Plugin API {{name}}',
|
|
802
|
+
type: 'api',
|
|
803
|
+
|
|
804
|
+
async load(app, config, context) {
|
|
805
|
+
// Routes API
|
|
806
|
+
context.addRoute('GET', '/api/{{name}}', (req, res) => {
|
|
807
|
+
res.json({ message: 'Hello from {{name}} API!' });
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
context.addRoute('POST', '/api/{{name}}', (req, res) => {
|
|
811
|
+
res.json({ received: req.body });
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
context.log('success', 'API {{name}} configurée');
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
`;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
renderTemplate(template, variables) {
|
|
821
|
+
let rendered = template;
|
|
822
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
823
|
+
rendered = rendered.replace(new RegExp(`{{${key}}}`, 'g'), value);
|
|
824
|
+
}
|
|
825
|
+
return rendered;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// ============= FONCTIONS UTILITAIRES =============
|
|
829
|
+
|
|
830
|
+
isValidVersion(version) {
|
|
831
|
+
return /^\d+\.\d+\.\d+(-[a-zA-Z0-9-]+)?$/.test(version);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
getPluginNameFromFile(filePath) {
|
|
835
|
+
const pluginsPath = path.join(process.cwd(), this.options.pluginsDir);
|
|
836
|
+
const relativePath = path.relative(pluginsPath, filePath);
|
|
837
|
+
const parts = relativePath.split(path.sep);
|
|
838
|
+
|
|
839
|
+
if (parts[0].endsWith('.js') || parts[0].endsWith('.ts')) {
|
|
840
|
+
return path.parse(parts[0]).name;
|
|
841
|
+
} else {
|
|
842
|
+
return parts[0];
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
listPluginHooks(pluginName) {
|
|
847
|
+
const hooks = [];
|
|
848
|
+
this.hooks.forEach((hookList, hookName) => {
|
|
849
|
+
const pluginHooks = hookList.filter(h => h.plugin === pluginName);
|
|
850
|
+
if (pluginHooks.length > 0) {
|
|
851
|
+
hooks.push({ hook: hookName, count: pluginHooks.length });
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
return hooks;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
wrapPluginMethods(plugin, profiler) {
|
|
858
|
+
// Wrapper pour mesurer les performances
|
|
859
|
+
const originalLoad = plugin.module.load;
|
|
860
|
+
plugin.module.load = async (...args) => {
|
|
861
|
+
const start = process.hrtime.bigint();
|
|
862
|
+
try {
|
|
863
|
+
const result = await originalLoad.call(plugin.module, ...args);
|
|
864
|
+
profiler.data.executionTime += Number(process.hrtime.bigint() - start) / 1000000;
|
|
865
|
+
return result;
|
|
866
|
+
} catch (error) {
|
|
867
|
+
profiler.data.errors++;
|
|
868
|
+
throw error;
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
collectMetrics() {
|
|
874
|
+
for (const [name, plugin] of this.plugins.entries()) {
|
|
875
|
+
const memUsage = process.memoryUsage();
|
|
876
|
+
const errorCount = this.errorCount.get(name) || 0;
|
|
877
|
+
|
|
878
|
+
this.metrics.set(name, {
|
|
879
|
+
uptime: Date.now() - plugin.loadTime,
|
|
880
|
+
memoryUsage: memUsage,
|
|
881
|
+
errorCount,
|
|
882
|
+
lastCheck: Date.now(),
|
|
883
|
+
health: errorCount === 0 ? 'healthy' : errorCount < 5 ? 'warning' : 'critical'
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// ============= INITIALISATION =============
|
|
889
|
+
init() {
|
|
890
|
+
this.setupHooks();
|
|
891
|
+
|
|
892
|
+
if (this.options.autoLoad) {
|
|
893
|
+
this.loadAllPlugins().catch(error => {
|
|
894
|
+
this.log('error', 'Erreur lors du chargement automatique', error.message);
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
setupHooks() {
|
|
900
|
+
// Hooks prédéfinis de Veko.js
|
|
901
|
+
const defaultHooks = [
|
|
902
|
+
'app:init',
|
|
903
|
+
'app:start',
|
|
904
|
+
'app:stop',
|
|
905
|
+
'route:load',
|
|
906
|
+
'route:create',
|
|
907
|
+
'route:delete',
|
|
908
|
+
'request:start',
|
|
909
|
+
'request:end',
|
|
910
|
+
'error:handle',
|
|
911
|
+
'websocket:connect',
|
|
912
|
+
'websocket:disconnect',
|
|
913
|
+
'file:change',
|
|
914
|
+
'plugin:load',
|
|
915
|
+
'plugin:unload',
|
|
916
|
+
'plugin:error',
|
|
917
|
+
'plugin:timeout'
|
|
918
|
+
];
|
|
919
|
+
|
|
920
|
+
defaultHooks.forEach(hookName => {
|
|
921
|
+
this.hooks.set(hookName, []);
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// ============= GESTION DES PLUGINS =============
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Charge un plugin avec gestion d'erreurs améliorée
|
|
929
|
+
* @param {string|Object} plugin - Nom du plugin ou objet plugin
|
|
930
|
+
* @param {Object} config - Configuration du plugin
|
|
931
|
+
*/
|
|
932
|
+
async loadPlugin(plugin, config = {}) {
|
|
933
|
+
let pluginName;
|
|
934
|
+
|
|
935
|
+
try {
|
|
936
|
+
let pluginModule;
|
|
937
|
+
|
|
938
|
+
if (typeof plugin === 'string') {
|
|
939
|
+
pluginName = plugin;
|
|
940
|
+
|
|
941
|
+
// Vérifier si déjà en cours de chargement
|
|
942
|
+
if (this.loadingQueue.has(pluginName)) {
|
|
943
|
+
throw new Error(`Plugin "${pluginName}" déjà en cours de chargement`);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
this.loadingQueue.add(pluginName);
|
|
947
|
+
|
|
948
|
+
// Essayer de charger depuis le dossier plugins
|
|
949
|
+
const pluginPath = path.join(process.cwd(), this.options.pluginsDir, plugin);
|
|
950
|
+
|
|
951
|
+
if (fs.existsSync(`${pluginPath}.js`)) {
|
|
952
|
+
pluginModule = require(`${pluginPath}.js`);
|
|
953
|
+
} else if (fs.existsSync(path.join(pluginPath, 'index.js'))) {
|
|
954
|
+
pluginModule = require(path.join(pluginPath, 'index.js'));
|
|
955
|
+
} else {
|
|
956
|
+
// Essayer depuis node_modules
|
|
957
|
+
try {
|
|
958
|
+
pluginModule = require(plugin);
|
|
959
|
+
} catch (e) {
|
|
960
|
+
throw new Error(`Plugin "${plugin}" introuvable`);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
} else {
|
|
964
|
+
pluginModule = plugin;
|
|
965
|
+
pluginName = plugin.name || 'anonymous';
|
|
966
|
+
this.loadingQueue.add(pluginName);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Vérifier si le plugin est déjà chargé
|
|
970
|
+
if (this.plugins.has(pluginName)) {
|
|
971
|
+
this.log('warning', 'Plugin déjà chargé', pluginName);
|
|
972
|
+
this.loadingQueue.delete(pluginName);
|
|
973
|
+
return this;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Valider la structure du plugin
|
|
977
|
+
this.validatePlugin(pluginModule, pluginName);
|
|
978
|
+
|
|
979
|
+
// Créer l'instance du plugin
|
|
980
|
+
const pluginInstance = {
|
|
981
|
+
name: pluginName,
|
|
982
|
+
version: pluginModule.version || '1.0.0',
|
|
983
|
+
description: pluginModule.description || '',
|
|
984
|
+
author: pluginModule.author || '',
|
|
985
|
+
dependencies: pluginModule.dependencies || [],
|
|
986
|
+
config: { ...pluginModule.defaultConfig, ...config },
|
|
987
|
+
module: pluginModule,
|
|
988
|
+
loaded: false,
|
|
989
|
+
active: false,
|
|
990
|
+
loadTime: Date.now(),
|
|
991
|
+
errorCount: 0
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
// Vérifier les dépendances
|
|
995
|
+
await this.checkDependencies(pluginInstance);
|
|
996
|
+
|
|
997
|
+
// Charger le plugin avec timeout
|
|
998
|
+
await this.executePluginLoadWithTimeout(pluginInstance);
|
|
999
|
+
|
|
1000
|
+
// Enregistrer le plugin
|
|
1001
|
+
this.plugins.set(pluginName, pluginInstance);
|
|
1002
|
+
this.loadOrder.push(pluginName);
|
|
1003
|
+
this.loadingQueue.delete(pluginName);
|
|
1004
|
+
|
|
1005
|
+
this.log('success', 'Plugin chargé', `${pluginName} v${pluginInstance.version}`);
|
|
1006
|
+
this.emit('plugin:loaded', pluginName, pluginInstance);
|
|
1007
|
+
await this.executeHook('plugin:load', pluginName, pluginInstance);
|
|
1008
|
+
|
|
1009
|
+
return this;
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
if (pluginName) {
|
|
1012
|
+
this.loadingQueue.delete(pluginName);
|
|
1013
|
+
this.errorCount.set(pluginName, (this.errorCount.get(pluginName) || 0) + 1);
|
|
1014
|
+
}
|
|
1015
|
+
this.log('error', 'Erreur lors du chargement du plugin', error.message);
|
|
1016
|
+
this.emit('plugin:error', pluginName, error);
|
|
1017
|
+
throw error;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Décharge un plugin avec nettoyage complet
|
|
1023
|
+
* @param {string} pluginName - Nom du plugin
|
|
1024
|
+
*/
|
|
1025
|
+
async unloadPlugin(pluginName) {
|
|
1026
|
+
try {
|
|
1027
|
+
const plugin = this.plugins.get(pluginName);
|
|
1028
|
+
|
|
1029
|
+
if (!plugin) {
|
|
1030
|
+
this.log('warning', 'Plugin introuvable', pluginName);
|
|
1031
|
+
return this;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Exécuter la méthode unload si elle existe
|
|
1035
|
+
if (plugin.module.unload && typeof plugin.module.unload === 'function') {
|
|
1036
|
+
try {
|
|
1037
|
+
await Promise.race([
|
|
1038
|
+
plugin.module.unload(this.app, plugin.config),
|
|
1039
|
+
new Promise((_, reject) =>
|
|
1040
|
+
setTimeout(() => reject(new Error('Timeout')), this.options.timeout)
|
|
1041
|
+
)
|
|
1042
|
+
]);
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
this.log('warning', `Erreur lors du déchargement de ${pluginName}`, error.message);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Nettoyer les hooks du plugin
|
|
1049
|
+
this.cleanupPluginHooks(pluginName);
|
|
1050
|
+
|
|
1051
|
+
// Nettoyer les middlewares du plugin
|
|
1052
|
+
this.cleanupPluginMiddleware(pluginName);
|
|
1053
|
+
|
|
1054
|
+
// Nettoyer les routes du plugin
|
|
1055
|
+
this.cleanupPluginRoutes(pluginName);
|
|
1056
|
+
|
|
1057
|
+
// Nettoyer les commandes du plugin
|
|
1058
|
+
this.cleanupPluginCommands(pluginName);
|
|
1059
|
+
|
|
1060
|
+
// Nettoyer le cache require si possible
|
|
1061
|
+
try {
|
|
1062
|
+
const pluginModule = plugin.module;
|
|
1063
|
+
if (pluginModule && pluginModule.__filename) {
|
|
1064
|
+
delete require.cache[pluginModule.__filename];
|
|
1065
|
+
}
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
// Ignore cache cleanup errors
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Supprimer de la liste
|
|
1071
|
+
this.plugins.delete(pluginName);
|
|
1072
|
+
this.loadOrder = this.loadOrder.filter(name => name !== pluginName);
|
|
1073
|
+
this.errorCount.delete(pluginName);
|
|
1074
|
+
|
|
1075
|
+
this.log('success', 'Plugin déchargé', pluginName);
|
|
1076
|
+
this.emit('plugin:unloaded', pluginName);
|
|
1077
|
+
await this.executeHook('plugin:unload', pluginName);
|
|
1078
|
+
|
|
1079
|
+
return this;
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
this.log('error', 'Erreur lors du déchargement du plugin', error.message);
|
|
1082
|
+
throw error;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Recharge un plugin avec nouvelle configuration
|
|
1088
|
+
* @param {string} pluginName - Nom du plugin
|
|
1089
|
+
* @param {Object} newConfig - Nouvelle configuration
|
|
1090
|
+
*/
|
|
1091
|
+
async reloadPlugin(pluginName, newConfig = {}) {
|
|
1092
|
+
const plugin = this.plugins.get(pluginName);
|
|
1093
|
+
if (!plugin) {
|
|
1094
|
+
throw new Error(`Plugin "${pluginName}" introuvable`);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const config = { ...plugin.config, ...newConfig };
|
|
1098
|
+
|
|
1099
|
+
await this.unloadPlugin(pluginName);
|
|
1100
|
+
await this.loadPlugin(pluginName, config);
|
|
1101
|
+
|
|
1102
|
+
this.log('success', 'Plugin rechargé', pluginName);
|
|
1103
|
+
return this;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Charge tous les plugins du dossier plugins avec gestion des dépendances
|
|
1108
|
+
*/
|
|
1109
|
+
async loadAllPlugins() {
|
|
1110
|
+
const pluginsPath = path.join(process.cwd(), this.options.pluginsDir);
|
|
1111
|
+
|
|
1112
|
+
if (!fs.existsSync(pluginsPath)) {
|
|
1113
|
+
this.log('info', 'Dossier plugins créé', `📁 ${this.options.pluginsDir}`);
|
|
1114
|
+
fs.mkdirSync(pluginsPath, { recursive: true });
|
|
1115
|
+
return this;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const files = fs.readdirSync(pluginsPath);
|
|
1119
|
+
const pluginFiles = files.filter(file =>
|
|
1120
|
+
file.endsWith('.js') || file.endsWith('.ts') ||
|
|
1121
|
+
(fs.statSync(path.join(pluginsPath, file)).isDirectory() &&
|
|
1122
|
+
(fs.existsSync(path.join(pluginsPath, file, 'index.js')) ||
|
|
1123
|
+
fs.existsSync(path.join(pluginsPath, file, 'index.ts'))))
|
|
1124
|
+
);
|
|
1125
|
+
|
|
1126
|
+
if (pluginFiles.length === 0) {
|
|
1127
|
+
this.log('info', 'Aucun plugin trouvé', `📁 ${this.options.pluginsDir}`);
|
|
1128
|
+
return this;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
this.log('info', 'Chargement des plugins...', `📦 ${pluginFiles.length} trouvés`);
|
|
1132
|
+
|
|
1133
|
+
// Collect plugin information and dependencies
|
|
1134
|
+
const pluginsInfo = [];
|
|
1135
|
+
for (const file of pluginFiles) {
|
|
1136
|
+
try {
|
|
1137
|
+
const pluginName = file.replace(/\.(js|ts)$/, '');
|
|
1138
|
+
const pluginModule = await this.resolvePlugin(pluginName);
|
|
1139
|
+
|
|
1140
|
+
// Log dependencies for debugging
|
|
1141
|
+
this.log('info', `Analyse du plugin ${pluginName}`,
|
|
1142
|
+
`Dépendances: ${JSON.stringify(pluginModule.dependencies || [])}`);
|
|
1143
|
+
|
|
1144
|
+
pluginsInfo.push({
|
|
1145
|
+
name: pluginName,
|
|
1146
|
+
dependencies: pluginModule.dependencies || [],
|
|
1147
|
+
module: pluginModule
|
|
1148
|
+
});
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
this.log('warning', `Problème d'analyse`, `${file} → ${error.message}`);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Sort plugins by dependencies
|
|
1155
|
+
const loadOrder = this.sortPluginsByDependencies(pluginsInfo);
|
|
1156
|
+
|
|
1157
|
+
// Log the calculated load order
|
|
1158
|
+
this.log('info', 'Ordre de chargement des plugins', loadOrder.join(' → '));
|
|
1159
|
+
|
|
1160
|
+
// Load plugins in dependency order with retry mechanism
|
|
1161
|
+
const results = { success: 0, failed: 0, errors: [] };
|
|
1162
|
+
|
|
1163
|
+
for (const pluginName of loadOrder) {
|
|
1164
|
+
let retries = 0;
|
|
1165
|
+
let loaded = false;
|
|
1166
|
+
|
|
1167
|
+
while (retries < this.options.maxRetries && !loaded) {
|
|
1168
|
+
try {
|
|
1169
|
+
await this.loadPlugin(pluginName);
|
|
1170
|
+
loaded = true;
|
|
1171
|
+
results.success++;
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
retries++;
|
|
1174
|
+
results.errors.push({ plugin: pluginName, error: error.message, attempt: retries });
|
|
1175
|
+
|
|
1176
|
+
if (retries < this.options.maxRetries) {
|
|
1177
|
+
this.log('warning', `Tentative ${retries + 1}/${this.options.maxRetries}`,
|
|
1178
|
+
`${pluginName} → ${error.message}`);
|
|
1179
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * retries));
|
|
1180
|
+
} else {
|
|
1181
|
+
this.log('error', `Échec définitif`, `${pluginName} → ${error.message}`);
|
|
1182
|
+
results.failed++;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Log final results
|
|
1189
|
+
this.log('success', 'Chargement terminé',
|
|
1190
|
+
`✅ ${results.success} réussis, ❌ ${results.failed} échoués`);
|
|
1191
|
+
|
|
1192
|
+
if (results.errors.length > 0) {
|
|
1193
|
+
this.log('warning', 'Erreurs détaillées',
|
|
1194
|
+
results.errors.map(e => `${e.plugin}: ${e.error}`).join('; '));
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
return this;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* Trie les plugins par ordre de dépendances
|
|
1202
|
+
*/
|
|
1203
|
+
sortPluginsByDependencies(pluginsInfo) {
|
|
1204
|
+
const loadOrder = [];
|
|
1205
|
+
const loaded = new Set();
|
|
1206
|
+
const loading = new Set();
|
|
1207
|
+
|
|
1208
|
+
const loadPlugin = (plugin) => {
|
|
1209
|
+
// Skip if already loaded
|
|
1210
|
+
if (loaded.has(plugin.name)) return;
|
|
1211
|
+
|
|
1212
|
+
// Detect circular dependencies
|
|
1213
|
+
if (loading.has(plugin.name)) {
|
|
1214
|
+
this.log('warning', 'Dépendance circulaire détectée', plugin.name);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
loading.add(plugin.name);
|
|
1219
|
+
|
|
1220
|
+
// Load dependencies first
|
|
1221
|
+
for (const depName of plugin.dependencies) {
|
|
1222
|
+
const depPlugin = pluginsInfo.find(p => p.name === depName);
|
|
1223
|
+
if (depPlugin) {
|
|
1224
|
+
loadPlugin(depPlugin);
|
|
1225
|
+
} else {
|
|
1226
|
+
this.log('warning', `Dépendance manquante`, `${plugin.name} → ${depName}`);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Then load the plugin itself
|
|
1231
|
+
loadOrder.push(plugin.name);
|
|
1232
|
+
loaded.add(plugin.name);
|
|
1233
|
+
loading.delete(plugin.name);
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
// Process all plugins
|
|
1237
|
+
for (const plugin of pluginsInfo) {
|
|
1238
|
+
loadPlugin(plugin);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
return loadOrder;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// ============= VALIDATION ET DÉPENDANCES =============
|
|
1245
|
+
|
|
1246
|
+
validatePlugin(pluginModule, pluginName) {
|
|
1247
|
+
if (!pluginModule || typeof pluginModule !== 'object') {
|
|
1248
|
+
throw new Error(`Plugin "${pluginName}" doit exporter un objet`);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
if (!pluginModule.load || typeof pluginModule.load !== 'function') {
|
|
1252
|
+
throw new Error(`Plugin "${pluginName}" doit avoir une méthode load()`);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// Validation des métadonnées
|
|
1256
|
+
if (pluginModule.name && typeof pluginModule.name !== 'string') {
|
|
1257
|
+
throw new Error(`Plugin "${pluginName}": name doit être une chaîne`);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (pluginModule.version && typeof pluginModule.version !== 'string') {
|
|
1261
|
+
throw new Error(`Plugin "${pluginName}": version doit être une chaîne`);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Validation optionnelle des autres méthodes
|
|
1265
|
+
const optionalMethods = ['unload', 'activate', 'deactivate'];
|
|
1266
|
+
optionalMethods.forEach(method => {
|
|
1267
|
+
if (pluginModule[method] && typeof pluginModule[method] !== 'function') {
|
|
1268
|
+
throw new Error(`Plugin "${pluginName}": ${method} doit être une fonction`);
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
// Validation des dépendances
|
|
1273
|
+
if (pluginModule.dependencies && !Array.isArray(pluginModule.dependencies)) {
|
|
1274
|
+
throw new Error(`Plugin "${pluginName}": dependencies doit être un tableau`);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
async checkDependencies(plugin) {
|
|
1279
|
+
if (!plugin.dependencies || plugin.dependencies.length === 0) {
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
const missing = [];
|
|
1284
|
+
for (const dep of plugin.dependencies) {
|
|
1285
|
+
if (!this.plugins.has(dep)) {
|
|
1286
|
+
missing.push(dep);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
if (missing.length > 0) {
|
|
1291
|
+
throw new Error(`Plugin "${plugin.name}" nécessite: ${missing.join(', ')}`);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
async executePluginLoadWithTimeout(plugin) {
|
|
1296
|
+
const pluginContext = this.createPluginContext(plugin);
|
|
1297
|
+
|
|
1298
|
+
try {
|
|
1299
|
+
// Charger le plugin avec timeout
|
|
1300
|
+
await Promise.race([
|
|
1301
|
+
plugin.module.load(this.app, plugin.config, pluginContext),
|
|
1302
|
+
new Promise((_, reject) =>
|
|
1303
|
+
setTimeout(() => reject(new Error('Timeout de chargement dépassé')), this.options.timeout)
|
|
1304
|
+
)
|
|
1305
|
+
]);
|
|
1306
|
+
|
|
1307
|
+
plugin.loaded = true;
|
|
1308
|
+
plugin.active = true;
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
if (error.message.includes('Timeout')) {
|
|
1311
|
+
this.emit('plugin:timeout', plugin.name);
|
|
1312
|
+
}
|
|
1313
|
+
throw error;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// ============= CONTEXTE ET API POUR PLUGINS =============
|
|
1318
|
+
|
|
1319
|
+
createPluginContext(plugin) {
|
|
1320
|
+
return {
|
|
1321
|
+
// Accès au système de hooks
|
|
1322
|
+
hook: (hookName, callback, priority = 10) =>
|
|
1323
|
+
this.addHook(hookName, callback, plugin.name, priority),
|
|
1324
|
+
removeHook: (hookName, callback) =>
|
|
1325
|
+
this.removeHook(hookName, callback, plugin.name),
|
|
1326
|
+
|
|
1327
|
+
// Ajout de middleware
|
|
1328
|
+
addMiddleware: (middleware) =>
|
|
1329
|
+
this.addPluginMiddleware(middleware, plugin.name),
|
|
1330
|
+
|
|
1331
|
+
// Ajout de routes
|
|
1332
|
+
addRoute: (method, path, handler) =>
|
|
1333
|
+
this.addPluginRoute(method, path, handler, plugin.name),
|
|
1334
|
+
|
|
1335
|
+
// Ajout de commandes CLI
|
|
1336
|
+
addCommand: (name, handler, description) =>
|
|
1337
|
+
this.addPluginCommand(name, handler, description, plugin.name),
|
|
1338
|
+
|
|
1339
|
+
// Logs avec nom du plugin
|
|
1340
|
+
log: (type, message, details = '') =>
|
|
1341
|
+
this.log(type, `[${plugin.name}] ${message}`, details),
|
|
1342
|
+
|
|
1343
|
+
// Accès aux autres plugins
|
|
1344
|
+
getPlugin: (name) => this.getPlugin(name),
|
|
1345
|
+
listPlugins: () => this.listPlugins(),
|
|
1346
|
+
|
|
1347
|
+
// Configuration
|
|
1348
|
+
getConfig: () => ({ ...plugin.config }),
|
|
1349
|
+
updateConfig: (newConfig) =>
|
|
1350
|
+
this.updatePluginConfig(plugin.name, newConfig),
|
|
1351
|
+
|
|
1352
|
+
// Stockage persistant pour le plugin
|
|
1353
|
+
storage: this.createPluginStorage(plugin.name),
|
|
1354
|
+
|
|
1355
|
+
// Émission d'événements
|
|
1356
|
+
emit: (eventName, ...args) =>
|
|
1357
|
+
this.emit(`plugin:${plugin.name}:${eventName}`, ...args),
|
|
1358
|
+
|
|
1359
|
+
// Accès à l'application
|
|
1360
|
+
app: this.app
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
createPluginStorage(pluginName) {
|
|
1365
|
+
const storageFile = path.join(process.cwd(), 'data', 'plugins', `${pluginName}.json`);
|
|
1366
|
+
|
|
1367
|
+
return {
|
|
1368
|
+
get: (key, defaultValue = null) => {
|
|
1369
|
+
try {
|
|
1370
|
+
if (!fs.existsSync(storageFile)) return defaultValue;
|
|
1371
|
+
const data = JSON.parse(fs.readFileSync(storageFile, 'utf8'));
|
|
1372
|
+
return key ? (data[key] !== undefined ? data[key] : defaultValue) : data;
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
this.log('warning', `Erreur lecture storage pour ${pluginName}`, error.message);
|
|
1375
|
+
return defaultValue;
|
|
1376
|
+
}
|
|
1377
|
+
},
|
|
1378
|
+
|
|
1379
|
+
set: (key, value) => {
|
|
1380
|
+
try {
|
|
1381
|
+
const dir = path.dirname(storageFile);
|
|
1382
|
+
if (!fs.existsSync(dir)) {
|
|
1383
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
let data = {};
|
|
1387
|
+
if (fs.existsSync(storageFile)) {
|
|
1388
|
+
try {
|
|
1389
|
+
data = JSON.parse(fs.readFileSync(storageFile, 'utf8'));
|
|
1390
|
+
} catch (error) {
|
|
1391
|
+
this.log('warning', `Fichier storage corrompu pour ${pluginName}`, 'réinitialisation');
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
if (typeof key === 'object') {
|
|
1396
|
+
data = { ...data, ...key };
|
|
1397
|
+
} else {
|
|
1398
|
+
data[key] = value;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
fs.writeFileSync(storageFile, JSON.stringify(data, null, 2));
|
|
1402
|
+
return true;
|
|
1403
|
+
} catch (error) {
|
|
1404
|
+
this.log('error', `Erreur écriture storage pour ${pluginName}`, error.message);
|
|
1405
|
+
return false;
|
|
1406
|
+
}
|
|
1407
|
+
},
|
|
1408
|
+
|
|
1409
|
+
delete: (key) => {
|
|
1410
|
+
try {
|
|
1411
|
+
if (!fs.existsSync(storageFile)) return true;
|
|
1412
|
+
const data = JSON.parse(fs.readFileSync(storageFile, 'utf8'));
|
|
1413
|
+
delete data[key];
|
|
1414
|
+
fs.writeFileSync(storageFile, JSON.stringify(data, null, 2));
|
|
1415
|
+
return true;
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
this.log('error', `Erreur suppression storage pour ${pluginName}`, error.message);
|
|
1418
|
+
return false;
|
|
1419
|
+
}
|
|
1420
|
+
},
|
|
1421
|
+
|
|
1422
|
+
clear: () => {
|
|
1423
|
+
try {
|
|
1424
|
+
if (fs.existsSync(storageFile)) {
|
|
1425
|
+
fs.unlinkSync(storageFile);
|
|
1426
|
+
}
|
|
1427
|
+
return true;
|
|
1428
|
+
} catch (error) {
|
|
1429
|
+
this.log('error', `Erreur nettoyage storage pour ${pluginName}`, error.message);
|
|
1430
|
+
return false;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// ============= SYSTÈME DE HOOKS =============
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* Ajoute un hook avec priorité
|
|
1440
|
+
* @param {string} hookName - Nom du hook
|
|
1441
|
+
* @param {Function} callback - Fonction à exécuter
|
|
1442
|
+
* @param {string} pluginName - Nom du plugin
|
|
1443
|
+
* @param {number} priority - Priorité (plus élevé = exécuté en premier)
|
|
1444
|
+
*/
|
|
1445
|
+
addHook(hookName, callback, pluginName = 'core', priority = 10) {
|
|
1446
|
+
if (!this.hooks.has(hookName)) {
|
|
1447
|
+
this.hooks.set(hookName, []);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
if (typeof callback !== 'function') {
|
|
1451
|
+
throw new Error(`Hook callback doit être une fonction pour ${hookName}`);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
this.hooks.get(hookName).push({
|
|
1455
|
+
callback,
|
|
1456
|
+
plugin: pluginName,
|
|
1457
|
+
priority: Number(priority) || 10
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
// Trier par priorité (plus élevé en premier)
|
|
1461
|
+
this.hooks.get(hookName).sort((a, b) => b.priority - a.priority);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* Supprime un hook
|
|
1466
|
+
* @param {string} hookName - Nom du hook
|
|
1467
|
+
* @param {Function} callback - Fonction à supprimer
|
|
1468
|
+
* @param {string} pluginName - Nom du plugin
|
|
1469
|
+
*/
|
|
1470
|
+
removeHook(hookName, callback, pluginName) {
|
|
1471
|
+
if (!this.hooks.has(hookName)) return;
|
|
1472
|
+
|
|
1473
|
+
const hooks = this.hooks.get(hookName);
|
|
1474
|
+
this.hooks.set(hookName, hooks.filter(hook =>
|
|
1475
|
+
!(hook.callback === callback && hook.plugin === pluginName)
|
|
1476
|
+
));
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/**
|
|
1480
|
+
* Execute un hook avec gestion d'erreur améliorée
|
|
1481
|
+
* @param {string} hookName - Nom du hook
|
|
1482
|
+
* @param {...any} args - Arguments à passer aux callbacks
|
|
1483
|
+
*/
|
|
1484
|
+
async executeHook(hookName, ...args) {
|
|
1485
|
+
if (!this.hooks.has(hookName)) return args;
|
|
1486
|
+
|
|
1487
|
+
const hooks = this.hooks.get(hookName);
|
|
1488
|
+
let result = args;
|
|
1489
|
+
|
|
1490
|
+
for (const hook of hooks) {
|
|
1491
|
+
try {
|
|
1492
|
+
const hookResult = await Promise.race([
|
|
1493
|
+
hook.callback(...result),
|
|
1494
|
+
new Promise((_, reject) =>
|
|
1495
|
+
setTimeout(() => reject(new Error('Hook timeout')), 5000)
|
|
1496
|
+
)
|
|
1497
|
+
]);
|
|
1498
|
+
|
|
1499
|
+
if (hookResult !== undefined) {
|
|
1500
|
+
result = Array.isArray(hookResult) ? hookResult : [hookResult];
|
|
1501
|
+
}
|
|
1502
|
+
} catch (error) {
|
|
1503
|
+
this.log('error', `Erreur dans le hook ${hookName}`,
|
|
1504
|
+
`Plugin: ${hook.plugin} → ${error.message}`);
|
|
1505
|
+
this.emit('hook:error', hookName, hook.plugin, error);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
return result;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// ============= GESTION DES ÉLÉMENTS AJOUTÉS PAR LES PLUGINS =============
|
|
1513
|
+
|
|
1514
|
+
addPluginMiddleware(middleware, pluginName) {
|
|
1515
|
+
if (typeof middleware !== 'function') {
|
|
1516
|
+
throw new Error('Le middleware doit être une fonction');
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
this.middleware.push({ middleware, plugin: pluginName });
|
|
1520
|
+
|
|
1521
|
+
if (this.app && this.app.use) {
|
|
1522
|
+
this.app.use(middleware);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
addPluginRoute(method, path, handler, pluginName) {
|
|
1527
|
+
if (!method || !path || !handler) {
|
|
1528
|
+
throw new Error('Méthode, chemin et handler requis pour une route');
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
const route = { method, path, handler, plugin: pluginName };
|
|
1532
|
+
this.routes.push(route);
|
|
1533
|
+
|
|
1534
|
+
if (this.app && this.app.createRoute) {
|
|
1535
|
+
this.app.createRoute(method, path, handler);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
addPluginCommand(name, handler, description, pluginName) {
|
|
1540
|
+
if (!name || !handler) {
|
|
1541
|
+
throw new Error('Nom et handler requis pour une commande');
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
this.commands.set(name, {
|
|
1545
|
+
handler,
|
|
1546
|
+
description: description || '',
|
|
1547
|
+
plugin: pluginName
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// ============= NETTOYAGE =============
|
|
1552
|
+
|
|
1553
|
+
cleanupPluginHooks(pluginName) {
|
|
1554
|
+
this.hooks.forEach((hooks, hookName) => {
|
|
1555
|
+
this.hooks.set(hookName, hooks.filter(hook => hook.plugin !== pluginName));
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
cleanupPluginMiddleware(pluginName) {
|
|
1560
|
+
this.middleware = this.middleware.filter(item => item.plugin !== pluginName);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
cleanupPluginRoutes(pluginName) {
|
|
1564
|
+
this.routes = this.routes.filter(route => route.plugin !== pluginName);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
cleanupPluginCommands(pluginName) {
|
|
1568
|
+
for (const [name, command] of this.commands.entries()) {
|
|
1569
|
+
if (command.plugin === pluginName) {
|
|
1570
|
+
this.commands.delete(name);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// ============= UTILITAIRES =============
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* Obtient un plugin
|
|
1579
|
+
* @param {string} pluginName - Nom du plugin
|
|
1580
|
+
*/
|
|
1581
|
+
getPlugin(pluginName) {
|
|
1582
|
+
return this.plugins.get(pluginName) || null;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/**
|
|
1586
|
+
* Liste tous les plugins
|
|
1587
|
+
*/
|
|
1588
|
+
listPlugins() {
|
|
1589
|
+
return Array.from(this.plugins.values()).map(plugin => ({
|
|
1590
|
+
name: plugin.name,
|
|
1591
|
+
version: plugin.version,
|
|
1592
|
+
description: plugin.description,
|
|
1593
|
+
author: plugin.author,
|
|
1594
|
+
loaded: plugin.loaded,
|
|
1595
|
+
active: plugin.active,
|
|
1596
|
+
loadTime: plugin.loadTime,
|
|
1597
|
+
errorCount: plugin.errorCount
|
|
1598
|
+
}));
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
/**
|
|
1602
|
+
* Met à jour la configuration d'un plugin
|
|
1603
|
+
*/
|
|
1604
|
+
updatePluginConfig(pluginName, newConfig) {
|
|
1605
|
+
const plugin = this.plugins.get(pluginName);
|
|
1606
|
+
if (plugin) {
|
|
1607
|
+
plugin.config = { ...plugin.config, ...newConfig };
|
|
1608
|
+
return true;
|
|
1609
|
+
}
|
|
1610
|
+
return false;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
/**
|
|
1614
|
+
* Active/désactive un plugin
|
|
1615
|
+
*/
|
|
1616
|
+
async togglePlugin(pluginName, active = null) {
|
|
1617
|
+
const plugin = this.plugins.get(pluginName);
|
|
1618
|
+
if (!plugin) return false;
|
|
1619
|
+
|
|
1620
|
+
const newState = active !== null ? active : !plugin.active;
|
|
1621
|
+
|
|
1622
|
+
try {
|
|
1623
|
+
if (newState && !plugin.active) {
|
|
1624
|
+
// Activer
|
|
1625
|
+
if (plugin.module.activate) {
|
|
1626
|
+
await plugin.module.activate(this.app, plugin.config);
|
|
1627
|
+
}
|
|
1628
|
+
plugin.active = true;
|
|
1629
|
+
this.log('success', 'Plugin activé', pluginName);
|
|
1630
|
+
this.emit('plugin:activated', pluginName);
|
|
1631
|
+
} else if (!newState && plugin.active) {
|
|
1632
|
+
// Désactiver
|
|
1633
|
+
if (plugin.module.deactivate) {
|
|
1634
|
+
await plugin.module.deactivate(this.app, plugin.config);
|
|
1635
|
+
}
|
|
1636
|
+
plugin.active = false;
|
|
1637
|
+
this.log('warning', 'Plugin désactivé', pluginName);
|
|
1638
|
+
this.emit('plugin:deactivated', pluginName);
|
|
1639
|
+
}
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
this.log('error', `Erreur lors du changement d'état de ${pluginName}`, error.message);
|
|
1642
|
+
throw error;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
return plugin.active;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
/**
|
|
1649
|
+
* Vérifie la santé d'un plugin
|
|
1650
|
+
*/
|
|
1651
|
+
checkPluginHealth(pluginName) {
|
|
1652
|
+
const plugin = this.plugins.get(pluginName);
|
|
1653
|
+
if (!plugin) return null;
|
|
1654
|
+
|
|
1655
|
+
const errorCount = this.errorCount.get(pluginName) || 0;
|
|
1656
|
+
const uptime = Date.now() - plugin.loadTime;
|
|
1657
|
+
|
|
1658
|
+
return {
|
|
1659
|
+
name: pluginName,
|
|
1660
|
+
loaded: plugin.loaded,
|
|
1661
|
+
active: plugin.active,
|
|
1662
|
+
errorCount,
|
|
1663
|
+
uptime,
|
|
1664
|
+
health: errorCount === 0 ? 'healthy' : errorCount < 5 ? 'warning' : 'critical'
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// ============= LOGS =============
|
|
1669
|
+
|
|
1670
|
+
log(type, message, details = '') {
|
|
1671
|
+
const timestamp = new Date().toLocaleTimeString('fr-FR');
|
|
1672
|
+
const prefix = `${colors.gray}[${timestamp}]${colors.reset}`;
|
|
1673
|
+
|
|
1674
|
+
const logStyles = {
|
|
1675
|
+
success: { badge: `${colors.bgGreen}${colors.white} 🔌 `, text: `${colors.green}${colors.bright}` },
|
|
1676
|
+
error: { badge: `${colors.bgRed}${colors.white} ❌ `, text: `${colors.red}${colors.bright}` },
|
|
1677
|
+
warning: { badge: `${colors.bgYellow}${colors.white} ⚠️ `, text: `${colors.yellow}${colors.bright}` },
|
|
1678
|
+
info: { badge: `${colors.bgBlue}${colors.white} 💎 `, text: `${colors.blue}${colors.bright}` },
|
|
1679
|
+
debug: { badge: `${colors.bgMagenta}${colors.white} 🐛 `, text: `${colors.magenta}${colors.bright}` }
|
|
1680
|
+
};
|
|
1681
|
+
|
|
1682
|
+
const style = logStyles[type] || logStyles.info;
|
|
1683
|
+
|
|
1684
|
+
const logMessage = `${prefix} ${style.badge}${colors.reset} ${style.text}${message}${colors.reset} ${colors.gray}${details}${colors.reset}`;
|
|
1685
|
+
console.log(logMessage);
|
|
1686
|
+
|
|
1687
|
+
// Émettre l'événement de log pour les plugins
|
|
1688
|
+
this.emit('log', { type, message, details, timestamp: new Date() });
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// ============= API PUBLIQUE =============
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* Crée un plugin simple depuis une fonction
|
|
1695
|
+
*/
|
|
1696
|
+
createSimplePlugin(name, loadFunction, options = {}) {
|
|
1697
|
+
return {
|
|
1698
|
+
name,
|
|
1699
|
+
version: options.version || '1.0.0',
|
|
1700
|
+
description: options.description || '',
|
|
1701
|
+
dependencies: options.dependencies || [],
|
|
1702
|
+
load: loadFunction,
|
|
1703
|
+
unload: options.unload,
|
|
1704
|
+
activate: options.activate,
|
|
1705
|
+
deactivate: options.deactivate,
|
|
1706
|
+
...options
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
/**
|
|
1711
|
+
* Statistiques détaillées des plugins
|
|
1712
|
+
*/
|
|
1713
|
+
getStats() {
|
|
1714
|
+
const plugins = Array.from(this.plugins.values());
|
|
1715
|
+
|
|
1716
|
+
return {
|
|
1717
|
+
total: plugins.length,
|
|
1718
|
+
active: plugins.filter(p => p.active).length,
|
|
1719
|
+
loaded: plugins.filter(p => p.loaded).length,
|
|
1720
|
+
loading: this.loadingQueue.size,
|
|
1721
|
+
hooks: this.hooks.size,
|
|
1722
|
+
totalHookCallbacks: Array.from(this.hooks.values()).reduce((sum, hooks) => sum + hooks.length, 0),
|
|
1723
|
+
middleware: this.middleware.length,
|
|
1724
|
+
routes: this.routes.length,
|
|
1725
|
+
commands: this.commands.size,
|
|
1726
|
+
errors: Array.from(this.errorCount.values()).reduce((sum, count) => sum + count, 0),
|
|
1727
|
+
uptime: plugins.length > 0 ? Date.now() - Math.min(...plugins.map(p => p.loadTime)) : 0
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
/**
|
|
1732
|
+
* Sauvegarde l'état des plugins
|
|
1733
|
+
*/
|
|
1734
|
+
saveState() {
|
|
1735
|
+
const state = {
|
|
1736
|
+
loadOrder: this.loadOrder,
|
|
1737
|
+
pluginConfigs: Array.from(this.plugins.entries()).map(([name, plugin]) => ({
|
|
1738
|
+
name,
|
|
1739
|
+
config: plugin.config,
|
|
1740
|
+
active: plugin.active
|
|
1741
|
+
})),
|
|
1742
|
+
timestamp: Date.now()
|
|
1743
|
+
};
|
|
1744
|
+
|
|
1745
|
+
try {
|
|
1746
|
+
const stateFile = path.join(process.cwd(), 'data', 'plugin-state.json');
|
|
1747
|
+
const dir = path.dirname(stateFile);
|
|
1748
|
+
|
|
1749
|
+
if (!fs.existsSync(dir)) {
|
|
1750
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
1754
|
+
return true;
|
|
1755
|
+
} catch (error) {
|
|
1756
|
+
this.log('error', 'Erreur sauvegarde état plugins', error.message);
|
|
1757
|
+
return false;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
/**
|
|
1762
|
+
* Restaure l'état des plugins
|
|
1763
|
+
*/
|
|
1764
|
+
async restoreState() {
|
|
1765
|
+
try {
|
|
1766
|
+
const stateFile = path.join(process.cwd(), 'data', 'plugin-state.json');
|
|
1767
|
+
|
|
1768
|
+
if (!fs.existsSync(stateFile)) {
|
|
1769
|
+
return false;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
1773
|
+
|
|
1774
|
+
// Charger les plugins dans l'ordre sauvegardé
|
|
1775
|
+
for (const pluginConfig of state.pluginConfigs) {
|
|
1776
|
+
try {
|
|
1777
|
+
await this.loadPlugin(pluginConfig.name, pluginConfig.config);
|
|
1778
|
+
if (!pluginConfig.active) {
|
|
1779
|
+
await this.togglePlugin(pluginConfig.name, false);
|
|
1780
|
+
}
|
|
1781
|
+
} catch (error) {
|
|
1782
|
+
this.log('warning', `Impossible de restaurer ${pluginConfig.name}`, error.message);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
this.log('success', 'État des plugins restauré', `${state.pluginConfigs.length} plugins`);
|
|
1787
|
+
return true;
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
this.log('error', 'Erreur restauration état plugins', error.message);
|
|
1790
|
+
return false;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
module.exports = PluginManager;
|