UNPKG

veko

Version:

🚀 Ultra-modern Node.js framework with hot reload, plugins, authentication, and advanced security features

1,614 lines (1,355 loc) ‱ 56.2 kB
const path = require('path'); const fs = require('fs'); const EventEmitter = require('events'); // Couleurs pour les logs const colors = { reset: '\x1b[0m', bright: '\x1b[1m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', gray: '\x1b[90m', bgGreen: '\x1b[42m', bgBlue: '\x1b[44m', bgMagenta: '\x1b[45m', bgCyan: '\x1b[46m', bgYellow: '\x1b[43m', bgRed: '\x1b[41m', white: '\x1b[37m' }; class PluginManager extends EventEmitter { constructor(app, options = {}) { super(); this.app = app; this.options = { pluginsDir: 'plugins', autoLoad: true, enableHooks: true, enableAPI: true, maxRetries: 3, timeout: 30000, supportTypeScript: true, devMode: process.env.NODE_ENV === 'development', watchMode: false, enableMetrics: true, enableValidation: true, allowHotReload: true, enableSandbox: false, ...options }; this.plugins = new Map(); this.hooks = new Map(); this.middleware = []; this.routes = []; this.commands = new Map(); this.loadOrder = []; this.loadingQueue = new Set(); this.errorCount = new Map(); this.metrics = new Map(); this.watchers = new Map(); this.schemas = new Map(); this.devTools = new Map(); // Support TypeScript this.tsSupport = this.initTypeScriptSupport(); this.init(); } // ============= SUPPORT TYPESCRIPT ============= initTypeScriptSupport() { if (!this.options.supportTypeScript) return null; try { // Essayer de charger ts-node pour l'exĂ©cution directe de TS const tsNode = require('ts-node'); tsNode.register({ transpileOnly: true, compilerOptions: { module: 'commonjs', target: 'es2020', esModuleInterop: true, allowSyntheticDefaultImports: true, experimentalDecorators: true, emitDecoratorMetadata: true } }); this.log('info', 'Support TypeScript activĂ©', 'đŸ”· ts-node configurĂ©'); return { enabled: true, runtime: 'ts-node' }; } catch (error) { // Fallback: support de la compilation Ă  la volĂ©e this.log('warning', 'ts-node non trouvĂ©', 'compilation Ă  la volĂ©e activĂ©e'); return { enabled: true, runtime: 'compile' }; } } async loadTypeScriptPlugin(pluginPath) { if (!this.tsSupport?.enabled) { throw new Error('Support TypeScript non activĂ©'); } if (this.tsSupport.runtime === 'ts-node') { // Chargement direct avec ts-node return require(pluginPath); } else { // Compilation Ă  la volĂ©e const typescript = require('typescript'); const tsContent = fs.readFileSync(pluginPath, 'utf8'); const result = typescript.transpile(tsContent, { module: typescript.ModuleKind.CommonJS, target: typescript.ScriptTarget.ES2020, esModuleInterop: true, allowSyntheticDefaultImports: true }); // Écrire le fichier JS temporaire const jsPath = pluginPath.replace('.ts', '.js'); fs.writeFileSync(jsPath, result); try { const plugin = require(jsPath); fs.unlinkSync(jsPath); // Nettoyer le fichier temporaire return plugin; } catch (error) { if (fs.existsSync(jsPath)) fs.unlinkSync(jsPath); throw error; } } } // ============= INITIALISATION AVANCÉE ============= init() { this.setupHooks(); this.setupDevTools(); this.setupMetrics(); this.setupWatcher(); if (this.options.autoLoad) { this.loadAllPlugins().catch(error => { this.log('error', 'Erreur lors du chargement automatique', error.message); }); } } setupHooks() { const defaultHooks = [ 'app:init', 'app:start', 'app:stop', 'app:restart', 'route:load', 'route:create', 'route:delete', 'route:update', 'request:start', 'request:end', 'request:error', 'response:start', 'response:end', 'response:error', 'middleware:add', 'middleware:remove', 'error:handle', 'error:critical', 'websocket:connect', 'websocket:disconnect', 'websocket:message', 'file:change', 'file:add', 'file:delete', 'plugin:load', 'plugin:unload', 'plugin:error', 'plugin:timeout', 'plugin:activate', 'plugin:deactivate', 'plugin:reload', 'config:change', 'config:validate', 'database:connect', 'database:disconnect', 'database:query', 'cache:set', 'cache:get', 'cache:delete', 'cache:clear', 'auth:login', 'auth:logout', 'auth:register', 'dev:hotreload', 'dev:debug', 'dev:profile' ]; defaultHooks.forEach(hookName => { this.hooks.set(hookName, []); }); } setupDevTools() { if (!this.options.devMode) return; this.devTools.set('profiler', { start: (name) => { const start = process.hrtime.bigint(); return { end: () => { const end = process.hrtime.bigint(); const duration = Number(end - start) / 1000000; // ms this.log('debug', `Profil: ${name}`, `${duration.toFixed(2)}ms`); return duration; } }; } }); this.devTools.set('debugger', { breakpoint: (message, data = {}) => { if (this.options.devMode) { console.log(`🔮 BREAKPOINT: ${message}`, data); debugger; // eslint-disable-line no-debugger } }, inspect: (obj, label = 'Object') => { console.log(`🔍 ${label}:`, require('util').inspect(obj, { colors: true, depth: 3 })); } }); this.devTools.set('hotreload', { enable: () => this.enableHotReload(), disable: () => this.disableHotReload(), trigger: (pluginName) => this.triggerHotReload(pluginName) }); } setupMetrics() { if (!this.options.enableMetrics) return; setInterval(() => { this.collectMetrics(); }, 30000); // Collecte toutes les 30 secondes } setupWatcher() { if (!this.options.watchMode && !this.options.allowHotReload) return; const chokidar = require('chokidar'); const pluginsPath = path.join(process.cwd(), this.options.pluginsDir); const watcher = chokidar.watch([ `${pluginsPath}/**/*.js`, `${pluginsPath}/**/*.ts`, `${pluginsPath}/**/package.json` ], { ignored: /node_modules/, persistent: true }); watcher.on('change', async (filePath) => { const pluginName = this.getPluginNameFromFile(filePath); if (pluginName && this.plugins.has(pluginName)) { this.log('info', 'Fichier modifiĂ© dĂ©tectĂ©', `${pluginName} → rechargement`); try { await this.reloadPlugin(pluginName); this.emit('dev:hotreload', pluginName, filePath); } catch (error) { this.log('error', 'Erreur hot reload', error.message); } } }); this.watchers.set('files', watcher); } // ============= CHARGEMENT AVANCÉ DES PLUGINS ============= async loadPlugin(plugin, config = {}) { let pluginName; try { let pluginModule; if (typeof plugin === 'string') { pluginName = plugin; if (this.loadingQueue.has(pluginName)) { throw new Error(`Plugin "${pluginName}" dĂ©jĂ  en cours de chargement`); } this.loadingQueue.add(pluginName); pluginModule = await this.resolvePlugin(plugin); } else { pluginModule = plugin; pluginName = plugin.name || 'anonymous'; this.loadingQueue.add(pluginName); } if (this.plugins.has(pluginName)) { this.log('warning', 'Plugin dĂ©jĂ  chargĂ©', pluginName); this.loadingQueue.delete(pluginName); return this; } // Validation avancĂ©e await this.validatePluginAdvanced(pluginModule, pluginName); const pluginInstance = { name: pluginName, version: pluginModule.version || '1.0.0', description: pluginModule.description || '', author: pluginModule.author || '', dependencies: pluginModule.dependencies || [], peerDependencies: pluginModule.peerDependencies || [], config: { ...pluginModule.defaultConfig, ...config }, module: pluginModule, loaded: false, active: false, loadTime: Date.now(), errorCount: 0, metrics: { loadTime: 0, executeCount: 0, errorCount: 0, lastError: null, performance: {} }, type: this.detectPluginType(pluginModule), priority: pluginModule.priority || 10, sandbox: this.options.enableSandbox ? this.createSandbox(pluginName) : null }; await this.checkDependenciesAdvanced(pluginInstance); await this.executePluginLoadWithTimeout(pluginInstance); this.plugins.set(pluginName, pluginInstance); this.loadOrder.push(pluginName); this.loadingQueue.delete(pluginName); this.log('success', 'Plugin chargĂ©', `${pluginName} v${pluginInstance.version}`); this.emit('plugin:loaded', pluginName, pluginInstance); await this.executeHook('plugin:load', pluginName, pluginInstance); return this; } catch (error) { if (pluginName) { this.loadingQueue.delete(pluginName); this.errorCount.set(pluginName, (this.errorCount.get(pluginName) || 0) + 1); } this.log('error', 'Erreur lors du chargement du plugin', error.message); this.emit('plugin:error', pluginName, error); throw error; } } async resolvePlugin(pluginName) { const pluginsPath = path.join(process.cwd(), this.options.pluginsDir); // Essayer diffĂ©rentes extensions et structures const possiblePaths = [ path.join(pluginsPath, `${pluginName}.js`), path.join(pluginsPath, `${pluginName}.ts`), path.join(pluginsPath, pluginName, 'index.js'), path.join(pluginsPath, pluginName, 'index.ts'), path.join(pluginsPath, pluginName, 'main.js'), path.join(pluginsPath, pluginName, 'main.ts'), path.join(pluginsPath, pluginName, 'plugin.js'), path.join(pluginsPath, pluginName, 'plugin.ts') ]; for (const pluginPath of possiblePaths) { if (fs.existsSync(pluginPath)) { if (pluginPath.endsWith('.ts')) { return await this.loadTypeScriptPlugin(pluginPath); } else { return require(pluginPath); } } } // Essayer depuis node_modules try { return require(pluginName); } catch (e) { throw new Error(`Plugin "${pluginName}" introuvable`); } } detectPluginType(pluginModule) { if (pluginModule.type) return pluginModule.type; // DĂ©tecter automatiquement le type if (pluginModule.middleware) return 'middleware'; if (pluginModule.routes) return 'router'; if (pluginModule.commands) return 'cli'; if (pluginModule.websocket) return 'websocket'; if (pluginModule.database) return 'database'; if (pluginModule.auth) return 'auth'; if (pluginModule.theme) return 'theme'; return 'generic'; } async validatePluginAdvanced(pluginModule, pluginName) { if (!this.options.enableValidation) return; // Validation de base if (!pluginModule || typeof pluginModule !== 'object') { throw new Error(`Plugin "${pluginName}" doit exporter un objet`); } if (!pluginModule.load || typeof pluginModule.load !== 'function') { throw new Error(`Plugin "${pluginName}" doit avoir une mĂ©thode load()`); } // Validation des mĂ©tadonnĂ©es const requiredFields = ['name', 'version']; const optionalFields = ['description', 'author', 'license', 'homepage', 'repository']; for (const field of requiredFields) { if (!pluginModule[field]) { this.log('warning', `Plugin ${pluginName}`, `Champ requis manquant: ${field}`); } } // Validation sĂ©mantique de version if (pluginModule.version && !this.isValidVersion(pluginModule.version)) { throw new Error(`Plugin "${pluginName}": version invalide "${pluginModule.version}"`); } // Validation des dĂ©pendances if (pluginModule.dependencies && !Array.isArray(pluginModule.dependencies)) { throw new Error(`Plugin "${pluginName}": dependencies doit ĂȘtre un tableau`); } // Validation du schĂ©ma de configuration if (pluginModule.configSchema) { this.schemas.set(pluginName, pluginModule.configSchema); } // Validation des hooks dĂ©clarĂ©s if (pluginModule.hooks && Array.isArray(pluginModule.hooks)) { for (const hookName of pluginModule.hooks) { if (!this.hooks.has(hookName)) { this.log('warning', `Plugin ${pluginName}`, `Hook inconnu: ${hookName}`); } } } // Validation des permissions if (pluginModule.permissions && Array.isArray(pluginModule.permissions)) { await this.validatePermissions(pluginName, pluginModule.permissions); } } async checkDependenciesAdvanced(plugin) { // VĂ©rifier les dĂ©pendances normales if (plugin.dependencies && plugin.dependencies.length > 0) { const missing = []; for (const dep of plugin.dependencies) { if (!this.plugins.has(dep)) { missing.push(dep); } } if (missing.length > 0) { throw new Error(`Plugin "${plugin.name}" nĂ©cessite: ${missing.join(', ')}`); } } // VĂ©rifier les peer dependencies if (plugin.peerDependencies && plugin.peerDependencies.length > 0) { const missingPeers = []; for (const peerDep of plugin.peerDependencies) { try { require.resolve(peerDep); } catch (error) { missingPeers.push(peerDep); } } if (missingPeers.length > 0) { this.log('warning', `Plugin ${plugin.name}`, `Peer dependencies manquantes: ${missingPeers.join(', ')}`); } } // VĂ©rifier les versions compatibles if (plugin.module.engines) { await this.checkEngineCompatibility(plugin.name, plugin.module.engines); } } // ============= FONCTIONS UTILITAIRES POUR DÉVELOPPEURS ============= /** * CrĂ©e un plugin de dĂ©veloppement rapide */ createDevPlugin(name, options = {}) { const plugin = { name, version: '1.0.0-dev', description: `Plugin de dĂ©veloppement: ${name}`, author: 'DĂ©veloppeur', type: 'dev', load: async (app, config, context) => { context.log('info', `Plugin de dev ${name} chargĂ©`); // Auto-setup des fonctionnalitĂ©s courantes if (options.routes) { Object.entries(options.routes).forEach(([path, handler]) => { context.addRoute('GET', path, handler); }); } if (options.middleware) { options.middleware.forEach(mw => context.addMiddleware(mw)); } if (options.hooks) { Object.entries(options.hooks).forEach(([hookName, callback]) => { context.hook(hookName, callback); }); } if (options.commands) { Object.entries(options.commands).forEach(([cmdName, cmd]) => { context.addCommand(cmdName, cmd.handler, cmd.description); }); } // ExĂ©cuter le code personnalisĂ© if (options.load && typeof options.load === 'function') { await options.load(app, config, context); } }, unload: options.unload, ...options }; return plugin; } /** * Plugin factory avec builder pattern */ createPluginBuilder() { return new PluginBuilder(); } /** * Injecte du code dans un plugin existant (dev uniquement) */ async injectCode(pluginName, code, type = 'before-load') { if (!this.options.devMode) { throw new Error('Injection de code disponible uniquement en mode dĂ©veloppement'); } const plugin = this.plugins.get(pluginName); if (!plugin) { throw new Error(`Plugin ${pluginName} non trouvĂ©`); } if (!plugin.injections) plugin.injections = []; plugin.injections.push({ code, type, timestamp: Date.now(), active: true }); this.log('debug', `Code injectĂ© dans ${pluginName}`, `Type: ${type}`); this.emit('dev:inject', pluginName, code, type); } /** * Monitore les performances d'un plugin */ profilePlugin(pluginName, duration = 60000) { const plugin = this.plugins.get(pluginName); if (!plugin) return null; const profiler = { start: Date.now(), end: Date.now() + duration, data: { hookCalls: 0, executionTime: 0, memoryUsage: process.memoryUsage(), errors: 0 } }; // Wrapper les mĂ©thodes du plugin pour collecter les mĂ©triques this.wrapPluginMethods(plugin, profiler); setTimeout(() => { this.log('info', `Profil de ${pluginName}`, JSON.stringify(profiler.data, null, 2)); this.emit('dev:profile', pluginName, profiler.data); }, duration); return profiler; } /** * DĂ©bogueur interactif pour plugin */ debugPlugin(pluginName) { const plugin = this.plugins.get(pluginName); if (!plugin) return null; const debugInterface = { inspect: () => this.devTools.get('debugger').inspect(plugin, `Plugin ${pluginName}`), config: () => console.log('Configuration:', plugin.config), metrics: () => console.log('MĂ©triques:', plugin.metrics), hooks: () => this.listPluginHooks(pluginName), reload: () => this.reloadPlugin(pluginName), toggle: () => this.togglePlugin(pluginName), breakpoint: (message) => this.devTools.get('debugger').breakpoint(`${pluginName}: ${message}`, plugin) }; global[`debug_${pluginName}`] = debugInterface; this.log('debug', `Interface de debug créée`, `Utilisez global.debug_${pluginName}`); return debugInterface; } /** * Teste un plugin avec diffĂ©rents scĂ©narios */ async testPlugin(pluginName, tests = {}) { const plugin = this.plugins.get(pluginName); if (!plugin) throw new Error(`Plugin ${pluginName} non trouvĂ©`); const results = { passed: 0, failed: 0, errors: [], details: {} }; // Tests par dĂ©faut const defaultTests = { 'load': () => plugin.loaded === true, 'active': () => plugin.active === true, 'config': () => plugin.config !== null, 'version': () => plugin.version && this.isValidVersion(plugin.version) }; const allTests = { ...defaultTests, ...tests }; for (const [testName, testFn] of Object.entries(allTests)) { try { const result = await testFn(plugin); if (result) { results.passed++; results.details[testName] = 'PASS'; } else { results.failed++; results.details[testName] = 'FAIL'; } } catch (error) { results.failed++; results.errors.push({ test: testName, error: error.message }); results.details[testName] = `ERROR: ${error.message}`; } } this.log('info', `Tests pour ${pluginName}`, `${results.passed} rĂ©ussis, ${results.failed} Ă©chouĂ©s`); return results; } /** * GĂ©nĂ©rateur de plugin depuis template */ async generatePlugin(name, template = 'basic', options = {}) { const templates = { basic: this.getBasicTemplate(), middleware: this.getMiddlewareTemplate(), api: this.getApiTemplate(), websocket: this.getWebSocketTemplate(), database: this.getDatabaseTemplate(), auth: this.getAuthTemplate() }; const pluginTemplate = templates[template]; if (!pluginTemplate) { throw new Error(`Template ${template} non trouvĂ©`); } const pluginCode = this.renderTemplate(pluginTemplate, { name, ...options }); const pluginPath = path.join(process.cwd(), this.options.pluginsDir, `${name}.js`); fs.writeFileSync(pluginPath, pluginCode); this.log('success', 'Plugin gĂ©nĂ©rĂ©', `${name} → ${pluginPath}`); return pluginPath; } /** * Backup et restore des plugins */ async backupPlugins(backupPath = null) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const defaultPath = path.join(process.cwd(), 'backups', `plugins-${timestamp}.json`); const outputPath = backupPath || defaultPath; const backup = { timestamp: Date.now(), plugins: Array.from(this.plugins.entries()).map(([name, plugin]) => ({ name, version: plugin.version, config: plugin.config, active: plugin.active, loadOrder: this.loadOrder.indexOf(name) })), loadOrder: this.loadOrder, options: this.options }; const dir = path.dirname(outputPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(outputPath, JSON.stringify(backup, null, 2)); this.log('success', 'Plugins sauvegardĂ©s', outputPath); return outputPath; } async restorePlugins(backupPath) { if (!fs.existsSync(backupPath)) { throw new Error(`Fichier de sauvegarde non trouvĂ©: ${backupPath}`); } const backup = JSON.parse(fs.readFileSync(backupPath, 'utf8')); // DĂ©charger tous les plugins actuels for (const pluginName of Array.from(this.plugins.keys())) { await this.unloadPlugin(pluginName); } // Charger les plugins dans l'ordre de la sauvegarde const sortedPlugins = backup.plugins.sort((a, b) => a.loadOrder - b.loadOrder); for (const pluginData of sortedPlugins) { try { await this.loadPlugin(pluginData.name, pluginData.config); if (!pluginData.active) { await this.togglePlugin(pluginData.name, false); } } catch (error) { this.log('error', `Erreur restauration ${pluginData.name}`, error.message); } } this.log('success', 'Plugins restaurĂ©s', `${sortedPlugins.length} plugins`); } // ============= TEMPLATES DE PLUGINS ============= getBasicTemplate() { return ` /** * Plugin {{name}} * GĂ©nĂ©rĂ© automatiquement par Veko.js PluginManager */ module.exports = { name: '{{name}}', version: '1.0.0', description: 'Description du plugin {{name}}', author: 'Votre nom', // Configuration par dĂ©faut defaultConfig: { enabled: true }, // MĂ©thode de chargement (obligatoire) async load(app, config, context) { context.log('info', 'Plugin {{name}} chargĂ© !'); // Votre code ici }, // MĂ©thode de dĂ©chargement (optionnelle) async unload(app, config) { console.log('Plugin {{name}} dĂ©chargĂ©'); }, // Activation/dĂ©sactivation (optionnelles) async activate(app, config) { console.log('Plugin {{name}} activĂ©'); }, async deactivate(app, config) { console.log('Plugin {{name}} dĂ©sactivĂ©'); } }; `; } getMiddlewareTemplate() { return ` module.exports = { name: '{{name}}', version: '1.0.0', description: 'Plugin middleware {{name}}', type: 'middleware', async load(app, config, context) { // Middleware personnalisĂ© const middleware = (req, res, next) => { context.log('info', 'Middleware {{name}} exĂ©cutĂ©'); // Votre logique ici next(); }; context.addMiddleware(middleware); context.log('success', 'Middleware {{name}} ajoutĂ©'); } }; `; } getApiTemplate() { return ` module.exports = { name: '{{name}}', version: '1.0.0', description: 'Plugin API {{name}}', type: 'api', async load(app, config, context) { // Routes API context.addRoute('GET', '/api/{{name}}', (req, res) => { res.json({ message: 'Hello from {{name}} API!' }); }); context.addRoute('POST', '/api/{{name}}', (req, res) => { res.json({ received: req.body }); }); context.log('success', 'API {{name}} configurĂ©e'); } }; `; } renderTemplate(template, variables) { let rendered = template; for (const [key, value] of Object.entries(variables)) { rendered = rendered.replace(new RegExp(`{{${key}}}`, 'g'), value); } return rendered; } // ============= FONCTIONS UTILITAIRES ============= isValidVersion(version) { return /^\d+\.\d+\.\d+(-[a-zA-Z0-9-]+)?$/.test(version); } getPluginNameFromFile(filePath) { const pluginsPath = path.join(process.cwd(), this.options.pluginsDir); const relativePath = path.relative(pluginsPath, filePath); const parts = relativePath.split(path.sep); if (parts[0].endsWith('.js') || parts[0].endsWith('.ts')) { return path.parse(parts[0]).name; } else { return parts[0]; } } listPluginHooks(pluginName) { const hooks = []; this.hooks.forEach((hookList, hookName) => { const pluginHooks = hookList.filter(h => h.plugin === pluginName); if (pluginHooks.length > 0) { hooks.push({ hook: hookName, count: pluginHooks.length }); } }); return hooks; } wrapPluginMethods(plugin, profiler) { // Wrapper pour mesurer les performances const originalLoad = plugin.module.load; plugin.module.load = async (...args) => { const start = process.hrtime.bigint(); try { const result = await originalLoad.call(plugin.module, ...args); profiler.data.executionTime += Number(process.hrtime.bigint() - start) / 1000000; return result; } catch (error) { profiler.data.errors++; throw error; } }; } collectMetrics() { for (const [name, plugin] of this.plugins.entries()) { const memUsage = process.memoryUsage(); const errorCount = this.errorCount.get(name) || 0; this.metrics.set(name, { uptime: Date.now() - plugin.loadTime, memoryUsage: memUsage, errorCount, lastCheck: Date.now(), health: errorCount === 0 ? 'healthy' : errorCount < 5 ? 'warning' : 'critical' }); } } // ============= INITIALISATION ============= init() { this.setupHooks(); if (this.options.autoLoad) { this.loadAllPlugins().catch(error => { this.log('error', 'Erreur lors du chargement automatique', error.message); }); } } setupHooks() { // Hooks prĂ©dĂ©finis de Veko.js const defaultHooks = [ 'app:init', 'app:start', 'app:stop', 'route:load', 'route:create', 'route:delete', 'request:start', 'request:end', 'error:handle', 'websocket:connect', 'websocket:disconnect', 'file:change', 'plugin:load', 'plugin:unload', 'plugin:error', 'plugin:timeout' ]; defaultHooks.forEach(hookName => { this.hooks.set(hookName, []); }); } // ============= GESTION DES PLUGINS ============= /** * Charge un plugin avec gestion d'erreurs amĂ©liorĂ©e * @param {string|Object} plugin - Nom du plugin ou objet plugin * @param {Object} config - Configuration du plugin */ async loadPlugin(plugin, config = {}) { let pluginName; try { let pluginModule; if (typeof plugin === 'string') { pluginName = plugin; // VĂ©rifier si dĂ©jĂ  en cours de chargement if (this.loadingQueue.has(pluginName)) { throw new Error(`Plugin "${pluginName}" dĂ©jĂ  en cours de chargement`); } this.loadingQueue.add(pluginName); // Essayer de charger depuis le dossier plugins const pluginPath = path.join(process.cwd(), this.options.pluginsDir, plugin); if (fs.existsSync(`${pluginPath}.js`)) { pluginModule = require(`${pluginPath}.js`); } else if (fs.existsSync(path.join(pluginPath, 'index.js'))) { pluginModule = require(path.join(pluginPath, 'index.js')); } else { // Essayer depuis node_modules try { pluginModule = require(plugin); } catch (e) { throw new Error(`Plugin "${plugin}" introuvable`); } } } else { pluginModule = plugin; pluginName = plugin.name || 'anonymous'; this.loadingQueue.add(pluginName); } // VĂ©rifier si le plugin est dĂ©jĂ  chargĂ© if (this.plugins.has(pluginName)) { this.log('warning', 'Plugin dĂ©jĂ  chargĂ©', pluginName); this.loadingQueue.delete(pluginName); return this; } // Valider la structure du plugin this.validatePlugin(pluginModule, pluginName); // CrĂ©er l'instance du plugin const pluginInstance = { name: pluginName, version: pluginModule.version || '1.0.0', description: pluginModule.description || '', author: pluginModule.author || '', dependencies: pluginModule.dependencies || [], config: { ...pluginModule.defaultConfig, ...config }, module: pluginModule, loaded: false, active: false, loadTime: Date.now(), errorCount: 0 }; // VĂ©rifier les dĂ©pendances await this.checkDependencies(pluginInstance); // Charger le plugin avec timeout await this.executePluginLoadWithTimeout(pluginInstance); // Enregistrer le plugin this.plugins.set(pluginName, pluginInstance); this.loadOrder.push(pluginName); this.loadingQueue.delete(pluginName); this.log('success', 'Plugin chargĂ©', `${pluginName} v${pluginInstance.version}`); this.emit('plugin:loaded', pluginName, pluginInstance); await this.executeHook('plugin:load', pluginName, pluginInstance); return this; } catch (error) { if (pluginName) { this.loadingQueue.delete(pluginName); this.errorCount.set(pluginName, (this.errorCount.get(pluginName) || 0) + 1); } this.log('error', 'Erreur lors du chargement du plugin', error.message); this.emit('plugin:error', pluginName, error); throw error; } } /** * DĂ©charge un plugin avec nettoyage complet * @param {string} pluginName - Nom du plugin */ async unloadPlugin(pluginName) { try { const plugin = this.plugins.get(pluginName); if (!plugin) { this.log('warning', 'Plugin introuvable', pluginName); return this; } // ExĂ©cuter la mĂ©thode unload si elle existe if (plugin.module.unload && typeof plugin.module.unload === 'function') { try { await Promise.race([ plugin.module.unload(this.app, plugin.config), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), this.options.timeout) ) ]); } catch (error) { this.log('warning', `Erreur lors du dĂ©chargement de ${pluginName}`, error.message); } } // Nettoyer les hooks du plugin this.cleanupPluginHooks(pluginName); // Nettoyer les middlewares du plugin this.cleanupPluginMiddleware(pluginName); // Nettoyer les routes du plugin this.cleanupPluginRoutes(pluginName); // Nettoyer les commandes du plugin this.cleanupPluginCommands(pluginName); // Nettoyer le cache require si possible try { const pluginModule = plugin.module; if (pluginModule && pluginModule.__filename) { delete require.cache[pluginModule.__filename]; } } catch (error) { // Ignore cache cleanup errors } // Supprimer de la liste this.plugins.delete(pluginName); this.loadOrder = this.loadOrder.filter(name => name !== pluginName); this.errorCount.delete(pluginName); this.log('success', 'Plugin dĂ©chargĂ©', pluginName); this.emit('plugin:unloaded', pluginName); await this.executeHook('plugin:unload', pluginName); return this; } catch (error) { this.log('error', 'Erreur lors du dĂ©chargement du plugin', error.message); throw error; } } /** * Recharge un plugin avec nouvelle configuration * @param {string} pluginName - Nom du plugin * @param {Object} newConfig - Nouvelle configuration */ async reloadPlugin(pluginName, newConfig = {}) { const plugin = this.plugins.get(pluginName); if (!plugin) { throw new Error(`Plugin "${pluginName}" introuvable`); } const config = { ...plugin.config, ...newConfig }; await this.unloadPlugin(pluginName); await this.loadPlugin(pluginName, config); this.log('success', 'Plugin rechargĂ©', pluginName); return this; } /** * Charge tous les plugins du dossier plugins avec gestion des dĂ©pendances */ async loadAllPlugins() { const pluginsPath = path.join(process.cwd(), this.options.pluginsDir); if (!fs.existsSync(pluginsPath)) { this.log('info', 'Dossier plugins créé', `📁 ${this.options.pluginsDir}`); fs.mkdirSync(pluginsPath, { recursive: true }); return this; } const files = fs.readdirSync(pluginsPath); const pluginFiles = files.filter(file => file.endsWith('.js') || file.endsWith('.ts') || (fs.statSync(path.join(pluginsPath, file)).isDirectory() && (fs.existsSync(path.join(pluginsPath, file, 'index.js')) || fs.existsSync(path.join(pluginsPath, file, 'index.ts')))) ); if (pluginFiles.length === 0) { this.log('info', 'Aucun plugin trouvĂ©', `📁 ${this.options.pluginsDir}`); return this; } this.log('info', 'Chargement des plugins...', `📩 ${pluginFiles.length} trouvĂ©s`); // Collect plugin information and dependencies const pluginsInfo = []; for (const file of pluginFiles) { try { const pluginName = file.replace(/\.(js|ts)$/, ''); const pluginModule = await this.resolvePlugin(pluginName); // Log dependencies for debugging this.log('info', `Analyse du plugin ${pluginName}`, `DĂ©pendances: ${JSON.stringify(pluginModule.dependencies || [])}`); pluginsInfo.push({ name: pluginName, dependencies: pluginModule.dependencies || [], module: pluginModule }); } catch (error) { this.log('warning', `ProblĂšme d'analyse`, `${file} → ${error.message}`); } } // Sort plugins by dependencies const loadOrder = this.sortPluginsByDependencies(pluginsInfo); // Log the calculated load order this.log('info', 'Ordre de chargement des plugins', loadOrder.join(' → ')); // Load plugins in dependency order with retry mechanism const results = { success: 0, failed: 0, errors: [] }; for (const pluginName of loadOrder) { let retries = 0; let loaded = false; while (retries < this.options.maxRetries && !loaded) { try { await this.loadPlugin(pluginName); loaded = true; results.success++; } catch (error) { retries++; results.errors.push({ plugin: pluginName, error: error.message, attempt: retries }); if (retries < this.options.maxRetries) { this.log('warning', `Tentative ${retries + 1}/${this.options.maxRetries}`, `${pluginName} → ${error.message}`); await new Promise(resolve => setTimeout(resolve, 1000 * retries)); } else { this.log('error', `Échec dĂ©finitif`, `${pluginName} → ${error.message}`); results.failed++; } } } } // Log final results this.log('success', 'Chargement terminĂ©', `✅ ${results.success} rĂ©ussis, ❌ ${results.failed} Ă©chouĂ©s`); if (results.errors.length > 0) { this.log('warning', 'Erreurs dĂ©taillĂ©es', results.errors.map(e => `${e.plugin}: ${e.error}`).join('; ')); } return this; } /** * Trie les plugins par ordre de dĂ©pendances */ sortPluginsByDependencies(pluginsInfo) { const loadOrder = []; const loaded = new Set(); const loading = new Set(); const loadPlugin = (plugin) => { // Skip if already loaded if (loaded.has(plugin.name)) return; // Detect circular dependencies if (loading.has(plugin.name)) { this.log('warning', 'DĂ©pendance circulaire dĂ©tectĂ©e', plugin.name); return; } loading.add(plugin.name); // Load dependencies first for (const depName of plugin.dependencies) { const depPlugin = pluginsInfo.find(p => p.name === depName); if (depPlugin) { loadPlugin(depPlugin); } else { this.log('warning', `DĂ©pendance manquante`, `${plugin.name} → ${depName}`); } } // Then load the plugin itself loadOrder.push(plugin.name); loaded.add(plugin.name); loading.delete(plugin.name); }; // Process all plugins for (const plugin of pluginsInfo) { loadPlugin(plugin); } return loadOrder; } // ============= VALIDATION ET DÉPENDANCES ============= validatePlugin(pluginModule, pluginName) { if (!pluginModule || typeof pluginModule !== 'object') { throw new Error(`Plugin "${pluginName}" doit exporter un objet`); } if (!pluginModule.load || typeof pluginModule.load !== 'function') { throw new Error(`Plugin "${pluginName}" doit avoir une mĂ©thode load()`); } // Validation des mĂ©tadonnĂ©es if (pluginModule.name && typeof pluginModule.name !== 'string') { throw new Error(`Plugin "${pluginName}": name doit ĂȘtre une chaĂźne`); } if (pluginModule.version && typeof pluginModule.version !== 'string') { throw new Error(`Plugin "${pluginName}": version doit ĂȘtre une chaĂźne`); } // Validation optionnelle des autres mĂ©thodes const optionalMethods = ['unload', 'activate', 'deactivate']; optionalMethods.forEach(method => { if (pluginModule[method] && typeof pluginModule[method] !== 'function') { throw new Error(`Plugin "${pluginName}": ${method} doit ĂȘtre une fonction`); } }); // Validation des dĂ©pendances if (pluginModule.dependencies && !Array.isArray(pluginModule.dependencies)) { throw new Error(`Plugin "${pluginName}": dependencies doit ĂȘtre un tableau`); } } async checkDependencies(plugin) { if (!plugin.dependencies || plugin.dependencies.length === 0) { return; } const missing = []; for (const dep of plugin.dependencies) { if (!this.plugins.has(dep)) { missing.push(dep); } } if (missing.length > 0) { throw new Error(`Plugin "${plugin.name}" nĂ©cessite: ${missing.join(', ')}`); } } async executePluginLoadWithTimeout(plugin) { const pluginContext = this.createPluginContext(plugin); try { // Charger le plugin avec timeout await Promise.race([ plugin.module.load(this.app, plugin.config, pluginContext), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout de chargement dĂ©passĂ©')), this.options.timeout) ) ]); plugin.loaded = true; plugin.active = true; } catch (error) { if (error.message.includes('Timeout')) { this.emit('plugin:timeout', plugin.name); } throw error; } } // ============= CONTEXTE ET API POUR PLUGINS ============= createPluginContext(plugin) { return { // AccĂšs au systĂšme de hooks hook: (hookName, callback, priority = 10) => this.addHook(hookName, callback, plugin.name, priority), removeHook: (hookName, callback) => this.removeHook(hookName, callback, plugin.name), // Ajout de middleware addMiddleware: (middleware) => this.addPluginMiddleware(middleware, plugin.name), // Ajout de routes addRoute: (method, path, handler) => this.addPluginRoute(method, path, handler, plugin.name), // Ajout de commandes CLI addCommand: (name, handler, description) => this.addPluginCommand(name, handler, description, plugin.name), // Logs avec nom du plugin log: (type, message, details = '') => this.log(type, `[${plugin.name}] ${message}`, details), // AccĂšs aux autres plugins getPlugin: (name) => this.getPlugin(name), listPlugins: () => this.listPlugins(), // Configuration getConfig: () => ({ ...plugin.config }), updateConfig: (newConfig) => this.updatePluginConfig(plugin.name, newConfig), // Stockage persistant pour le plugin storage: this.createPluginStorage(plugin.name), // Émission d'Ă©vĂ©nements emit: (eventName, ...args) => this.emit(`plugin:${plugin.name}:${eventName}`, ...args), // AccĂšs Ă  l'application app: this.app }; } createPluginStorage(pluginName) { const storageFile = path.join(process.cwd(), 'data', 'plugins', `${pluginName}.json`); return { get: (key, defaultValue = null) => { try { if (!fs.existsSync(storageFile)) return defaultValue; const data = JSON.parse(fs.readFileSync(storageFile, 'utf8')); return key ? (data[key] !== undefined ? data[key] : defaultValue) : data; } catch (error) { this.log('warning', `Erreur lecture storage pour ${pluginName}`, error.message); return defaultValue; } }, set: (key, value) => { try { const dir = path.dirname(storageFile); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } let data = {}; if (fs.existsSync(storageFile)) { try { data = JSON.parse(fs.readFileSync(storageFile, 'utf8')); } catch (error) { this.log('warning', `Fichier storage corrompu pour ${pluginName}`, 'rĂ©initialisation'); } } if (typeof key === 'object') { data = { ...data, ...key }; } else { data[key] = value; } fs.writeFileSync(storageFile, JSON.stringify(data, null, 2)); return true; } catch (error) { this.log('error', `Erreur Ă©criture storage pour ${pluginName}`, error.message); return false; } }, delete: (key) => { try { if (!fs.existsSync(storageFile)) return true; const data = JSON.parse(fs.readFileSync(storageFile, 'utf8')); delete data[key]; fs.writeFileSync(storageFile, JSON.stringify(data, null, 2)); return true; } catch (error) { this.log('error', `Erreur suppression storage pour ${pluginName}`, error.message); return false; } }, clear: () => { try { if (fs.existsSync(storageFile)) { fs.unlinkSync(storageFile); } return true; } catch (error) { this.log('error', `Erreur nettoyage storage pour ${pluginName}`, error.message); return false; } } }; } // ============= SYSTÈME DE HOOKS ============= /** * Ajoute un hook avec prioritĂ© * @param {string} hookName - Nom du hook * @param {Function} callback - Fonction Ă  exĂ©cuter * @param {string} pluginName - Nom du plugin * @param {number} priority - PrioritĂ© (plus Ă©levĂ© = exĂ©cutĂ© en premier) */ addHook(hookName, callback, pluginName = 'core', priority = 10) { if (!this.hooks.has(hookName)) { this.hooks.set(hookName, []); } if (typeof callback !== 'function') { throw new Error(`Hook callback doit ĂȘtre une fonction pour ${hookName}`); } this.hooks.get(hookName).push({ callback, plugin: pluginName, priority: Number(priority) || 10 }); // Trier par prioritĂ© (plus Ă©levĂ© en premier) this.hooks.get(hookName).sort((a, b) => b.priority - a.priority); } /** * Supprime un hook * @param {string} hookName - Nom du hook * @param {Function} callback - Fonction Ă  supprimer * @param {string} pluginName - Nom du plugin */ removeHook(hookName, callback, pluginName) { if (!this.hooks.has(hookName)) return; const hooks = this.hooks.get(hookName); this.hooks.set(hookName, hooks.filter(hook => !(hook.callback === callback && hook.plugin === pluginName) )); } /** * Execute un hook avec gestion d'erreur amĂ©liorĂ©e * @param {string} hookName - Nom du hook * @param {...any} args - Arguments Ă  passer aux callbacks */ async executeHook(hookName, ...args) { if (!this.hooks.has(hookName)) return args; const hooks = this.hooks.get(hookName); let result = args; for (const hook of hooks) { try { const hookResult = await Promise.race([ hook.callback(...result), new Promise((_, reject) => setTimeout(() => reject(new Error('Hook timeout')), 5000) ) ]); if (hookResult !== undefined) { result = Array.isArray(hookResult) ? hookResult : [hookResult]; } } catch (error) { this.log('error', `Erreur dans le hook ${hookName}`, `Plugin: ${hook.plugin} → ${error.message}`); this.emit('hook:error', hookName, hook.plugin, error); } } return result; } // ============= GESTION DES ÉLÉMENTS AJOUTÉS PAR LES PLUGINS ============= addPluginMiddleware(middleware, pluginName) { if (typeof middleware !== 'function') { throw new Error('Le middleware doit ĂȘtre une fonction'); } this.middleware.push({ middleware, plugin: pluginName }); if (this.app && this.app.use) { this.app.use(middleware); } } addPluginRoute(method, path, handler, pluginName) { if (!method || !path || !handler) { throw new Error('MĂ©thode, chemin et handler requis pour une route'); } const route = { method, path, handler, plugin: pluginName }; this.routes.push(route); if (this.app && this.app.createRoute) { this.app.createRoute(method, path, handler); } } addPluginCommand(name, handler, description, pluginName) { if (!name || !handler) { throw new Error('Nom et handler requis pour une commande'); } this.commands.set(name, { handler, description: description || '', plugin: pluginName }); } // ============= NETTOYAGE ============= cleanupPluginHooks(pluginName) { this.hooks.forEach((hooks, hookName) => { this.hooks.set(hookName, hooks.filter(hook => hook.plugin !== pluginName)); }); } cleanupPluginMiddleware(pluginName) { this.middleware = this.middleware.filter(item => item.plugin !== pluginName); } cleanupPluginRoutes(pluginName) { this.routes = this.routes.filter(route => route.plugin !== pluginName); } cleanupPluginCommands(pluginName) { for (const [name, command] of this.commands.entries()) { if (command.plugin === pluginName) { this.commands.delete(name); } } } // ============= UTILITAIRES ============= /** * Obtient un plugin * @param {string} pluginName - Nom du plugin */ getPlugin(pluginName) { return this.plugins.get(pluginName) || null; } /** * Liste tous les plugins */ listPlugins() { return Array.from(this.plugins.values()).map(plugin => ({ name: plugin.name, version: plugin.version, description: plugin.description, author: plugin.author, loaded: plugin.loaded, active: plugin.active, loadTime: plugin.loadTime, errorCount: plugin.errorCount })); } /** * Met Ă  jour la configuration d'un plugin */ updatePluginConfig(pluginName, newConfig) { const plugin = this.plugins.get(pluginName); if (plugin) { plugin.config = { ...plugin.config, ...newConfig }; return true; } return false; } /** * Active/dĂ©sactive un plug