组件发现和动态加载系统
This commit is contained in:
102
packages/editor-core/src/Services/ComponentDiscoveryService.ts
Normal file
102
packages/editor-core/src/Services/ComponentDiscoveryService.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import type { IService } from '@esengine/ecs-framework';
|
||||
import { Injectable } from '@esengine/ecs-framework';
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
import { MessageHub } from './MessageHub';
|
||||
|
||||
const logger = createLogger('ComponentDiscoveryService');
|
||||
|
||||
export interface ComponentFileInfo {
|
||||
path: string;
|
||||
fileName: string;
|
||||
className: string | null;
|
||||
}
|
||||
|
||||
export interface ComponentScanOptions {
|
||||
basePath: string;
|
||||
pattern: string;
|
||||
scanFunction: (path: string, pattern: string) => Promise<string[]>;
|
||||
readFunction: (path: string) => Promise<string>;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ComponentDiscoveryService implements IService {
|
||||
private discoveredComponents: Map<string, ComponentFileInfo> = new Map();
|
||||
private messageHub: MessageHub;
|
||||
|
||||
constructor(messageHub: MessageHub) {
|
||||
this.messageHub = messageHub;
|
||||
}
|
||||
|
||||
public async scanComponents(options: ComponentScanOptions): Promise<ComponentFileInfo[]> {
|
||||
try {
|
||||
logger.info('Scanning for components', {
|
||||
basePath: options.basePath,
|
||||
pattern: options.pattern
|
||||
});
|
||||
|
||||
const files = await options.scanFunction(options.basePath, options.pattern);
|
||||
logger.info(`Found ${files.length} component files`);
|
||||
|
||||
const componentInfos: ComponentFileInfo[] = [];
|
||||
|
||||
for (const filePath of files) {
|
||||
try {
|
||||
const fileContent = await options.readFunction(filePath);
|
||||
const componentInfo = this.parseComponentFile(filePath, fileContent);
|
||||
|
||||
if (componentInfo) {
|
||||
componentInfos.push(componentInfo);
|
||||
this.discoveredComponents.set(filePath, componentInfo);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to parse component file: ${filePath}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
await this.messageHub.publish('components:discovered', {
|
||||
count: componentInfos.length,
|
||||
components: componentInfos
|
||||
});
|
||||
|
||||
logger.info(`Successfully parsed ${componentInfos.length} components`);
|
||||
return componentInfos;
|
||||
} catch (error) {
|
||||
logger.error('Failed to scan components', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public getDiscoveredComponents(): ComponentFileInfo[] {
|
||||
return Array.from(this.discoveredComponents.values());
|
||||
}
|
||||
|
||||
public clearDiscoveredComponents(): void {
|
||||
this.discoveredComponents.clear();
|
||||
logger.info('Cleared discovered components');
|
||||
}
|
||||
|
||||
private parseComponentFile(filePath: string, content: string): ComponentFileInfo | null {
|
||||
const fileName = filePath.split(/[\\/]/).pop() || '';
|
||||
|
||||
const classMatch = content.match(/export\s+class\s+(\w+)\s+extends\s+Component/);
|
||||
|
||||
if (classMatch) {
|
||||
const className = classMatch[1];
|
||||
logger.debug(`Found component class: ${className} in ${fileName}`);
|
||||
|
||||
return {
|
||||
path: filePath,
|
||||
fileName,
|
||||
className
|
||||
};
|
||||
}
|
||||
|
||||
logger.debug(`No valid component class found in ${fileName}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.discoveredComponents.clear();
|
||||
logger.info('ComponentDiscoveryService disposed');
|
||||
}
|
||||
}
|
||||
141
packages/editor-core/src/Services/ComponentLoaderService.ts
Normal file
141
packages/editor-core/src/Services/ComponentLoaderService.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import type { IService } from '@esengine/ecs-framework';
|
||||
import { Injectable, Component } from '@esengine/ecs-framework';
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
import { MessageHub } from './MessageHub';
|
||||
import type { ComponentFileInfo } from './ComponentDiscoveryService';
|
||||
import { ComponentRegistry } from './ComponentRegistry';
|
||||
|
||||
const logger = createLogger('ComponentLoaderService');
|
||||
|
||||
export interface LoadedComponentInfo {
|
||||
fileInfo: ComponentFileInfo;
|
||||
componentClass: typeof Component;
|
||||
loadedAt: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ComponentLoaderService implements IService {
|
||||
private loadedComponents: Map<string, LoadedComponentInfo> = new Map();
|
||||
private messageHub: MessageHub;
|
||||
private componentRegistry: ComponentRegistry;
|
||||
|
||||
constructor(messageHub: MessageHub, componentRegistry: ComponentRegistry) {
|
||||
this.messageHub = messageHub;
|
||||
this.componentRegistry = componentRegistry;
|
||||
}
|
||||
|
||||
public async loadComponents(
|
||||
componentInfos: ComponentFileInfo[],
|
||||
modulePathTransform?: (filePath: string) => string
|
||||
): Promise<LoadedComponentInfo[]> {
|
||||
logger.info(`Loading ${componentInfos.length} components`);
|
||||
|
||||
const loadedComponents: LoadedComponentInfo[] = [];
|
||||
|
||||
for (const componentInfo of componentInfos) {
|
||||
try {
|
||||
const loadedComponent = await this.loadComponent(componentInfo, modulePathTransform);
|
||||
if (loadedComponent) {
|
||||
loadedComponents.push(loadedComponent);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to load component: ${componentInfo.fileName}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
await this.messageHub.publish('components:loaded', {
|
||||
count: loadedComponents.length,
|
||||
components: loadedComponents
|
||||
});
|
||||
|
||||
logger.info(`Successfully loaded ${loadedComponents.length} components`);
|
||||
return loadedComponents;
|
||||
}
|
||||
|
||||
public async loadComponent(
|
||||
componentInfo: ComponentFileInfo,
|
||||
modulePathTransform?: (filePath: string) => string
|
||||
): Promise<LoadedComponentInfo | null> {
|
||||
try {
|
||||
const modulePath = modulePathTransform
|
||||
? modulePathTransform(componentInfo.path)
|
||||
: this.convertToModulePath(componentInfo.path);
|
||||
|
||||
logger.debug(`Loading component from: ${modulePath}`);
|
||||
|
||||
const module = await import(/* @vite-ignore */ modulePath);
|
||||
|
||||
if (!componentInfo.className) {
|
||||
logger.warn(`No class name found for component: ${componentInfo.fileName}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const componentClass = module[componentInfo.className];
|
||||
|
||||
if (!componentClass || !(componentClass.prototype instanceof Component)) {
|
||||
logger.error(`Invalid component class: ${componentInfo.className}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
this.componentRegistry.register({
|
||||
name: componentInfo.className,
|
||||
type: componentClass
|
||||
});
|
||||
|
||||
const loadedInfo: LoadedComponentInfo = {
|
||||
fileInfo: componentInfo,
|
||||
componentClass,
|
||||
loadedAt: Date.now()
|
||||
};
|
||||
|
||||
this.loadedComponents.set(componentInfo.path, loadedInfo);
|
||||
|
||||
logger.info(`Component loaded and registered: ${componentInfo.className}`);
|
||||
|
||||
return loadedInfo;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to load component: ${componentInfo.fileName}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public getLoadedComponents(): LoadedComponentInfo[] {
|
||||
return Array.from(this.loadedComponents.values());
|
||||
}
|
||||
|
||||
public unloadComponent(filePath: string): boolean {
|
||||
const loadedComponent = this.loadedComponents.get(filePath);
|
||||
|
||||
if (!loadedComponent || !loadedComponent.fileInfo.className) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.componentRegistry.unregister(loadedComponent.fileInfo.className);
|
||||
this.loadedComponents.delete(filePath);
|
||||
|
||||
logger.info(`Component unloaded: ${loadedComponent.fileInfo.className}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
public clearLoadedComponents(): void {
|
||||
for (const [filePath] of this.loadedComponents) {
|
||||
this.unloadComponent(filePath);
|
||||
}
|
||||
logger.info('Cleared all loaded components');
|
||||
}
|
||||
|
||||
private convertToModulePath(filePath: string): string {
|
||||
const normalizedPath = filePath.replace(/\\/g, '/');
|
||||
|
||||
if (normalizedPath.startsWith('http://') || normalizedPath.startsWith('https://')) {
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
return `file:///${normalizedPath}`;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.clearLoadedComponents();
|
||||
logger.info('ComponentLoaderService disposed');
|
||||
}
|
||||
}
|
||||
@@ -15,5 +15,7 @@ export * from './Services/ComponentRegistry';
|
||||
export * from './Services/LocaleService';
|
||||
export * from './Services/PropertyMetadata';
|
||||
export * from './Services/ProjectService';
|
||||
export * from './Services/ComponentDiscoveryService';
|
||||
export * from './Services/ComponentLoaderService';
|
||||
|
||||
export * from './Types/UITypes';
|
||||
|
||||
Reference in New Issue
Block a user