UNPKG

@dankupfer/create-dn-starter

Version:

Interactive CLI for creating modular React Native apps with Expo

354 lines (294 loc) 10.6 kB
// src/config/moduleLoader.ts - Corrected hybrid approach import { ModuleConfig, ModuleState, LoadedModule, ModuleMetadata, getModuleById, getEnabledModules, getLoadOrder, getModuleImportFn } from './modules'; // Display metadata interface for UI interface ModuleDisplayMetadata { id: string; name: string; version: string; category: 'core' | 'feature'; features: string[]; state: ModuleState; component: any; api?: any; loadTime?: number; error?: Error; } class ModuleLoader { private static instance: ModuleLoader; private modules: Map<string, LoadedModule> = new Map(); private loadingPromises: Map<string, Promise<LoadedModule>> = new Map(); private initialized = false; private constructor() { console.log('ModuleLoader: Initialized'); } static getInstance(): ModuleLoader { if (!ModuleLoader.instance) { ModuleLoader.instance = new ModuleLoader(); } return ModuleLoader.instance; } // Allow modules to register their metadata registerModuleMetadata(moduleId: string, metadata: ModuleMetadata): void { const module = this.modules.get(moduleId); if (module) { module.metadata = metadata; console.log(`ModuleLoader: Metadata registered for ${moduleId}`); } else { console.warn(`ModuleLoader: Attempted to register metadata for non-existent module: ${moduleId}`); } } // Initialize and load all enabled modules async initialize(): Promise<void> { if (this.initialized) { console.log('ModuleLoader: Already initialized'); return; } try { console.log('ModuleLoader: Starting initialization...'); const enabledModules = getEnabledModules(); const moduleIds = enabledModules.map(m => m.id); const loadOrder = getLoadOrder(moduleIds); console.log('ModuleLoader: Load order:', loadOrder); // Load all modules in order for (const moduleId of loadOrder) { try { await this.loadModule(moduleId); } catch (error) { console.error(`ModuleLoader: Failed to load module ${moduleId}:`, error); // Continue loading other modules even if one fails } } this.initialized = true; console.log('ModuleLoader: Initialization complete'); // Log final status this.logStatus(); } catch (error) { console.error('ModuleLoader: Initialization failed', error); throw error; } } // Load a single module async loadModule(moduleId: string): Promise<LoadedModule> { // Check if already loaded const existing = this.modules.get(moduleId); if (existing?.state === ModuleState.LOADED) { return existing; } // Check if already loading const loadingPromise = this.loadingPromises.get(moduleId); if (loadingPromise) { return loadingPromise; } const config = getModuleById(moduleId); if (!config) { throw new Error(`Module config not found: ${moduleId}`); } // Create loading promise const promise = this.doLoadModule(config); this.loadingPromises.set(moduleId, promise); try { const result = await promise; return result; } finally { this.loadingPromises.delete(moduleId); } } private async doLoadModule(config: ModuleConfig): Promise<LoadedModule> { console.log(`ModuleLoader: Loading module ${config.id}...`); const module: LoadedModule = { config, state: ModuleState.LOADING }; this.modules.set(config.id, module); try { const startTime = Date.now(); // Load dependencies first if (config.dependencies?.length) { console.log(`ModuleLoader: Loading dependencies for ${config.id}:`, config.dependencies); for (const depId of config.dependencies) { const dep = await this.loadModule(depId); if (dep.state !== ModuleState.LOADED) { throw new Error(`Dependency ${depId} failed to load`); } } } // Get and execute the import function const importFn = getModuleImportFn(config.id); if (!importFn) { throw new Error(`Import function not found for module: ${config.id}`); } const imported = await importFn(); // Check for component const component = imported.default || imported; if (!component) { throw new Error(`Module ${config.id} must export a component`); } // Check for metadata (optional) if (imported.metadata) { module.metadata = imported.metadata; } module.component = component; module.state = ModuleState.LOADED; module.loadTime = Date.now() - startTime; console.log(`ModuleLoader: Module ${config.id} loaded successfully (${module.loadTime}ms)`); // If module has a register function, call it if (imported.register && typeof imported.register === 'function') { try { imported.register(this); console.log(`ModuleLoader: Module ${config.id} registered successfully`); } catch (error) { console.warn(`ModuleLoader: Failed to register module ${config.id}:`, error); } } return module; } catch (error) { console.error(`ModuleLoader: Failed to load ${config.id}:`, error); module.state = ModuleState.ERROR; module.error = error instanceof Error ? error : new Error(String(error)); throw error; } } // Get display metadata (combines config and module-provided metadata) getModuleDisplayMetadata(moduleId: string): ModuleDisplayMetadata | null { const module = this.modules.get(moduleId); if (!module) return null; const config = module.config; const metadata = module.metadata || {}; return { id: config.id, name: metadata.name || config.name, version: metadata.version || config.version || 'unknown', category: config.category, features: metadata.features || [], state: module.state, component: module.component, api: metadata.api, loadTime: module.loadTime, error: module.error }; } // Get all modules metadata for UI display getAllModulesDisplayMetadata(): ModuleDisplayMetadata[] { return Array.from(this.modules.values()) .map(module => this.getModuleDisplayMetadata(module.config.id)) .filter((item): item is ModuleDisplayMetadata => item !== null); } // Get a loaded module getModule(moduleId: string): LoadedModule | undefined { return this.modules.get(moduleId); } // Get all loaded modules getAllModules(): LoadedModule[] { return Array.from(this.modules.values()); } // Get modules by state getModulesByState(state: ModuleState): LoadedModule[] { return Array.from(this.modules.values()).filter(m => m.state === state); } // Get module component getModuleComponent(moduleId: string): any { const module = this.modules.get(moduleId); return module?.state === ModuleState.LOADED ? module.component : null; } // Check if module is loaded isModuleLoaded(moduleId: string): boolean { const module = this.modules.get(moduleId); return module?.state === ModuleState.LOADED; } // Force reload a module async reloadModule(moduleId: string): Promise<LoadedModule> { console.log(`ModuleLoader: Reloading module ${moduleId}...`); // Clear the module state const existing = this.modules.get(moduleId); if (existing) { existing.state = ModuleState.UNLOADED; existing.component = undefined; existing.error = undefined; } return this.loadModule(moduleId); } // Get loading status summary getLoadingStatus() { const modules = Array.from(this.modules.values()); return { total: modules.length, loaded: modules.filter(m => m.state === ModuleState.LOADED).length, loading: modules.filter(m => m.state === ModuleState.LOADING).length, error: modules.filter(m => m.state === ModuleState.ERROR).length, pending: modules.filter(m => m.state === ModuleState.PENDING).length, unloaded: modules.filter(m => m.state === ModuleState.UNLOADED).length }; } // Get loaded modules (backward compatibility) getLoadedModules(): string[] { if (!this.initialized) { console.warn('ModuleLoader: Attempting to get loaded modules before initialization. Returning empty array.'); return []; } return this.getModulesByState(ModuleState.LOADED).map(m => m.config.id); } // Backward compatibility alias for initialize async loadModules(): Promise<string[]> { await this.initialize(); return this.getLoadedModules(); } // Get API for a specific module getModuleAPI(moduleId: string): any | undefined { const displayMeta = this.getModuleDisplayMetadata(moduleId); return displayMeta?.api; } // Get metadata for all loaded modules (backward compatibility) getLoadedModulesMetadata(): ModuleDisplayMetadata[] { return this.getLoadedModules() .map(id => this.getModuleDisplayMetadata(id)) .filter((item): item is ModuleDisplayMetadata => item !== null); } // Log detailed status for debugging logStatus(): void { console.log('\n=== ModuleLoader Status ==='); const status = this.getLoadingStatus(); console.log('Overall Status:', status); const allModules = this.getAllModulesDisplayMetadata(); // Group by state with proper typing type ModulesByState = Record<ModuleState, ModuleDisplayMetadata[]>; const byState: ModulesByState = { [ModuleState.LOADED]: [], [ModuleState.LOADING]: [], [ModuleState.ERROR]: [], [ModuleState.PENDING]: [], [ModuleState.UNLOADED]: [] }; allModules.forEach(module => { byState[module.state].push(module); }); // Log each state group Object.keys(byState).forEach(stateKey => { const state = stateKey as ModuleState; const modules = byState[state]; if (modules.length > 0) { console.log(`\n${state.toUpperCase()} (${modules.length}):`); modules.forEach(m => { let line = ` - ${m.name} (${m.id}) v${m.version}`; if (m.loadTime) line += ` - ${m.loadTime}ms`; if (m.error) line += ` - Error: ${m.error.message}`; console.log(line); }); } }); console.log('\n==========================\n'); } } // Export singleton instance export const moduleLoader = ModuleLoader.getInstance(); // Export for type safety and for direct class access if needed export { ModuleLoader }; export type { LoadedModule, ModuleDisplayMetadata };