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.
@@ -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;