* refactor: reorganize package structure and decouple framework packages ## Package Structure Reorganization - Reorganized 55 packages into categorized subdirectories: - packages/framework/ - Generic framework (Laya/Cocos compatible) - packages/engine/ - ESEngine core modules - packages/rendering/ - Rendering modules (WASM dependent) - packages/physics/ - Physics modules - packages/streaming/ - World streaming - packages/network-ext/ - Network extensions - packages/editor/ - Editor framework and plugins - packages/rust/ - Rust WASM engine - packages/tools/ - Build tools and SDK ## Framework Package Decoupling - Decoupled behavior-tree and blueprint packages from ESEngine dependencies - Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent) - ESEngine-specific code moved to esengine/ subpath exports - Framework packages now usable with Cocos/Laya without ESEngine ## CI Configuration - Updated CI to only type-check and lint framework packages - Added type-check:framework and lint:framework scripts ## Breaking Changes - Package import paths changed due to directory reorganization - ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine') * fix: update es-engine file path after directory reorganization * docs: update README to focus on framework over engine * ci: only build framework packages, remove Rust/WASM dependencies * fix: remove esengine subpath from behavior-tree and blueprint builds ESEngine integration code will only be available in full engine builds. Framework packages are now purely engine-agnostic. * fix: move network-protocols to framework, build both in CI * fix: update workflow paths from packages/core to packages/framework/core * fix: exclude esengine folder from type-check in behavior-tree and blueprint * fix: update network tsconfig references to new paths * fix: add test:ci:framework to only test framework packages in CI * fix: only build core and math npm packages in CI * fix: exclude test files from CodeQL and fix string escaping security issue
416 lines
11 KiB
TypeScript
416 lines
11 KiB
TypeScript
/**
|
||
* @zh 插件加载器
|
||
* @en Plugin Loader
|
||
*
|
||
* @zh 提供统一的插件加载机制,支持:
|
||
* - 动态 ESM 导入
|
||
* - 依赖拓扑排序
|
||
* - 加载状态追踪
|
||
* - 错误隔离
|
||
*
|
||
* @en Provides unified plugin loading with:
|
||
* - Dynamic ESM imports
|
||
* - Dependency topological sorting
|
||
* - Load state tracking
|
||
* - Error isolation
|
||
*/
|
||
|
||
import { createLogger } from '@esengine/ecs-framework';
|
||
import type { IRuntimePlugin, ModuleManifest } from './PluginManager';
|
||
import { runtimePluginManager } from './PluginManager';
|
||
import {
|
||
topologicalSort,
|
||
validateDependencies as validateDeps,
|
||
resolveDependencyId,
|
||
type IDependable
|
||
} from './utils/DependencyUtils';
|
||
|
||
const logger = createLogger('PluginLoader');
|
||
|
||
// ============================================================================
|
||
// 类型定义 | Types
|
||
// ============================================================================
|
||
|
||
/**
|
||
* @zh 插件加载状态
|
||
* @en Plugin load state
|
||
*/
|
||
export type PluginLoadState =
|
||
| 'pending' // 等待加载
|
||
| 'loading' // 加载中
|
||
| 'loaded' // 已加载
|
||
| 'failed' // 加载失败
|
||
| 'missing'; // 依赖缺失
|
||
|
||
/**
|
||
* @zh 插件源类型
|
||
* @en Plugin source type
|
||
*/
|
||
export type PluginSourceType = 'npm' | 'local' | 'static';
|
||
|
||
/**
|
||
* @zh 插件包信息
|
||
* @en Plugin package info
|
||
*/
|
||
export interface PluginPackageInfo {
|
||
plugin: boolean;
|
||
pluginExport: string;
|
||
category?: string;
|
||
isEnginePlugin?: boolean;
|
||
dependencies?: string[];
|
||
}
|
||
|
||
/**
|
||
* @zh 插件配置
|
||
* @en Plugin configuration
|
||
*/
|
||
export interface PluginConfig {
|
||
enabled: boolean;
|
||
options?: Record<string, unknown>;
|
||
}
|
||
|
||
/**
|
||
* @zh 项目插件配置
|
||
* @en Project plugin configuration
|
||
*/
|
||
export interface ProjectPluginConfig {
|
||
plugins: Record<string, PluginConfig>;
|
||
}
|
||
|
||
/**
|
||
* @zh 插件加载配置
|
||
* @en Plugin load configuration
|
||
*/
|
||
export interface PluginLoadConfig {
|
||
packageId: string;
|
||
enabled: boolean;
|
||
sourceType: PluginSourceType;
|
||
exportName?: string;
|
||
dependencies?: string[];
|
||
options?: Record<string, unknown>;
|
||
localPath?: string;
|
||
}
|
||
|
||
/**
|
||
* @zh 插件加载信息
|
||
* @en Plugin load info
|
||
*/
|
||
export interface PluginLoadInfo {
|
||
packageId: string;
|
||
state: PluginLoadState;
|
||
plugin?: IRuntimePlugin;
|
||
error?: string;
|
||
missingDeps?: string[];
|
||
loadTime?: number;
|
||
}
|
||
|
||
/**
|
||
* @zh 加载器配置
|
||
* @en Loader configuration
|
||
*/
|
||
export interface PluginLoaderConfig {
|
||
plugins: PluginLoadConfig[];
|
||
timeout?: number;
|
||
continueOnFailure?: boolean;
|
||
localLoader?: (path: string) => Promise<string>;
|
||
localExecutor?: (code: string, id: string) => Promise<IRuntimePlugin | null>;
|
||
}
|
||
|
||
// ============================================================================
|
||
// 插件加载器 | Plugin Loader
|
||
// ============================================================================
|
||
|
||
/**
|
||
* @zh 插件加载器
|
||
* @en Plugin Loader
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const loader = new PluginLoader({
|
||
* plugins: [
|
||
* { packageId: '@esengine/sprite', enabled: true, sourceType: 'npm' }
|
||
* ]
|
||
* });
|
||
* await loader.loadAll();
|
||
* ```
|
||
*/
|
||
export class PluginLoader {
|
||
private _config: Required<PluginLoaderConfig>;
|
||
private _loaded = new Map<string, PluginLoadInfo>();
|
||
private _loading = false;
|
||
|
||
constructor(config: PluginLoaderConfig) {
|
||
this._config = {
|
||
plugins: config.plugins,
|
||
timeout: config.timeout ?? 30000,
|
||
continueOnFailure: config.continueOnFailure ?? true,
|
||
localLoader: config.localLoader ?? (async () => ''),
|
||
localExecutor: config.localExecutor ?? (async () => null)
|
||
};
|
||
}
|
||
|
||
/**
|
||
* @zh 加载所有启用的插件
|
||
* @en Load all enabled plugins
|
||
*/
|
||
async loadAll(): Promise<Map<string, PluginLoadInfo>> {
|
||
if (this._loading) {
|
||
throw new Error('Loading already in progress');
|
||
}
|
||
|
||
this._loading = true;
|
||
const start = Date.now();
|
||
|
||
try {
|
||
const enabled = this._config.plugins.filter(p => p.enabled);
|
||
|
||
// 验证依赖
|
||
const missing = this._validateDependencies(enabled);
|
||
for (const [id, deps] of missing) {
|
||
this._loaded.set(id, {
|
||
packageId: id,
|
||
state: 'missing',
|
||
missingDeps: deps,
|
||
error: `Missing: ${deps.join(', ')}`
|
||
});
|
||
}
|
||
|
||
// 过滤有效插件并排序
|
||
const valid = enabled.filter(p => !missing.has(p.packageId));
|
||
const sorted = this._sortByDependencies(valid);
|
||
|
||
// 串行加载
|
||
for (const plugin of sorted) {
|
||
await this._loadOne(plugin);
|
||
}
|
||
|
||
const time = Date.now() - start;
|
||
const loadedCount = this.getLoaded().length;
|
||
logger.info(`Loaded ${loadedCount}/${enabled.length} plugins in ${time}ms`);
|
||
|
||
return this._loaded;
|
||
} finally {
|
||
this._loading = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @zh 获取已加载插件
|
||
* @en Get loaded plugins
|
||
*/
|
||
getLoaded(): PluginLoadInfo[] {
|
||
return [...this._loaded.values()].filter(p => p.state === 'loaded');
|
||
}
|
||
|
||
/**
|
||
* @zh 获取失败的插件
|
||
* @en Get failed plugins
|
||
*/
|
||
getFailed(): PluginLoadInfo[] {
|
||
return [...this._loaded.values()].filter(
|
||
p => p.state === 'failed' || p.state === 'missing'
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @zh 获取插件信息
|
||
* @en Get plugin info
|
||
*/
|
||
get(packageId: string): PluginLoadInfo | undefined {
|
||
return this._loaded.get(packageId);
|
||
}
|
||
|
||
/**
|
||
* @zh 重置
|
||
* @en Reset
|
||
*/
|
||
reset(): void {
|
||
this._loaded.clear();
|
||
}
|
||
|
||
// ========== 私有方法 ==========
|
||
|
||
private async _loadOne(config: PluginLoadConfig): Promise<void> {
|
||
const info: PluginLoadInfo = {
|
||
packageId: config.packageId,
|
||
state: 'loading'
|
||
};
|
||
this._loaded.set(config.packageId, info);
|
||
const start = Date.now();
|
||
|
||
try {
|
||
let plugin: IRuntimePlugin | null = null;
|
||
|
||
switch (config.sourceType) {
|
||
case 'npm':
|
||
plugin = await this._loadNpm(config);
|
||
break;
|
||
case 'local':
|
||
plugin = await this._loadLocal(config);
|
||
break;
|
||
case 'static':
|
||
logger.warn(`Static plugin ${config.packageId} should be pre-registered`);
|
||
break;
|
||
}
|
||
|
||
if (plugin) {
|
||
info.plugin = plugin;
|
||
info.state = 'loaded';
|
||
info.loadTime = Date.now() - start;
|
||
runtimePluginManager.register(plugin);
|
||
logger.debug(`Loaded: ${config.packageId} (${info.loadTime}ms)`);
|
||
} else {
|
||
throw new Error('Plugin export not found');
|
||
}
|
||
} catch (error) {
|
||
info.state = 'failed';
|
||
info.error = error instanceof Error ? error.message : String(error);
|
||
info.loadTime = Date.now() - start;
|
||
logger.error(`Failed: ${config.packageId} - ${info.error}`);
|
||
|
||
if (!this._config.continueOnFailure) {
|
||
throw error;
|
||
}
|
||
}
|
||
}
|
||
|
||
private async _loadNpm(config: PluginLoadConfig): Promise<IRuntimePlugin | null> {
|
||
const module = await import(/* @vite-ignore */ config.packageId);
|
||
const exportName = config.exportName || 'default';
|
||
const plugin = module[exportName] as IRuntimePlugin;
|
||
return plugin?.manifest ? plugin : null;
|
||
}
|
||
|
||
private async _loadLocal(config: PluginLoadConfig): Promise<IRuntimePlugin | null> {
|
||
if (!config.localPath) {
|
||
throw new Error('Local path not specified');
|
||
}
|
||
const code = await this._config.localLoader(config.localPath);
|
||
return this._config.localExecutor(code, config.packageId);
|
||
}
|
||
|
||
private _sortByDependencies(plugins: PluginLoadConfig[]): PluginLoadConfig[] {
|
||
const items: IDependable[] = plugins.map(p => ({
|
||
id: p.packageId,
|
||
dependencies: p.dependencies
|
||
}));
|
||
const map = new Map(plugins.map(p => [p.packageId, p]));
|
||
|
||
const result = topologicalSort(items, { resolveId: resolveDependencyId });
|
||
if (result.hasCycles) {
|
||
throw new Error(`Circular dependency: ${result.cycleIds?.join(', ')}`);
|
||
}
|
||
|
||
return result.sorted.map(item => map.get(item.id)!);
|
||
}
|
||
|
||
private _validateDependencies(plugins: PluginLoadConfig[]): Map<string, string[]> {
|
||
const enabledIds = new Set(plugins.map(p => p.packageId));
|
||
const missing = new Map<string, string[]>();
|
||
|
||
for (const plugin of plugins) {
|
||
const deps = plugin.dependencies || [];
|
||
const missingDeps = deps
|
||
.map(d => resolveDependencyId(d))
|
||
.filter(d => !enabledIds.has(d));
|
||
|
||
if (missingDeps.length > 0) {
|
||
missing.set(plugin.packageId, missingDeps);
|
||
}
|
||
}
|
||
|
||
return missing;
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// 便捷函数 | Convenience Functions
|
||
// ============================================================================
|
||
|
||
/** @zh 已加载插件缓存 @en Loaded plugins cache */
|
||
const loadedCache = new Map<string, IRuntimePlugin>();
|
||
|
||
/**
|
||
* @zh 加载单个插件
|
||
* @en Load single plugin
|
||
*/
|
||
export async function loadPlugin(
|
||
packageId: string,
|
||
info: PluginPackageInfo
|
||
): Promise<IRuntimePlugin | null> {
|
||
if (loadedCache.has(packageId)) {
|
||
return loadedCache.get(packageId)!;
|
||
}
|
||
|
||
try {
|
||
const module = await import(/* @vite-ignore */ packageId);
|
||
const plugin = module[info.pluginExport || 'default'] as IRuntimePlugin;
|
||
|
||
if (!plugin?.manifest) {
|
||
logger.warn(`Invalid plugin: ${packageId}`);
|
||
return null;
|
||
}
|
||
|
||
loadedCache.set(packageId, plugin);
|
||
return plugin;
|
||
} catch (error) {
|
||
logger.error(`Failed to load ${packageId}:`, error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @zh 加载启用的插件(简化 API)
|
||
* @en Load enabled plugins (simplified API)
|
||
*/
|
||
export async function loadEnabledPlugins(
|
||
config: ProjectPluginConfig,
|
||
packageInfoMap: Record<string, PluginPackageInfo>
|
||
): Promise<void> {
|
||
const plugins: PluginLoadConfig[] = [];
|
||
|
||
for (const [id, cfg] of Object.entries(config.plugins)) {
|
||
if (!cfg.enabled) continue;
|
||
const info = packageInfoMap[id];
|
||
if (!info) {
|
||
logger.warn(`No package info for ${id}`);
|
||
continue;
|
||
}
|
||
|
||
plugins.push({
|
||
packageId: id,
|
||
enabled: true,
|
||
sourceType: 'npm',
|
||
exportName: info.pluginExport,
|
||
dependencies: info.dependencies
|
||
});
|
||
}
|
||
|
||
const loader = new PluginLoader({ plugins });
|
||
await loader.loadAll();
|
||
}
|
||
|
||
/**
|
||
* @zh 注册静态插件
|
||
* @en Register static plugin
|
||
*/
|
||
export function registerStaticPlugin(plugin: IRuntimePlugin): void {
|
||
runtimePluginManager.register(plugin);
|
||
}
|
||
|
||
/**
|
||
* @zh 获取已加载插件
|
||
* @en Get loaded plugins
|
||
*/
|
||
export function getLoadedPlugins(): IRuntimePlugin[] {
|
||
return [...loadedCache.values()];
|
||
}
|
||
|
||
/**
|
||
* @zh 重置加载器
|
||
* @en Reset loader
|
||
*/
|
||
export function resetPluginLoader(): void {
|
||
loadedCache.clear();
|
||
}
|