@dankupfer/create-dn-starter
Version:
Interactive CLI for creating modular React Native apps with Expo
354 lines (294 loc) • 10.6 kB
text/typescript
// 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 };