import { Plugin } from 'vite'; import { readdir, readFile } from 'fs/promises'; import { join, resolve } from 'path'; interface ListenerInfo { eventName: string; importPath: string; fileName: string; } export function AutoRegistryEventsPlugin(): Plugin { return { name: 'AutoRegistryEventsPlugin', // Add configResolved hook to run earlier configResolved(config) { console.log('🔧 Config resolved, mode:', config.command); }, // Add buildStart hook async buildStart() { console.log('🚀 BuildStart hook triggered'); await generateRegistryFile(); }, // Add configureServer hook for dev mode configureServer(server) { console.log('🔧 Configure server hook triggered'); // Generate registry file immediately for dev mode generateRegistryFile().catch(console.error); }, // Add resolveId hook to handle the import resolveId(id, importer) { if (id === './auto-listeners-registry' && importer?.includes('src/index.ts')) { console.log('🔍 Resolving auto-listeners-registry import'); const registryPath = resolve(process.cwd(), 'src/auto-listeners-registry.ts'); return registryPath; } } }; } async function generateRegistryFile() { console.log('🔍 Discovering event listeners...'); // Generate the auto-registry file during build const listenersDir = resolve(process.cwd(), 'src/listeners'); const listeners = await discoverListeners(listenersDir); console.log(`📦 Found ${listeners.length} event listeners`); // Generate the auto-registry file const registryCode = generateAutoRegistry(listeners); const registryPath = resolve(process.cwd(), 'src/auto-listeners-registry.ts'); // Write the auto-registry file const fs = await import('fs/promises'); await fs.writeFile(registryPath, registryCode); console.log('✅ Auto-registry file generated at:', registryPath); } async function discoverListeners(listenersDir: string): Promise { const listeners: ListenerInfo[] = []; try { const files = await readdir(listenersDir); const tsFiles = files.filter(file => file.endsWith('.ts') && !file.endsWith('.d.ts')); for (const file of tsFiles) { const filePath = join(listenersDir, file); const content = await readFile(filePath, 'utf-8'); // Check if it's a function-based listener const eventNameMatch = content.match(/export const eventName = (EcommerceEvents\.\w+);/); const hasDefaultExport = content.includes('export default (payload:'); if (eventNameMatch && hasDefaultExport) { // Check if the default export function has meaningful logic if (hasActualLogic(content)) { const eventName = eventNameMatch[1]; const importPath = `./listeners/${file.replace('.ts', '')}`; const fileName = file.replace('.ts', ''); listeners.push({ eventName, importPath, fileName }); } else { console.log(`⏭️ Skipping listener ${file} - no meaningful logic detected`); } } } } catch (error) { console.warn('Warning: Could not discover listeners:', error); } return listeners; } /** * Check if the listener function contains meaningful logic beyond console.log and placeholder comments */ function hasActualLogic(content: string): boolean { // Extract the default export function body const defaultExportMatch = content.match(/export default \(payload:[^)]+\):[^{]*{([^}]*)}/s); if (!defaultExportMatch) { return false; } const functionBody = defaultExportMatch[1]; // Remove comments and whitespace for analysis const cleanBody = functionBody .replace(/\/\/.*$/gm, '') // Remove single-line comments .replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments .replace(/\s+/g, ' ') // Normalize whitespace .trim(); // Check if the function body is empty after removing comments if (!cleanBody) { return false; } // Check if the only content is console.log statements const consoleLogRegex = /console\.(log|warn|error|info|debug)\s*\([^)]*\)\s*;?/g; const bodyWithoutConsoleLogs = cleanBody.replace(consoleLogRegex, '').trim(); // If nothing remains after removing console.log statements, it's not meaningful logic if (!bodyWithoutConsoleLogs) { return false; } // Additional check for common placeholder patterns const placeholderPatterns = [ /add\s+your\s+custom\s+tracking\s+logic\s+here/i, /todo/i, /implement\s+your\s+logic/i, /your\s+code\s+here/i ]; // If the remaining content only contains placeholder text, it's not meaningful const hasOnlyPlaceholders = placeholderPatterns.some(pattern => pattern.test(bodyWithoutConsoleLogs) && bodyWithoutConsoleLogs.replace(pattern, '').trim() === '' ); return !hasOnlyPlaceholders; } function generateAutoRegistry(listeners: ListenerInfo[]): string { const toCamelCase = (str: string) => str.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); const imports = listeners.map(listener => { const camelCaseName = toCamelCase(listener.fileName); return `import ${camelCaseName}Handler, { eventName as ${camelCaseName}EventName } from '${listener.importPath}';`; }).join('\n'); const registrations = listeners.map(listener => { const camelCaseName = toCamelCase(listener.fileName); return `listeners.set(${camelCaseName}EventName, ${camelCaseName}Handler);`; }).join('\n'); return `// Auto-generated listeners registry - DO NOT EDIT MANUALLY // This file is generated by the Vite listeners plugin ${imports} const listeners: Map void> = new Map(); ${registrations} export { listeners }; `; }