Files
esengine/packages/engine/runtime-core/src/PluginLoader.ts
YHH 155411e743 refactor: reorganize package structure and decouple framework packages (#338)
* 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
2025-12-26 14:50:35 +08:00

416 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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();
}