@salla.sa/ecommerce-events-base
Version:
Base types and utilities for Salla ecommerce event tracking
176 lines (140 loc) • 5.78 kB
text/typescript
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<ListenerInfo[]> {
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<string, (payload: any) => void> = new Map();
${registrations}
export { listeners };
`;
}