refactor: 代码规范化与依赖清理 (#317)

* refactor(deps): 统一编辑器包依赖配置 & 优化分层架构

- 将 ecs-engine-bindgen 提升为 Layer 1 核心包
- 统一 9 个编辑器包的依赖声明模式
- 清理废弃的包目录 (ui, ui-editor, network-*)

* refactor(tokens): 修复 PrefabService 令牌冲突 & 补充 module.json

- 将 editor-core 的 PrefabServiceToken 改名为 EditorPrefabServiceToken
  避免与 asset-system 的 PrefabServiceToken 冲突 (Symbol.for 冲突)
- 为 mesh-3d 添加 module.json
- 为 world-streaming 添加 module.json

* refactor(editor-core): 整理导出结构 & 添加 blueprint tokens.ts

- 按功能分组整理 editor-core 的 65 行导出
- 添加清晰的分组注释 (中英双语)
- 为 blueprint 添加占位符 tokens.ts

* chore(editor): 为 14 个编辑器插件包添加 module.json

统一编辑器包的模块配置,包含:
- isEditorPlugin 标识
- runtimeModule 关联
- exports 导出清单 (inspectors, panels, gizmos)

* refactor(core): 改进类型安全 - 减少 as any 使用

- 添加 GlobalTypes.ts 定义小游戏平台和 Chrome API 类型
- SoAStorage 使用 IComponentTypeMetadata 替代 as any
- PlatformDetector 使用类型安全的平台检测
- 添加 ISoAStorageStats/ISoAFieldStats 接口

* feat(editor): 添加 EditorServicesContext 解决 prop drilling

- 新增 contexts/EditorServicesContext.tsx 提供统一服务访问
- App.tsx 包裹 EditorServicesProvider
- 提供 useEditorServices/useMessageHub 等便捷 hooks
- SceneHierarchy 添加迁移注释,后续可移除 props

* docs(editor): 澄清 inspector 目录架构关系

- inspector/ 标记为内部实现,添加 @deprecated 警告
- inspectors/ 标记为公共 API 入口点
- 添加架构说明文档

* refactor(editor): 添加全局类型声明消除 window as any

- 创建 editor-app/src/global.d.ts 声明 Window 接口扩展
- 创建 editor-core/src/global.d.ts 声明 Window 接口扩展
- 更新 App.tsx 使用类型安全的 window 属性访问
- 更新 PluginLoader.ts 使用 window.__ESENGINE_PLUGINS__
- 更新 PluginSDKRegistry.ts 使用 window.__ESENGINE_SDK__
- 更新 UserCodeService.ts 使用类型安全的全局变量访问

* refactor(editor): 提取项目和场景操作到独立 hooks

- 创建 useProjectActions hook 封装项目操作
- 创建 useSceneActions hook 封装场景操作
- 为渐进式重构 App.tsx 做准备

* refactor(editor): 清理冗余代码和未使用文件

删除的目录和文件:
- application/state/ - 重复的状态管理(与 stores/ 重复)
- 8 个孤立 CSS 文件(对应组件不存在)
- AssetBrowser.tsx - 仅为 ContentBrowser 的向后兼容包装
- AssetPicker.tsx - 未被使用
- AssetPickerDialog.tsx (顶级) - 已被 dialogs/ 版本取代
- EntityInspector.tsx (顶级) - 已被 inspectors/views/ 版本取代

修复:
- 移除 App.tsx 中未使用的导入
- 更新 application/index.ts 移除已删除模块
- 修复 useProjectActions.ts 的 MutableRefObject 类型

* refactor(editor): 统一 inspectors 模块导出结构

- 在 inspectors/index.ts 重新导出 PropertyInspector
- 创建 inspectors/fields/index.ts barrel export
- 导出 views、fields、common 子模块
- 更新 EntityInspector 使用统一入口导入

* refactor(editor): 删除废弃的 Profiler 组件

删除未使用的组件(共 1059 行):
- ProfilerPanel.tsx (229 行)
- ProfilerWindow.tsx (589 行)
- ProfilerDockPanel.tsx (241 行)
- ProfilerPanel.css
- ProfilerDockPanel.css

保留:AdvancedProfiler + AdvancedProfilerWindow(正在使用)

* refactor(runtime-core): 统一依赖处理与插件状态管理

- 新增 DependencyUtils 统一拓扑排序和依赖验证
- 新增 PluginState 定义插件生命周期状态机
- 合并 UnifiedPluginLoader 到 PluginLoader
- 清理 index.ts 移除不必要的 Token re-exports
- 新增 RuntimeMode/UserCodeRealm/ImportMapGenerator

* refactor(editor-core): 使用统一的 ImportMapGenerator

- WebBuildPipeline 使用 runtime-core 的 generateImportMap
- UserCodeService 添加 ImportMap 相关接口

* feat(compiler): 增强 esbuild 查找策略

- 支持本地 node_modules、pnpm exec、npx、全局多种来源
- EngineService 使用 RuntimeMode

* refactor(runtime-core): 简化 GameRuntime 代码

- 合并 _disableGameLogicSystems/_enableGameLogicSystems 为 _setGameLogicSystemsEnabled
- 精简本地 Token 定义的注释

* refactor(editor-core): 引入 BaseRegistry 基类消除代码重复

- 新增 BaseRegistry 和 PrioritizedRegistry 基类
- 重构 CompilerRegistry, InspectorRegistry, FieldEditorRegistry
- 统一注册表的日志记录和错误处理

* refactor(editor-core): 扩展 BaseRegistry 重构

- ComponentInspectorRegistry 继承 PrioritizedRegistry
- EditorComponentRegistry 继承 BaseRegistry
- EntityCreationRegistry 继承 BaseRegistry
- PropertyRendererRegistry 继承 PrioritizedRegistry
- 导出 BaseRegistry 基类供外部使用
- 统一双语注释格式

* refactor(editor-core): 代码优雅性优化

CommandManager:
- 提取 tryMergeWithLast() 和 pushToUndoStack() 消除重复代码
- 统一双语注释格式

FileActionRegistry:
- 提取 normalizeExtension() 消除扩展名规范化重复
- 统一私有属性命名风格(_前缀)
- 使用 createRegistryToken 统一 Token 创建

BaseRegistry:
- 添加 IOrdered 接口
- 添加 sortByOrder() 排序辅助方法

EntityCreationRegistry:
- 使用 sortByOrder() 简化排序逻辑

* refactor(editor-core): 统一日志系统 & 代码规范优化

- GizmoRegistry: 使用 createLogger 替代 console.warn
- VirtualNodeRegistry: 使用 createLogger 替代 console.warn
- WindowRegistry: 使用 logger、添加 _ 前缀、导出 IWindowRegistry token
- EditorViewportService: 使用 createLogger 替代 console.warn
- ComponentActionRegistry: 使用 logger、添加 _ 前缀、返回值改进
- SettingsRegistry: 使用 logger、提取 ensureCategory/ensureSection 方法
- 添加 WindowRegistry 到主导出

* refactor(editor-core): ModuleRegistry 使用 logger 替代 console

* refactor(editor-core): SerializerRegistry/UIRegistry 添加 token 和 _ 前缀

* refactor(editor-core): UIRegistry 代码优雅性 & Token 命名统一

- UIRegistry: 提取 _sortByOrder 消除 6 处重复排序逻辑
- UIRegistry: 添加分节注释和双语文档
- FieldEditorRegistry: Token 重命名为 FieldEditorRegistryToken
- PropertyRendererRegistry: Token 重命名为 PropertyRendererRegistryToken

* refactor(core): 统一日志系统 - console 替换为 logger

- ComponentSerializer: 使用 logger 替代 console.warn
- ComponentRegistry: console.warn → logger.warn (已有 logger)
- SceneSerializer: 添加 logger,替换 console.warn/error
- SystemScheduler: 添加 logger,替换 console.warn
- VersionMigration: 添加 logger,替换所有 console.warn
- RuntimeModeService: console.error → logger.error
- Core.ts: _logger 改为 readonly,双语错误消息
- SceneSerializer 修复:使用 getComponentTypeName 替代 constructor.name

* fix(core): 修复 constructor.name 压缩后失效问题

- Scene.ts: 使用 system.systemName 替代 system.constructor.name
- CommandBuffer.ts: 使用 getComponentTypeName() 替代 constructor.name

* refactor(editor-core): 代码规范优化 - 私有方法命名 & 日志统一

- BuildService: console → logger
- FileActionRegistry: 添加 logger, 私有方法 _ 前缀
- SettingsRegistry: 私有方法 _ 前缀 (ensureCategory → _ensureCategory)

* refactor(core): Scene.ts 私有变量命名规范化

- logger → _logger (遵循私有变量 _ 前缀规范)

* refactor(editor-core): 服务类私有成员命名规范化

- CommandManager: 私有变量/方法添加 _ 前缀
  - undoStack/redoStack/config/isExecuting
  - tryMergeWithLast/pushToUndoStack
- LocaleService: 私有变量/方法添加 _ 前缀
  - currentLocale/translations/changeListeners
  - deepMerge/getNestedValue/loadSavedLocale/saveLocale

* refactor(core): 私有成员命名规范化 & 单例模式优化

- Component.ts: _idGenerator 私有静态变量规范化
- PlatformManager.ts: _instance, _adapter, _logger 规范化
- AutoProfiler.ts: _instance, _config 及所有私有方法规范化
- ProfilerSDK.ts: _instance, _config 及所有私有方法规范化
- ComponentPoolManager: _instance, _pools, _usageTracker 规范化
- GlobalEventBus: _instance 规范化
- 添加中英双语 JSDoc 注释

* refactor(editor-app,behavior-tree-editor): 私有成员 & 单例模式命名规范化

editor-app:
- EngineService: private static instance → _instance
- EditorEngineSync: 所有私有成员添加 _ 前缀
- RuntimeResolver: 所有私有成员和方法添加 _ 前缀
- SettingsService: 所有私有成员和方法添加 _ 前缀

behavior-tree-editor:
- GlobalBlackboardService: 所有私有成员和方法添加 _ 前缀
- NotificationService: private static instance → _instance
- NodeRegistryService: 所有私有成员和方法添加 _ 前缀
- TreeStateAdapter: private static instance → _instance

* fix(editor-runtime): 添加 editor-core 到 external 避免传递依赖问题

将 @esengine/editor-core 添加到 vite external 配置,
避免 editor-core → runtime-core → ecs-engine-bindgen 的传递依赖
被错误地打包进 editor-runtime.js,导致 CI 构建失败。

* fix(core): 修复空接口 lint 错误

将 IByteDanceMiniGameAPI、IAlipayMiniGameAPI、IBaiduMiniGameAPI 从空接口改为类型别名,修复 no-empty-object-type 规则报错
This commit is contained in:
YHH
2025-12-24 20:57:08 +08:00
committed by GitHub
parent 58f70a5783
commit dbc6793dc4
133 changed files with 6880 additions and 9141 deletions

View File

@@ -29,19 +29,10 @@ import {
import { AssetManager, EngineIntegration, AssetManagerToken, setGlobalAssetDatabase } from '@esengine/asset-system';
// ============================================================================
// 本地服务令牌定义 | Local Service Token Definitions
// 本地服务令牌(松耦合设计)| Local Service Tokens (Loose Coupling)
// ============================================================================
// 这些令牌使用 createServiceToken() 本地定义,而不是从源模块导入。
// 这是有意为之:
// 1. runtime-core 应保持与 ui/sprite/behavior-tree 等模块的松耦合
// 2. createServiceToken() 使用 Symbol.for(),确保相同名称在运行时匹配
// 3. 本地接口提供类型安全,无需引入模块依赖
//
// These tokens are defined locally using createServiceToken() instead of
// importing from source modules. This is intentional:
// 1. runtime-core should remain loosely coupled to ui/sprite/behavior-tree etc.
// 2. createServiceToken() uses Symbol.for(), ensuring same names match at runtime
// 3. Local interfaces provide type safety without introducing module dependencies
// 使用 Symbol.for() 确保与源模块的 Token 在运行时匹配
// Uses Symbol.for() to match source module tokens at runtime
// ============================================================================
/**
@@ -97,16 +88,23 @@ import {
type IRuntimeModule
} from './PluginManager';
import {
loadEnabledPlugins,
type PluginPackageInfo,
type ProjectPluginConfig
PluginLoader,
type PluginLoadConfig
} from './PluginLoader';
import {
BUILTIN_PLUGIN_PACKAGES,
mergeProjectConfig,
convertToPluginLoadConfigs,
type ProjectConfig
} from './ProjectConfig';
import type { IPlatformAdapter, PlatformAdapterConfig } from './IPlatformAdapter';
import {
RuntimeMode,
getRuntimeModeConfig,
isEditorMode as checkIsEditorMode,
shouldEnableGameLogic,
type RuntimeModeConfig
} from './RuntimeMode';
/**
* 运行时配置
@@ -143,6 +141,7 @@ export interface RuntimeState {
initialized: boolean;
running: boolean;
paused: boolean;
mode: RuntimeMode;
}
/**
@@ -166,7 +165,8 @@ export class GameRuntime {
private _state: RuntimeState = {
initialized: false,
running: false,
paused: false
paused: false,
mode: RuntimeMode.EditorStatic
};
private _animationFrameId: number | null = null;
@@ -196,6 +196,14 @@ export class GameRuntime {
return { ...this._state };
}
/**
* @zh 获取当前运行模式
* @en Get current runtime mode
*/
get mode(): RuntimeMode {
return this._state.mode;
}
/**
* 获取场景
*/
@@ -253,6 +261,95 @@ export class GameRuntime {
return this._platform;
}
/**
* @zh 设置运行模式
* @en Set runtime mode
*
* @zh 根据模式自动配置系统启用状态、UI 显示等
* @en Automatically configures system enabling, UI visibility, etc. based on mode
*
* @param mode - @zh 目标模式 @en Target mode
* @param options - @zh 可选覆盖配置 @en Optional override configuration
*/
setMode(mode: RuntimeMode, options?: Partial<RuntimeModeConfig>): void {
if (!this._state.initialized) {
console.warn('[GameRuntime] Cannot set mode before initialization');
return;
}
const previousMode = this._state.mode;
this._state.mode = mode;
// 获取模式配置并应用覆盖
const config = { ...getRuntimeModeConfig(mode), ...options };
console.log(`[GameRuntime] Mode: ${previousMode}${mode}`);
// 1. 配置场景编辑器模式
if (this._scene) {
this._scene.isEditorMode = !config.triggerLifecycle;
}
// 2. 配置渲染系统
if (this._renderSystem) {
this._renderSystem.setPreviewMode(mode !== RuntimeMode.EditorStatic);
this._renderSystem.setShowGizmos(config.showGizmos);
}
// 3. 配置引擎桥接
if (this._bridge) {
this._bridge.setShowGrid(config.showGrid);
this._bridge.setEditorMode(config.isEditorEnvironment);
}
// 4. 配置输入系统
if (this._inputSystem) {
this._inputSystem.enabled = config.enableInput;
}
// 5. 配置游戏逻辑系统
this._applySystemConfig(config);
// 6. 触发生命周期(从静态模式进入其他模式时)
if (previousMode === RuntimeMode.EditorStatic && config.triggerLifecycle) {
this._scene?.begin();
}
// 7. 更新运行状态
this._state.running = shouldEnableGameLogic(mode);
this._state.paused = false;
}
/**
* @zh 应用系统配置
* @en Apply system configuration
*/
private _applySystemConfig(config: RuntimeModeConfig): void {
const services = this._systemContext?.services;
if (!services) return;
// 物理系统
const physicsSystem = services.get(Physics2DSystemToken);
if (physicsSystem) {
physicsSystem.enabled = config.enablePhysics;
}
// 行为树系统
const behaviorTreeSystem = services.get(BehaviorTreeSystemToken);
if (behaviorTreeSystem) {
behaviorTreeSystem.enabled = config.enableBehaviorTree;
if (config.enableBehaviorTree) {
behaviorTreeSystem.startAllAutoStartTrees?.();
}
}
// 动画系统
const animatorSystem = services.get(SpriteAnimatorSystemToken);
if (animatorSystem) {
animatorSystem.enabled = config.enableAnimation;
}
}
/**
* 初始化运行时
* Initialize runtime
@@ -395,14 +492,18 @@ export class GameRuntime {
defaultWorld.start();
}
// 14. 编辑器模式下的特殊处理
if (this._platform.isEditorMode()) {
// 禁用游戏逻辑系统
this._disableGameLogicSystems();
}
this._state.initialized = true;
// 14. 设置初始模式
const initialMode = this._platform.isEditorMode()
? RuntimeMode.EditorStatic
: RuntimeMode.Standalone;
this._state.mode = initialMode;
// 应用初始模式配置(不触发 setMode 的完整流程,因为刚初始化)
const modeConfig = getRuntimeModeConfig(initialMode);
this._applySystemConfig(modeConfig);
// 15. 自动启动渲染循环
if (this._config.autoStartRenderLoop !== false) {
this._startRenderLoop();
@@ -414,72 +515,53 @@ export class GameRuntime {
}
/**
* 加载并初始化插件
* @zh 加载并初始化插件
* @en Load and initialize plugins
*/
private async _initializePlugins(): Promise<void> {
// 检查是否已有插件注册(静态导入场景)
// Check if plugins are already registered (static import scenario)
const hasPlugins = runtimePluginManager.getPlugins().length > 0;
if (!hasPlugins) {
// 没有预注册的插件,尝试动态加载
// No pre-registered plugins, try dynamic loading
await loadEnabledPlugins(
{ plugins: this._projectConfig.plugins },
BUILTIN_PLUGIN_PACKAGES
);
const configs = convertToPluginLoadConfigs(this._projectConfig, BUILTIN_PLUGIN_PACKAGES);
const loader = new PluginLoader({ plugins: configs });
await loader.loadAll();
const loaded = loader.getLoaded();
const failed = loader.getFailed();
if (loaded.length > 0) {
console.log(`[GameRuntime] Loaded: ${loaded.map(p => p.packageId).join(', ')}`);
}
if (failed.length > 0) {
for (const p of failed) {
console.warn(`[GameRuntime] Failed: ${p.packageId} - ${p.error}`);
}
}
}
// 初始化插件(注册组件和服务)
await runtimePluginManager.initializeRuntime(Core.services);
}
/**
* 禁用游戏逻辑系统(编辑器模式)
* @zh 设置游戏逻辑系统启用状态
* @en Set game logic systems enabled state
*/
private _disableGameLogicSystems(): void {
const services = this._systemContext?.services;
if (!services) return;
// 这些系统由插件创建,通过服务注册表获取引用
const animatorSystem = services.get(SpriteAnimatorSystemToken);
if (animatorSystem) {
animatorSystem.enabled = false;
}
const behaviorTreeSystem = services.get(BehaviorTreeSystemToken);
if (behaviorTreeSystem) {
behaviorTreeSystem.enabled = false;
}
const physicsSystem = services.get(Physics2DSystemToken);
if (physicsSystem) {
physicsSystem.enabled = false;
}
}
/**
* 启用游戏逻辑系统(预览/运行模式)
*/
private _enableGameLogicSystems(): void {
private _setGameLogicSystemsEnabled(enabled: boolean): void {
const services = this._systemContext?.services;
if (!services) return;
const animatorSystem = services.get(SpriteAnimatorSystemToken);
if (animatorSystem) {
animatorSystem.enabled = true;
}
if (animatorSystem) animatorSystem.enabled = enabled;
const behaviorTreeSystem = services.get(BehaviorTreeSystemToken);
if (behaviorTreeSystem) {
behaviorTreeSystem.enabled = true;
behaviorTreeSystem.startAllAutoStartTrees?.();
behaviorTreeSystem.enabled = enabled;
if (enabled) behaviorTreeSystem.startAllAutoStartTrees?.();
}
const physicsSystem = services.get(Physics2DSystemToken);
if (physicsSystem) {
physicsSystem.enabled = true;
}
if (physicsSystem) physicsSystem.enabled = enabled;
}
/**
@@ -518,36 +600,23 @@ export class GameRuntime {
}
/**
* 开始运行(启用游戏逻辑
* Start running (enable game logic)
* @zh 开始运行(切换到预览模式
* @en Start running (switch to preview mode)
*
* @zh 在编辑器中,从 EditorStatic 切换到 EditorPreview
* @en In editor, switches from EditorStatic to EditorPreview
*/
start(): void {
if (!this._state.initialized || this._state.running) {
return;
}
this._state.running = true;
this._state.paused = false;
// 根据平台选择目标模式
const targetMode = this._platform.isEditorMode()
? RuntimeMode.EditorPreview
: RuntimeMode.Standalone;
// 启用预览模式
if (this._renderSystem) {
this._renderSystem.setPreviewMode(true);
}
// 禁用编辑器模式,启用 InputSystem 和组件生命周期回调
// Disable editor mode to enable InputSystem and component lifecycle callbacks
if (this._scene) {
this._scene.isEditorMode = false;
}
// 调用场景 begin() 触发延迟的组件生命周期回调
// Call scene begin() to trigger deferred component lifecycle callbacks
if (this._scene) {
this._scene.begin();
}
// 启用游戏逻辑系统
this._enableGameLogicSystems();
this.setMode(targetMode);
// 确保渲染循环在运行
this._startRenderLoop();
@@ -576,32 +645,23 @@ export class GameRuntime {
}
/**
* 停止运行(禁用游戏逻辑
* Stop running (disable game logic)
* @zh 停止运行(切换回静态模式
* @en Stop running (switch back to static mode)
*
* @zh 在编辑器中,从 EditorPreview 切换回 EditorStatic
* @en In editor, switches from EditorPreview back to EditorStatic
*/
stop(): void {
if (!this._state.running) {
return;
}
this._state.running = false;
this._state.paused = false;
// 禁用预览模式
if (this._renderSystem) {
this._renderSystem.setPreviewMode(false);
// 在编辑器中切换回静态模式setMode 会处理系统禁用)
if (this._platform.isEditorMode()) {
this.setMode(RuntimeMode.EditorStatic);
}
// 恢复编辑器模式(如果是编辑器平台)
// Restore editor mode (if editor platform)
if (this._scene && this._platform.isEditorMode()) {
this._scene.isEditorMode = true;
}
// 禁用游戏逻辑系统
this._disableGameLogicSystems();
// 重置物理系统
// 重置物理系统状态
const physicsSystem = this._systemContext?.services.get(Physics2DSystemToken);
if (physicsSystem) {
physicsSystem.reset?.();
@@ -618,9 +678,9 @@ export class GameRuntime {
}
// 启用系统执行一帧
this._enableGameLogicSystems();
this._setGameLogicSystemsEnabled(true);
Core.update(1 / 60);
this._disableGameLogicSystems();
this._setGameLogicSystemsEnabled(false);
}
/**

View File

@@ -0,0 +1,247 @@
/**
* @zh ImportMap 生成器
* @en ImportMap Generator
*
* @zh 提供统一的 ImportMap 生成逻辑,供编辑器预览和构建共用
* @en Provides unified ImportMap generation logic for editor preview and build
*/
import type { ModuleManifest } from './PluginManager';
import {
extractShortId,
getPackageName as getPackageNameFromId,
topologicalSort,
type IDependable
} from './utils/DependencyUtils';
/**
* @zh ImportMap 生成模式
* @en ImportMap generation mode
*/
export type ImportMapMode =
| 'development' // 开发模式:每个模块单独文件
| 'production' // 生产模式:核心模块打包,插件分离
| 'single-bundle'; // 单文件模式:所有模块打包到一个文件
/**
* @zh ImportMap 生成配置
* @en ImportMap generation configuration
*/
export interface ImportMapConfig {
/**
* @zh 生成模式
* @en Generation mode
*/
mode: ImportMapMode;
/**
* @zh 基础路径(用于构造相对 URL
* @en Base path (for constructing relative URLs)
*/
basePath?: string;
/**
* @zh 核心模块列表
* @en List of core modules
*/
coreModules: ModuleManifest[];
/**
* @zh 插件模块列表
* @en List of plugin modules
*/
pluginModules?: ModuleManifest[];
/**
* @zh 自定义路径生成器(可选)
* @en Custom path generator (optional)
*/
pathGenerator?: (module: ModuleManifest, isCore: boolean) => string;
}
/**
* @zh ImportMap 条目
* @en ImportMap entry
*/
export interface ImportMapEntry {
/**
* @zh 包名(如 @esengine/ecs-framework
* @en Package name (e.g., @esengine/ecs-framework)
*/
packageName: string;
/**
* @zh 模块路径(相对 URL
* @en Module path (relative URL)
*/
path: string;
/**
* @zh 模块 ID如 core
* @en Module ID (e.g., core)
*/
moduleId: string;
/**
* @zh 是否为核心模块
* @en Whether it's a core module
*/
isCore: boolean;
}
/**
* @zh 生成 ImportMap
* @en Generate ImportMap
*/
export function generateImportMap(config: ImportMapConfig): Record<string, string> {
const imports: Record<string, string> = {};
const basePath = config.basePath || '.';
// 根据模式选择路径生成策略
const getModulePath = config.pathGenerator || ((module: ModuleManifest, isCore: boolean) => {
switch (config.mode) {
case 'development':
// 开发模式:每个模块单独文件
return `${basePath}/libs/${module.id}/${module.id}.js`;
case 'production':
// 生产模式:核心模块打包,插件分离
if (isCore) {
return `${basePath}/libs/esengine.core.js`;
}
return `${basePath}/libs/plugins/${module.id}.js`;
case 'single-bundle':
// 单文件模式:所有模块打包
return `${basePath}/libs/esengine.bundle.js`;
}
});
// 处理核心模块
for (const module of config.coreModules) {
if (module.name) {
imports[module.name] = getModulePath(module, true);
}
}
// 处理插件模块
if (config.pluginModules) {
for (const module of config.pluginModules) {
if (module.name) {
imports[module.name] = getModulePath(module, false);
}
// 处理外部依赖
if (module.externalDependencies) {
for (const dep of module.externalDependencies) {
if (!imports[dep]) {
const depId = extractModuleId(dep);
imports[dep] = `${basePath}/libs/${config.mode === 'development' ? `${depId}/${depId}` : `plugins/${depId}`}.js`;
}
}
}
}
}
return imports;
}
/**
* @zh 生成 ImportMap 条目列表
* @en Generate ImportMap entry list
*/
export function generateImportMapEntries(config: ImportMapConfig): ImportMapEntry[] {
const entries: ImportMapEntry[] = [];
const importMap = generateImportMap(config);
for (const [packageName, path] of Object.entries(importMap)) {
const moduleId = extractModuleId(packageName);
const isCore = config.coreModules.some(m => m.name === packageName);
entries.push({
packageName,
path,
moduleId,
isCore
});
}
return entries;
}
/**
* @zh 生成 ImportMap HTML 脚本标签
* @en Generate ImportMap HTML script tag
*
* @param imports - @zh ImportMap 对象 @en ImportMap object
* @param indent - @zh 缩进空格数 @en Number of indent spaces
*/
export function generateImportMapScript(imports: Record<string, string>, indent = 4): string {
const indentStr = ' '.repeat(indent);
const json = JSON.stringify({ imports }, null, 2)
.split('\n')
.map((line, i) => i === 0 ? line : indentStr + line)
.join('\n');
return `<script type="importmap">
${indentStr}${json}
${indentStr}</script>`;
}
/**
* @zh 从包名提取模块 ID
* @en Extract module ID from package name
*
* @zh 重新导出自 DependencyUtils保持向后兼容
* @en Re-exported from DependencyUtils (for backward compatibility)
*
* @example
* extractModuleId('@esengine/ecs-framework') // 'core'
* extractModuleId('@esengine/sprite') // 'sprite'
*/
export const extractModuleId = extractShortId;
/**
* @zh 从模块 ID 获取包名
* @en Get package name from module ID
*
* @zh 重新导出自 DependencyUtils保持向后兼容
* @en Re-exported from DependencyUtils (for backward compatibility)
*
* @example
* getPackageName('core') // '@esengine/ecs-framework'
* getPackageName('sprite') // '@esengine/sprite'
*/
export const getPackageName = getPackageNameFromId;
/**
* @zh 收集模块的外部依赖
* @en Collect external dependencies of modules
*/
export function collectExternalDependencies(modules: ModuleManifest[]): Set<string> {
const deps = new Set<string>();
for (const module of modules) {
if (module.externalDependencies) {
for (const dep of module.externalDependencies) {
deps.add(dep);
}
}
}
return deps;
}
/**
* @zh 按依赖排序模块(拓扑排序)
* @en Sort modules by dependencies (topological sort)
*
* @zh 使用统一的 DependencyUtils.topologicalSort
* @en Uses unified DependencyUtils.topologicalSort
*/
export function sortModulesByDependencies<T extends IDependable>(
modules: T[]
): T[] {
const result = topologicalSort(modules, { algorithm: 'dfs' });
return result.sorted;
}

View File

@@ -1,118 +1,415 @@
import { runtimePluginManager, type IRuntimePlugin } from './PluginManager';
/**
* @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, any>;
options?: Record<string, unknown>;
}
/**
* @zh 项目插件配置
* @en Project plugin configuration
*/
export interface ProjectPluginConfig {
plugins: Record<string, PluginConfig>;
}
interface LoadedPluginInfo {
id: string;
plugin: IRuntimePlugin;
packageInfo: PluginPackageInfo;
/**
* @zh 插件加载配置
* @en Plugin load configuration
*/
export interface PluginLoadConfig {
packageId: string;
enabled: boolean;
sourceType: PluginSourceType;
exportName?: string;
dependencies?: string[];
options?: Record<string, unknown>;
localPath?: string;
}
const loadedPlugins = new Map<string, LoadedPluginInfo>();
/**
* @zh 插件加载信息
* @en Plugin load info
*/
export interface PluginLoadInfo {
packageId: string;
state: PluginLoadState;
plugin?: IRuntimePlugin;
error?: string;
missingDeps?: string[];
loadTime?: number;
}
/**
* 从模块动态加载插件
* @param packageId 包 ID如 '@esengine/sprite'
* @param packageInfo 包的 esengine 配置
* @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,
packageInfo: PluginPackageInfo
info: PluginPackageInfo
): Promise<IRuntimePlugin | null> {
if (loadedPlugins.has(packageId)) {
return loadedPlugins.get(packageId)!.plugin;
if (loadedCache.has(packageId)) {
return loadedCache.get(packageId)!;
}
try {
const module = await import(/* @vite-ignore */ packageId);
const exportName = packageInfo.pluginExport || 'default';
const plugin = module[exportName] as IRuntimePlugin;
const plugin = module[info.pluginExport || 'default'] as IRuntimePlugin;
if (!plugin || !plugin.manifest) {
console.warn(`[PluginLoader] Invalid plugin export from ${packageId}`);
if (!plugin?.manifest) {
logger.warn(`Invalid plugin: ${packageId}`);
return null;
}
loadedPlugins.set(packageId, {
id: packageId,
plugin,
packageInfo
});
loadedCache.set(packageId, plugin);
return plugin;
} catch (error) {
console.error(`[PluginLoader] Failed to load plugin ${packageId}:`, 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 sortedPlugins: Array<{ id: string; info: PluginPackageInfo }> = [];
const plugins: PluginLoadConfig[] = [];
for (const [id, pluginConfig] of Object.entries(config.plugins)) {
if (!pluginConfig.enabled) continue;
const packageInfo = packageInfoMap[id];
if (!packageInfo) {
console.warn(`[PluginLoader] No package info for ${id}, skipping`);
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;
}
sortedPlugins.push({ id, info: packageInfo });
plugins.push({
packageId: id,
enabled: true,
sourceType: 'npm',
exportName: info.pluginExport,
dependencies: info.dependencies
});
}
// 引擎核心插件优先加载
sortedPlugins.sort((a, b) => {
if (a.info.isEnginePlugin && !b.info.isEnginePlugin) return -1;
if (!a.info.isEnginePlugin && b.info.isEnginePlugin) return 1;
return 0;
});
for (const { id, info } of sortedPlugins) {
const plugin = await loadPlugin(id, info);
if (plugin) {
runtimePluginManager.register(plugin);
}
}
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 Array.from(loadedPlugins.values()).map(info => info.plugin);
return [...loadedCache.values()];
}
/**
* 重置插件加载器状态
* @zh 重置加载器
* @en Reset loader
*/
export function resetPluginLoader(): void {
loadedPlugins.clear();
loadedCache.clear();
}

View File

@@ -1,140 +1,601 @@
/**
* Runtime Plugin Manager
* 运行时插件管理器
* @zh 运行时插件管理器
* @en Runtime Plugin Manager
*
* @zh 提供插件生命周期管理的核心实现。
* @en Provides core implementation for plugin lifecycle management.
*
* @zh 设计原则 | Design principles:
* @en
* 1. 最小依赖 - 只依赖 ecs-framework 和 engine-core
* 2. 状态跟踪 - 详细的插件状态用于调试和 UI
* 3. 依赖验证 - 确保加载顺序正确
* 4. 错误隔离 - 单个插件失败不影响其他
*/
import { GlobalComponentRegistry, ServiceContainer } from '@esengine/ecs-framework';
import { GlobalComponentRegistry, ServiceContainer, createLogger } from '@esengine/ecs-framework';
import type { IScene } from '@esengine/ecs-framework';
import type { IRuntimePlugin, IRuntimeModule, SystemContext, ModuleManifest } from '@esengine/engine-core';
import {
topologicalSort,
resolveDependencyId,
getReverseDependencies,
type IDependable
} from './utils/DependencyUtils';
import type { PluginState } from './PluginState';
export type { IRuntimePlugin, IRuntimeModule, SystemContext, ModuleManifest };
export class RuntimePluginManager {
private _plugins = new Map<string, IRuntimePlugin>();
private _enabledPlugins = new Set<string>();
private _bInitialized = false;
const logger = createLogger('PluginManager');
// ============================================================================
// 类型定义 | Type Definitions
// ============================================================================
// PluginState 从 ./PluginState 重新导出
// PluginState is re-exported from ./PluginState
/**
* @zh 已注册的插件信息
* @en Registered plugin info
*/
export interface RegisteredPluginInfo {
/**
* @zh 插件实例
* @en Plugin instance
*/
plugin: IRuntimePlugin;
/**
* @zh 插件状态
* @en Plugin state
*/
state: PluginState;
/**
* @zh 是否启用
* @en Whether enabled
*/
enabled: boolean;
/**
* @zh 错误信息
* @en Error message
*/
error?: Error;
/**
* @zh 注册时间
* @en Registration time
*/
registeredAt: number;
/**
* @zh 激活时间
* @en Activation time
*/
activatedAt?: number;
/**
* @zh 创建的系统实例(用于清理)
* @en Created system instances (for cleanup)
*/
systemInstances?: any[];
}
/**
* @zh 插件配置
* @en Plugin configuration
*/
export interface RuntimePluginConfig {
/**
* @zh 启用的插件 ID 列表
* @en Enabled plugin ID list
*/
enabledPlugins: string[];
}
// ============================================================================
// RuntimePluginManager
// ============================================================================
/**
* @zh 运行时插件管理器
* @en Runtime Plugin Manager
*
* @zh 管理运行时插件的注册、初始化和生命周期。
* @en Manages registration, initialization, and lifecycle of runtime plugins.
*/
export class RuntimePluginManager {
private _plugins = new Map<string, RegisteredPluginInfo>();
private _initialized = false;
private _currentScene: IScene | null = null;
private _currentContext: SystemContext | null = null;
// ============================================================================
// 注册 | Registration
// ============================================================================
/**
* @zh 注册插件
* @en Register plugin
*
* @param plugin - @zh 插件实例 @en Plugin instance
*/
register(plugin: IRuntimePlugin): void {
const id = plugin.manifest.id;
if (this._plugins.has(id)) {
if (!plugin?.manifest?.id) {
logger.error('Cannot register plugin: invalid manifest');
return;
}
this._plugins.set(id, plugin);
if (plugin.manifest.defaultEnabled !== false) {
this._enabledPlugins.add(id);
const id = plugin.manifest.id;
if (this._plugins.has(id)) {
logger.warn(`Plugin ${id} is already registered, skipping`);
return;
}
const enabled = plugin.manifest.isCore === true ||
plugin.manifest.isEngineModule === true ||
plugin.manifest.defaultEnabled !== false;
this._plugins.set(id, {
plugin,
state: 'loading', // 已加载但未初始化
enabled,
registeredAt: Date.now()
});
logger.debug(`Plugin registered: ${id}`, {
enabled,
isCore: plugin.manifest.isCore,
isEngineModule: plugin.manifest.isEngineModule
});
}
/**
* @zh 批量注册插件
* @en Register multiple plugins
*
* @param plugins - @zh 插件列表 @en Plugin list
*/
registerMany(plugins: IRuntimePlugin[]): void {
for (const plugin of plugins) {
this.register(plugin);
}
}
enable(pluginId: string): void {
this._enabledPlugins.add(pluginId);
}
// ============================================================================
// 启用/禁用 | Enable/Disable
// ============================================================================
disable(pluginId: string): void {
this._enabledPlugins.delete(pluginId);
}
isEnabled(pluginId: string): boolean {
return this._enabledPlugins.has(pluginId);
}
loadConfig(config: { enabledPlugins: string[] }): void {
this._enabledPlugins.clear();
for (const id of config.enabledPlugins) {
this._enabledPlugins.add(id);
/**
* @zh 启用插件
* @en Enable plugin
*
* @param pluginId - @zh 插件 ID @en Plugin ID
* @returns @zh 是否成功 @en Whether successful
*/
enable(pluginId: string): boolean {
const info = this._plugins.get(pluginId);
if (!info) {
logger.error(`Plugin ${pluginId} not found`);
return false;
}
// 始终启用引擎核心模块
for (const [id, plugin] of this._plugins) {
if (plugin.manifest.isEngineModule) {
this._enabledPlugins.add(id);
if (info.plugin.manifest.isCore) {
logger.warn(`Core plugin ${pluginId} is always enabled`);
return true;
}
// 检查依赖 | Check dependencies
const deps = info.plugin.manifest.dependencies || [];
for (const dep of deps) {
const depId = resolveDependencyId(dep);
const depInfo = this._plugins.get(depId);
if (!depInfo?.enabled) {
logger.error(`Cannot enable ${pluginId}: dependency ${dep} is not enabled`);
return false;
}
}
info.enabled = true;
logger.info(`Plugin enabled: ${pluginId}`);
return true;
}
/**
* @zh 禁用插件
* @en Disable plugin
*
* @param pluginId - @zh 插件 ID @en Plugin ID
* @returns @zh 是否成功 @en Whether successful
*/
disable(pluginId: string): boolean {
const info = this._plugins.get(pluginId);
if (!info) {
logger.error(`Plugin ${pluginId} not found`);
return false;
}
if (info.plugin.manifest.isCore) {
logger.warn(`Core plugin ${pluginId} cannot be disabled`);
return false;
}
// 检查是否有其他插件依赖此插件(使用统一工具)
// Check if other plugins depend on this (using unified util)
const reverseDeps = this._getReverseDependencies(pluginId);
const enabledReverseDeps = Array.from(reverseDeps).filter(
id => this._plugins.get(id)?.enabled
);
if (enabledReverseDeps.length > 0) {
logger.error(`Cannot disable ${pluginId}: plugins ${enabledReverseDeps.join(', ')} depend on it`);
return false;
}
// 清理系统实例 | Cleanup system instances
if (info.systemInstances && this._currentScene) {
for (const system of info.systemInstances) {
try {
this._currentScene.removeSystem(system);
} catch (e) {
logger.warn(`Failed to remove system from ${pluginId}:`, e);
}
}
info.systemInstances = [];
}
info.enabled = false;
info.state = 'disabled';
logger.info(`Plugin disabled: ${pluginId}`);
return true;
}
/**
* @zh 检查插件是否启用
* @en Check if plugin is enabled
*/
isEnabled(pluginId: string): boolean {
return this._plugins.get(pluginId)?.enabled ?? false;
}
/**
* @zh 加载配置
* @en Load configuration
*
* @param config - @zh 插件配置 @en Plugin configuration
*/
loadConfig(config: RuntimePluginConfig): void {
const { enabledPlugins } = config;
for (const [id, info] of this._plugins) {
if (info.plugin.manifest.isCore || info.plugin.manifest.isEngineModule) {
info.enabled = true;
continue;
}
const shouldEnable = enabledPlugins.includes(id) ||
info.plugin.manifest.defaultEnabled === true;
info.enabled = shouldEnable;
}
logger.info('Plugin configuration loaded', {
enabled: Array.from(this._plugins.values()).filter(p => p.enabled).length,
total: this._plugins.size
});
}
// ============================================================================
// 初始化 | Initialization
// ============================================================================
/**
* @zh 初始化所有启用的插件
* @en Initialize all enabled plugins
*
* @param services - @zh 服务容器 @en Service container
*/
async initializeRuntime(services: ServiceContainer): Promise<void> {
if (this._bInitialized) {
if (this._initialized) {
logger.warn('Runtime already initialized');
return;
}
for (const [id, plugin] of this._plugins) {
if (!this._enabledPlugins.has(id)) continue;
const mod = plugin.runtimeModule;
const startTime = Date.now();
const sortedPlugins = this._topologicalSort();
// Phase 1: 注册组件 | Register components
for (const pluginId of sortedPlugins) {
const info = this._plugins.get(pluginId);
if (!info?.enabled) continue;
const mod = info.plugin.runtimeModule;
if (mod?.registerComponents) {
try {
info.state = 'initializing';
mod.registerComponents(GlobalComponentRegistry);
logger.debug(`Components registered for: ${pluginId}`);
} catch (e) {
console.error(`[PluginManager] Failed to register components for ${id}:`, e);
logger.error(`Failed to register components for ${pluginId}:`, e);
info.state = 'error';
info.error = e as Error;
}
}
}
for (const [id, plugin] of this._plugins) {
if (!this._enabledPlugins.has(id)) continue;
const mod = plugin.runtimeModule;
// Phase 2: 注册服务 | Register services
for (const pluginId of sortedPlugins) {
const info = this._plugins.get(pluginId);
if (!info?.enabled || info.state === 'error') continue;
const mod = info.plugin.runtimeModule;
if (mod?.registerServices) {
try {
mod.registerServices(services);
logger.debug(`Services registered for: ${pluginId}`);
} catch (e) {
console.error(`[PluginManager] Failed to register services for ${id}:`, e);
logger.error(`Failed to register services for ${pluginId}:`, e);
info.state = 'error';
info.error = e as Error;
}
}
}
for (const [id, plugin] of this._plugins) {
if (!this._enabledPlugins.has(id)) continue;
const mod = plugin.runtimeModule;
// Phase 3: 初始化回调 | Initialize callbacks
for (const pluginId of sortedPlugins) {
const info = this._plugins.get(pluginId);
if (!info?.enabled || info.state === 'error') continue;
const mod = info.plugin.runtimeModule;
if (mod?.onInitialize) {
try {
await mod.onInitialize();
info.state = 'active';
info.activatedAt = Date.now();
logger.debug(`Initialized: ${pluginId}`);
} catch (e) {
console.error(`[PluginManager] Failed to initialize ${id}:`, e);
logger.error(`Failed to initialize ${pluginId}:`, e);
info.state = 'error';
info.error = e as Error;
}
} else {
info.state = 'active';
info.activatedAt = Date.now();
}
}
this._bInitialized = true;
this._initialized = true;
const duration = Date.now() - startTime;
const activeCount = Array.from(this._plugins.values())
.filter(p => p.state === 'active').length;
logger.info(`Runtime initialized | 运行时初始化完成`, {
active: activeCount,
total: this._plugins.size,
duration: `${duration}ms`
});
}
/**
* @zh 为场景创建系统
* @en Create systems for scene
*
* @param scene - @zh 场景 @en Scene
* @param context - @zh 系统上下文 @en System context
*/
createSystemsForScene(scene: IScene, context: SystemContext): void {
// Phase 1: 创建系统
for (const [id, plugin] of this._plugins) {
if (!this._enabledPlugins.has(id)) continue;
const mod = plugin.runtimeModule;
this._currentScene = scene;
this._currentContext = context;
const sortedPlugins = this._topologicalSort();
// Phase 1: 创建系统 | Create systems
for (const pluginId of sortedPlugins) {
const info = this._plugins.get(pluginId);
if (!info?.enabled || info.state === 'error') continue;
const mod = info.plugin.runtimeModule;
if (mod?.createSystems) {
try {
const systemsBefore = scene.systems.length;
mod.createSystems(scene, context);
// 跟踪创建的系统 | Track created systems
const systemsAfter = scene.systems;
info.systemInstances = [];
for (let i = systemsBefore; i < systemsAfter.length; i++) {
info.systemInstances.push(systemsAfter[i]);
}
logger.debug(`Systems created for: ${pluginId}`, {
count: info.systemInstances.length
});
} catch (e) {
console.error(`[PluginManager] Failed to create systems for ${id}:`, e);
logger.error(`Failed to create systems for ${pluginId}:`, e);
info.state = 'error';
info.error = e as Error;
}
}
}
// Phase 2: 连接跨插件依赖
for (const [id, plugin] of this._plugins) {
if (!this._enabledPlugins.has(id)) continue;
const mod = plugin.runtimeModule;
// Phase 2: 系统创建后回调 | Post-creation callbacks
for (const pluginId of sortedPlugins) {
const info = this._plugins.get(pluginId);
if (!info?.enabled || info.state === 'error') continue;
const mod = info.plugin.runtimeModule;
if (mod?.onSystemsCreated) {
try {
mod.onSystemsCreated(scene, context);
logger.debug(`Systems wired for: ${pluginId}`);
} catch (e) {
console.error(`[PluginManager] Failed to wire dependencies for ${id}:`, e);
logger.error(`Failed to wire systems for ${pluginId}:`, e);
}
}
}
logger.info('Systems created for scene | 场景系统创建完成');
}
getPlugins(): IRuntimePlugin[] {
return Array.from(this._plugins.values());
}
// ============================================================================
// 查询 | Query
// ============================================================================
/**
* @zh 获取插件
* @en Get plugin
*/
getPlugin(id: string): IRuntimePlugin | undefined {
return this._plugins.get(id)?.plugin;
}
/**
* @zh 获取插件信息
* @en Get plugin info
*/
getPluginInfo(id: string): RegisteredPluginInfo | undefined {
return this._plugins.get(id);
}
/**
* @zh 获取所有插件
* @en Get all plugins
*/
getPlugins(): IRuntimePlugin[] {
return Array.from(this._plugins.values()).map(p => p.plugin);
}
/**
* @zh 获取所有启用的插件
* @en Get all enabled plugins
*/
getEnabledPlugins(): IRuntimePlugin[] {
return Array.from(this._plugins.values())
.filter(p => p.enabled)
.map(p => p.plugin);
}
/**
* @zh 获取插件状态
* @en Get plugin state
*/
getState(pluginId: string): PluginState | undefined {
return this._plugins.get(pluginId)?.state;
}
/**
* @zh 获取失败的插件
* @en Get failed plugins
*/
getFailedPlugins(): Array<{ id: string; error: Error }> {
const failed: Array<{ id: string; error: Error }> = [];
for (const [id, info] of this._plugins) {
if (info.state === 'error' && info.error) {
failed.push({ id, error: info.error });
}
}
return failed;
}
/**
* @zh 是否已初始化
* @en Whether initialized
*/
get initialized(): boolean {
return this._initialized;
}
// ============================================================================
// 生命周期 | Lifecycle
// ============================================================================
/**
* @zh 清理场景系统
* @en Clear scene systems
*/
clearSceneSystems(): void {
for (const [pluginId, info] of this._plugins) {
if (!info.enabled) continue;
const mod = info.plugin.runtimeModule;
if (mod?.onDestroy) {
try {
mod.onDestroy();
} catch (e) {
logger.error(`Error in ${pluginId}.onDestroy:`, e);
}
}
info.systemInstances = [];
}
this._currentScene = null;
this._currentContext = null;
logger.debug('Scene systems cleared');
}
/**
* @zh 重置管理器
* @en Reset manager
*/
reset(): void {
this.clearSceneSystems();
this._plugins.clear();
this._enabledPlugins.clear();
this._bInitialized = false;
this._initialized = false;
logger.info('PluginManager reset');
}
// ============================================================================
// 私有方法 | Private Methods
// ============================================================================
/**
* @zh 拓扑排序(使用统一的 DependencyUtils
* @en Topological sort (using unified DependencyUtils)
*/
private _topologicalSort(): string[] {
// 转换为 IDependable 格式
const items: IDependable[] = Array.from(this._plugins.entries()).map(
([id, info]) => ({
id,
dependencies: info.plugin.manifest.dependencies
})
);
const result = topologicalSort(items, {
algorithm: 'dfs',
resolveId: resolveDependencyId
});
if (result.hasCycles) {
logger.warn(`Circular dependencies detected: ${result.cycleIds?.join(', ')}`);
}
return result.sorted.map(item => item.id);
}
/**
* @zh 获取反向依赖(使用统一的 DependencyUtils
* @en Get reverse dependencies (using unified DependencyUtils)
*/
private _getReverseDependencies(pluginId: string): Set<string> {
const items: IDependable[] = Array.from(this._plugins.entries()).map(
([id, info]) => ({
id,
dependencies: info.plugin.manifest.dependencies
})
);
return getReverseDependencies(pluginId, items, {
resolveId: resolveDependencyId
});
}
}
/**
* @zh 全局运行时插件管理器实例
* @en Global runtime plugin manager instance
*/
export const runtimePluginManager = new RuntimePluginManager();

View File

@@ -0,0 +1,189 @@
/**
* @zh 插件生命周期状态
* @en Plugin Lifecycle State
*
* @zh 提供统一的插件状态定义,确保运行时和编辑器使用一致的状态机。
* @en Provides unified plugin state definition, ensuring runtime and editor use consistent state machine.
*
* @zh 状态转换图 | State Transition Diagram:
* ```
* ┌──────────────────────────────────────────┐
* │ │
* ▼ │
* ┌──────────┐ load ┌─────────┐ init ┌────────────┐ │
* │ Unloaded │ ──────► │ Loading │ ──────► │Initializing│ │
* └──────────┘ └─────────┘ └────────────┘ │
* ▲ │ │ │
* │ │ fail │ success │
* │ ▼ ▼ │
* │ ┌─────────┐ ┌─────────┐ │
* │ │ Error │◄────────│ Active │ │
* │ └─────────┘ error └─────────┘ │
* │ │ │ │
* │ │ retry │ disable │
* │ ▼ ▼ │
* │ ┌─────────┐ ┌──────────┐ │
* └──────────────│ Loading │◄────────│ Disabled │─────┘
* unload └─────────┘ enable └──────────┘
* ```
*/
// ============================================================================
// 插件状态 | Plugin State
// ============================================================================
/**
* @zh 插件生命周期状态
* @en Plugin lifecycle state
*
* @zh 统一定义,供 runtime-core 和 editor-core 共用
* @en Unified definition for both runtime-core and editor-core
*/
export enum PluginLifecycleState {
/**
* @zh 未加载 - 初始状态
* @en Unloaded - initial state
*/
Unloaded = 'unloaded',
/**
* @zh 加载中 - 正在加载插件代码
* @en Loading - loading plugin code
*/
Loading = 'loading',
/**
* @zh 初始化中 - 正在执行初始化逻辑
* @en Initializing - executing initialization logic
*/
Initializing = 'initializing',
/**
* @zh 活动中 - 插件正常运行
* @en Active - plugin running normally
*/
Active = 'active',
/**
* @zh 错误 - 加载或运行时出错
* @en Error - error during loading or runtime
*/
Error = 'error',
/**
* @zh 已禁用 - 用户主动禁用
* @en Disabled - user disabled
*/
Disabled = 'disabled'
}
/**
* @zh 插件状态(简化别名,向后兼容)
* @en Plugin state (simplified alias for backward compatibility)
*/
export type PluginState =
| 'unloaded'
| 'loading'
| 'initializing'
| 'active'
| 'error'
| 'disabled';
// ============================================================================
// 状态转换 | State Transitions
// ============================================================================
/**
* @zh 有效的状态转换
* @en Valid state transitions
*/
export const VALID_STATE_TRANSITIONS: Record<PluginState, PluginState[]> = {
unloaded: ['loading'],
loading: ['initializing', 'error', 'unloaded'],
initializing: ['active', 'error'],
active: ['disabled', 'error', 'unloaded'],
error: ['loading', 'unloaded'],
disabled: ['loading', 'unloaded']
};
/**
* @zh 检查状态转换是否有效
* @en Check if state transition is valid
*
* @param from - @zh 当前状态 @en Current state
* @param to - @zh 目标状态 @en Target state
* @returns @zh 是否允许转换 @en Whether transition is allowed
*/
export function isValidStateTransition(from: PluginState, to: PluginState): boolean {
return VALID_STATE_TRANSITIONS[from]?.includes(to) ?? false;
}
/**
* @zh 检查插件是否可操作
* @en Check if plugin is operable
*/
export function isPluginOperable(state: PluginState): boolean {
return state === 'active' || state === 'disabled';
}
/**
* @zh 检查插件是否正在加载
* @en Check if plugin is loading
*/
export function isPluginLoading(state: PluginState): boolean {
return state === 'loading' || state === 'initializing';
}
/**
* @zh 检查插件是否可用
* @en Check if plugin is available
*/
export function isPluginAvailable(state: PluginState): boolean {
return state === 'active';
}
// ============================================================================
// 状态转换事件 | State Transition Events
// ============================================================================
/**
* @zh 状态转换事件
* @en State transition event
*/
export interface PluginStateChangeEvent {
/**
* @zh 插件 ID
* @en Plugin ID
*/
pluginId: string;
/**
* @zh 之前的状态
* @en Previous state
*/
previousState: PluginState;
/**
* @zh 当前状态
* @en Current state
*/
currentState: PluginState;
/**
* @zh 错误信息(如果有)
* @en Error message (if any)
*/
error?: Error;
/**
* @zh 时间戳
* @en Timestamp
*/
timestamp: number;
}
/**
* @zh 状态变更监听器
* @en State change listener
*/
export type PluginStateChangeListener = (event: PluginStateChangeEvent) => void;

View File

@@ -1,4 +1,4 @@
import type { PluginPackageInfo, PluginConfig } from './PluginLoader';
import type { PluginPackageInfo, PluginConfig, PluginLoadConfig } from './PluginLoader';
export interface ProjectConfig {
name: string;
@@ -7,58 +7,128 @@ export interface ProjectConfig {
}
/**
* 内置引擎插件包信息
* 这些信息在构建时从各包的 package.json 中提取
* @zh 扩展的插件包信息(包含依赖)
* @en Extended plugin package info (with dependencies)
*/
export const BUILTIN_PLUGIN_PACKAGES: Record<string, PluginPackageInfo> = {
export interface ExtendedPluginPackageInfo extends PluginPackageInfo {
/**
* @zh 依赖的包 ID 列表
* @en List of dependency package IDs
*/
dependencies?: string[];
}
/**
* @zh 内置引擎插件的包信息(包含依赖关系)
* @en Built-in engine plugin package info (with dependencies)
*
* @zh 依赖顺序很重要,确保插件按正确顺序加载
* @en Dependency order matters, ensures plugins load in correct order
*/
export const BUILTIN_PLUGIN_PACKAGES: Record<string, ExtendedPluginPackageInfo> = {
'@esengine/engine-core': {
plugin: true,
pluginExport: 'EnginePlugin',
category: 'core',
isEnginePlugin: true
isEnginePlugin: true,
dependencies: []
},
'@esengine/camera': {
plugin: true,
pluginExport: 'CameraPlugin',
category: 'core',
isEnginePlugin: true
isEnginePlugin: true,
dependencies: ['@esengine/engine-core']
},
'@esengine/sprite': {
plugin: true,
pluginExport: 'SpritePlugin',
category: 'rendering',
isEnginePlugin: true
isEnginePlugin: true,
dependencies: ['@esengine/engine-core']
},
'@esengine/audio': {
plugin: true,
pluginExport: 'AudioPlugin',
category: 'audio',
isEnginePlugin: true
isEnginePlugin: true,
dependencies: ['@esengine/engine-core']
},
'@esengine/ui': {
plugin: true,
pluginExport: 'UIPlugin',
category: 'ui',
dependencies: ['@esengine/engine-core', '@esengine/sprite']
},
'@esengine/fairygui': {
plugin: true,
pluginExport: 'FGUIPlugin',
category: 'ui'
category: 'ui',
dependencies: ['@esengine/engine-core', '@esengine/sprite']
},
'@esengine/tilemap': {
plugin: true,
pluginExport: 'TilemapPlugin',
category: 'tilemap'
category: 'tilemap',
dependencies: ['@esengine/engine-core', '@esengine/sprite']
},
'@esengine/behavior-tree': {
plugin: true,
pluginExport: 'BehaviorTreePlugin',
category: 'ai'
category: 'ai',
dependencies: ['@esengine/engine-core']
},
'@esengine/physics-rapier2d': {
plugin: true,
pluginExport: 'PhysicsPlugin',
category: 'physics'
category: 'physics',
dependencies: ['@esengine/engine-core']
},
'@esengine/particle': {
plugin: true,
pluginExport: 'ParticlePlugin',
category: 'rendering',
dependencies: ['@esengine/engine-core', '@esengine/sprite']
}
};
/**
* 创建默认项目配置
* @zh 将项目配置转换为 UnifiedPluginLoader 配置
* @en Convert project config to UnifiedPluginLoader config
*
* @param config - @zh 项目配置 @en Project config
* @param packageInfoMap - @zh 包信息映射 @en Package info map
* @returns @zh 插件加载配置列表 @en Plugin load config list
*/
export function convertToPluginLoadConfigs(
config: ProjectConfig,
packageInfoMap: Record<string, ExtendedPluginPackageInfo> = BUILTIN_PLUGIN_PACKAGES
): PluginLoadConfig[] {
const result: PluginLoadConfig[] = [];
for (const [packageId, pluginConfig] of Object.entries(config.plugins)) {
const packageInfo = packageInfoMap[packageId];
if (!packageInfo) {
console.warn(`[ProjectConfig] No package info for ${packageId}, skipping`);
continue;
}
result.push({
packageId,
enabled: pluginConfig.enabled,
sourceType: 'npm',
exportName: packageInfo.pluginExport,
dependencies: packageInfo.dependencies,
options: pluginConfig.options
});
}
return result;
}
/**
* @zh 创建默认项目配置
* @en Create default project config
*/
export function createDefaultProjectConfig(): ProjectConfig {
return {
@@ -69,7 +139,9 @@ export function createDefaultProjectConfig(): ProjectConfig {
'@esengine/camera': { enabled: true },
'@esengine/sprite': { enabled: true },
'@esengine/audio': { enabled: true },
'@esengine/fairygui': { enabled: true },
'@esengine/ui': { enabled: true },
'@esengine/particle': { enabled: false },
'@esengine/fairygui': { enabled: false },
'@esengine/tilemap': { enabled: false },
'@esengine/behavior-tree': { enabled: false },
'@esengine/physics-rapier2d': { enabled: false }

View File

@@ -0,0 +1,190 @@
/**
* @zh 运行时模式枚举
* @en Runtime mode enumeration
*
* @zh 定义游戏运行时的不同运行模式,每种模式有不同的系统启用策略
* @en Defines different runtime modes with different system enabling strategies
*/
export enum RuntimeMode {
/**
* @zh 编辑器静态模式 - 场景编辑状态
* @en Editor static mode - scene editing state
*
* @zh 特性:
* - 所有游戏逻辑系统禁用(物理、行为树、动画)
* - 显示编辑器 UIGrid、Gizmo、坐标轴
* - 组件生命周期回调被延迟onAwake/onStart 不触发)
* - 输入系统禁用(避免与编辑器操作冲突)
*
* @en Features:
* - All game logic systems disabled (physics, behavior tree, animation)
* - Editor UI visible (grid, gizmos, axis indicator)
* - Component lifecycle callbacks deferred (onAwake/onStart not triggered)
* - Input system disabled (avoid conflict with editor operations)
*/
EditorStatic = 'editor-static',
/**
* @zh 编辑器预览模式 - 在编辑器中播放游戏
* @en Editor preview mode - play game within editor
*
* @zh 特性:
* - 游戏逻辑系统启用(物理、行为树、动画)
* - 可选显示 Gizmo用于调试
* - 组件生命周期回调触发
* - 输入系统可选启用
* - 场景快照用于恢复
*
* @en Features:
* - Game logic systems enabled (physics, behavior tree, animation)
* - Gizmos optionally visible (for debugging)
* - Component lifecycle callbacks triggered
* - Input system optionally enabled
* - Scene snapshot for restoration
*/
EditorPreview = 'editor-preview',
/**
* @zh 独立运行模式 - Web/桌面/小程序完整运行
* @en Standalone mode - full Web/desktop/mini-program runtime
*
* @zh 特性:
* - 所有系统启用
* - 无编辑器 UI
* - 完整的输入处理
* - 生产环境配置
*
* @en Features:
* - All systems enabled
* - No editor UI
* - Full input handling
* - Production configuration
*/
Standalone = 'standalone'
}
/**
* @zh 运行模式配置
* @en Runtime mode configuration
*/
export interface RuntimeModeConfig {
/**
* @zh 是否启用物理系统
* @en Whether to enable physics system
*/
enablePhysics: boolean;
/**
* @zh 是否启用行为树系统
* @en Whether to enable behavior tree system
*/
enableBehaviorTree: boolean;
/**
* @zh 是否启用动画系统
* @en Whether to enable animation system
*/
enableAnimation: boolean;
/**
* @zh 是否启用输入系统
* @en Whether to enable input system
*/
enableInput: boolean;
/**
* @zh 是否显示网格
* @en Whether to show grid
*/
showGrid: boolean;
/**
* @zh 是否显示 Gizmo
* @en Whether to show gizmos
*/
showGizmos: boolean;
/**
* @zh 是否显示坐标轴指示器
* @en Whether to show axis indicator
*/
showAxisIndicator: boolean;
/**
* @zh 是否触发组件生命周期回调
* @en Whether to trigger component lifecycle callbacks
*/
triggerLifecycle: boolean;
/**
* @zh 是否为编辑器环境(影响资产加载等)
* @en Whether in editor environment (affects asset loading, etc.)
*/
isEditorEnvironment: boolean;
}
/**
* @zh 获取指定模式的默认配置
* @en Get default configuration for specified mode
*
* @param mode - @zh 运行模式 @en Runtime mode
* @returns @zh 模式配置 @en Mode configuration
*/
export function getRuntimeModeConfig(mode: RuntimeMode): RuntimeModeConfig {
switch (mode) {
case RuntimeMode.EditorStatic:
return {
enablePhysics: false,
enableBehaviorTree: false,
enableAnimation: false,
enableInput: false,
showGrid: true,
showGizmos: true,
showAxisIndicator: true,
triggerLifecycle: false,
isEditorEnvironment: true
};
case RuntimeMode.EditorPreview:
return {
enablePhysics: true,
enableBehaviorTree: true,
enableAnimation: true,
enableInput: true,
showGrid: false,
showGizmos: false, // 预览时默认隐藏,可通过设置开启
showAxisIndicator: false,
triggerLifecycle: true,
isEditorEnvironment: true
};
case RuntimeMode.Standalone:
return {
enablePhysics: true,
enableBehaviorTree: true,
enableAnimation: true,
enableInput: true,
showGrid: false,
showGizmos: false,
showAxisIndicator: false,
triggerLifecycle: true,
isEditorEnvironment: false
};
}
}
/**
* @zh 检查模式是否为编辑器模式
* @en Check if mode is an editor mode
*/
export function isEditorMode(mode: RuntimeMode): boolean {
return mode === RuntimeMode.EditorStatic || mode === RuntimeMode.EditorPreview;
}
/**
* @zh 检查模式是否应启用游戏逻辑
* @en Check if mode should enable game logic
*/
export function shouldEnableGameLogic(mode: RuntimeMode): boolean {
return mode === RuntimeMode.EditorPreview || mode === RuntimeMode.Standalone;
}

View File

@@ -0,0 +1,641 @@
/**
* @zh 用户代码隔离域
* @en User Code Realm
*
* @zh 提供用户代码(组件、系统、服务)与引擎核心的隔离。
* @en Provides isolation between user code (components, systems, services) and engine core.
*
* @zh 设计目标 | Design goals:
* @en
* 1. 隔离注册 - 用户组件/系统不污染引擎核心注册表
* 2. 干净卸载 - 切换项目或热更新时可完全清理用户代码
* 3. 类型安全 - 使用 ServiceToken 提供类型安全的服务访问
* 4. 热更新友好 - 支持组件/系统的原地更新
*/
import type { Component, EntitySystem, IScene } from '@esengine/ecs-framework';
import {
ComponentRegistry,
GlobalComponentRegistry,
PluginServiceRegistry,
createServiceToken,
type ServiceToken
} from '@esengine/ecs-framework';
import { createLogger } from '@esengine/ecs-framework';
const logger = createLogger('UserCodeRealm');
/**
* @zh 用户代码隔离域配置
* @en User Code Realm configuration
*/
export interface UserCodeRealmConfig {
/**
* @zh 是否启用热更新模式
* @en Whether to enable hot reload mode
* @default true
*/
hotReloadEnabled?: boolean;
/**
* @zh 是否在初始化时从全局注册表继承组件
* @en Whether to inherit components from global registry on initialization
* @default true
*/
inheritGlobalComponents?: boolean;
}
/**
* @zh 已注册的用户系统信息
* @en Registered user system info
*/
export interface UserSystemInfo {
/**
* @zh 系统名称
* @en System name
*/
name: string;
/**
* @zh 系统类
* @en System class
*/
systemClass: new (...args: unknown[]) => EntitySystem;
/**
* @zh 系统实例
* @en System instance
*/
instance: EntitySystem;
/**
* @zh 系统所属场景
* @en Scene the system belongs to
*/
scene: IScene;
/**
* @zh 更新顺序
* @en Update order
*/
updateOrder: number;
}
/**
* @zh 已注册的用户组件信息
* @en Registered user component info
*/
export interface UserComponentInfo {
/**
* @zh 组件名称
* @en Component name
*/
name: string;
/**
* @zh 组件类
* @en Component class
*/
componentClass: new (...args: unknown[]) => Component;
/**
* @zh 分配的位索引
* @en Allocated bit index
*/
bitIndex: number;
}
/**
* @zh 用户代码隔离域
* @en User Code Realm
*
* @zh 管理用户定义的组件、系统和服务,提供与引擎核心的隔离。
* @en Manages user-defined components, systems, and services with isolation from engine core.
*
* @example
* ```typescript
* const realm = new UserCodeRealm();
*
* // 注册用户组件 | Register user component
* realm.registerComponent(MyComponent);
*
* // 创建用户系统实例 | Create user system instance
* const system = realm.createSystem(MySystem, scene);
*
* // 注册用户服务 | Register user service
* realm.registerService(MyServiceToken, myServiceInstance);
*
* // 清理所有用户代码 | Clean up all user code
* realm.dispose();
* ```
*/
export class UserCodeRealm {
/**
* @zh 用户组件注册表
* @en User component registry
*/
private _componentRegistry: ComponentRegistry;
/**
* @zh 用户服务注册表
* @en User service registry
*/
private _serviceRegistry: PluginServiceRegistry;
/**
* @zh 已注册的用户系统
* @en Registered user systems
*/
private _systems: UserSystemInfo[] = [];
/**
* @zh 已注册的用户组件信息
* @en Registered user component info
*/
private _components: Map<string, UserComponentInfo> = new Map();
/**
* @zh 配置
* @en Configuration
*/
private _config: Required<UserCodeRealmConfig>;
/**
* @zh 是否已释放
* @en Whether disposed
*/
private _disposed = false;
/**
* @zh 创建用户代码隔离域
* @en Create user code realm
*
* @param config - @zh 配置选项 @en Configuration options
*/
constructor(config?: UserCodeRealmConfig) {
this._config = {
hotReloadEnabled: config?.hotReloadEnabled ?? true,
inheritGlobalComponents: config?.inheritGlobalComponents ?? true
};
this._componentRegistry = new ComponentRegistry();
this._serviceRegistry = new PluginServiceRegistry();
if (this._config.hotReloadEnabled) {
this._componentRegistry.enableHotReload();
}
if (this._config.inheritGlobalComponents) {
this._componentRegistry.cloneFrom(GlobalComponentRegistry);
}
logger.debug('UserCodeRealm created', {
hotReloadEnabled: this._config.hotReloadEnabled,
inheritGlobalComponents: this._config.inheritGlobalComponents
});
}
// ============================================================================
// 组件管理 | Component Management
// ============================================================================
/**
* @zh 注册用户组件类
* @en Register user component class
*
* @param componentClass - @zh 组件类 @en Component class
* @returns @zh 分配的位索引 @en Allocated bit index
*/
registerComponent<T extends Component>(
componentClass: new (...args: unknown[]) => T
): number {
this._ensureNotDisposed();
const name = componentClass.name;
const bitIndex = this._componentRegistry.register(componentClass as any);
// 同时注册到全局注册表(用于序列化/反序列化)
// Also register to global registry (for serialization/deserialization)
try {
GlobalComponentRegistry.register(componentClass as any);
} catch {
// 已注册则忽略 | Ignore if already registered
}
this._components.set(name, {
name,
componentClass,
bitIndex
});
logger.debug(`Component registered: ${name}`, { bitIndex });
return bitIndex;
}
/**
* @zh 注销用户组件
* @en Unregister user component
*
* @param componentName - @zh 组件名称 @en Component name
*/
unregisterComponent(componentName: string): void {
this._ensureNotDisposed();
this._componentRegistry.unregister(componentName);
this._components.delete(componentName);
logger.debug(`Component unregistered: ${componentName}`);
}
/**
* @zh 获取用户组件类
* @en Get user component class
*
* @param componentName - @zh 组件名称 @en Component name
* @returns @zh 组件类或 undefined @en Component class or undefined
*/
getComponent(componentName: string): UserComponentInfo | undefined {
return this._components.get(componentName);
}
/**
* @zh 获取所有已注册的用户组件
* @en Get all registered user components
*/
getAllComponents(): UserComponentInfo[] {
return Array.from(this._components.values());
}
/**
* @zh 获取用户组件注册表
* @en Get user component registry
*/
get componentRegistry(): ComponentRegistry {
return this._componentRegistry;
}
// ============================================================================
// 系统管理 | System Management
// ============================================================================
/**
* @zh 创建并注册用户系统
* @en Create and register user system
*
* @param systemClass - @zh 系统类 @en System class
* @param scene - @zh 目标场景 @en Target scene
* @param updateOrder - @zh 更新顺序 @en Update order
* @returns @zh 创建的系统实例 @en Created system instance
*/
createSystem<T extends EntitySystem>(
systemClass: new (...args: unknown[]) => T,
scene: IScene,
updateOrder = 0
): T {
this._ensureNotDisposed();
const instance = new systemClass();
const name = systemClass.name;
// 设置系统属性 | Set system properties
if ('updateOrder' in instance) {
(instance as any).updateOrder = updateOrder;
}
// 添加到场景 | Add to scene
scene.addSystem(instance);
// 记录系统信息 | Record system info
this._systems.push({
name,
systemClass,
instance,
scene,
updateOrder
});
logger.debug(`System created: ${name}`, { updateOrder });
return instance;
}
/**
* @zh 移除用户系统
* @en Remove user system
*
* @param system - @zh 系统实例 @en System instance
*/
removeSystem(system: EntitySystem): void {
this._ensureNotDisposed();
const index = this._systems.findIndex(s => s.instance === system);
if (index !== -1) {
const info = this._systems[index];
// 从场景移除 | Remove from scene
try {
info.scene.removeSystem(system);
} catch (err) {
logger.warn(`Failed to remove system from scene: ${info.name}`, err);
}
this._systems.splice(index, 1);
logger.debug(`System removed: ${info.name}`);
}
}
/**
* @zh 移除场景的所有用户系统
* @en Remove all user systems from a scene
*
* @param scene - @zh 目标场景 @en Target scene
*/
removeSystemsFromScene(scene: IScene): void {
this._ensureNotDisposed();
const toRemove = this._systems.filter(s => s.scene === scene);
for (const info of toRemove) {
try {
scene.removeSystem(info.instance);
} catch (err) {
logger.warn(`Failed to remove system from scene: ${info.name}`, err);
}
}
this._systems = this._systems.filter(s => s.scene !== scene);
logger.debug(`Removed ${toRemove.length} systems from scene`);
}
/**
* @zh 获取所有用户系统
* @en Get all user systems
*/
getAllSystems(): UserSystemInfo[] {
return [...this._systems];
}
/**
* @zh 获取场景的用户系统
* @en Get user systems of a scene
*
* @param scene - @zh 目标场景 @en Target scene
*/
getSystemsForScene(scene: IScene): UserSystemInfo[] {
return this._systems.filter(s => s.scene === scene);
}
// ============================================================================
// 服务管理 | Service Management
// ============================================================================
/**
* @zh 注册用户服务
* @en Register user service
*
* @param token - @zh 服务令牌 @en Service token
* @param service - @zh 服务实例 @en Service instance
*/
registerService<T>(token: ServiceToken<T>, service: T): void {
this._ensureNotDisposed();
this._serviceRegistry.register(token, service);
logger.debug(`Service registered: ${token.name}`);
}
/**
* @zh 获取用户服务
* @en Get user service
*
* @param token - @zh 服务令牌 @en Service token
* @returns @zh 服务实例或 undefined @en Service instance or undefined
*/
getService<T>(token: ServiceToken<T>): T | undefined {
return this._serviceRegistry.get(token);
}
/**
* @zh 获取用户服务(必需)
* @en Get user service (required)
*
* @param token - @zh 服务令牌 @en Service token
* @throws @zh 如果服务未注册 @en If service not registered
*/
requireService<T>(token: ServiceToken<T>): T {
return this._serviceRegistry.require(token);
}
/**
* @zh 检查服务是否已注册
* @en Check if service is registered
*
* @param token - @zh 服务令牌 @en Service token
*/
hasService<T>(token: ServiceToken<T>): boolean {
return this._serviceRegistry.has(token);
}
/**
* @zh 注销用户服务
* @en Unregister user service
*
* @param token - @zh 服务令牌 @en Service token
*/
unregisterService<T>(token: ServiceToken<T>): boolean {
const result = this._serviceRegistry.unregister(token);
if (result) {
logger.debug(`Service unregistered: ${token.name}`);
}
return result;
}
/**
* @zh 获取用户服务注册表
* @en Get user service registry
*/
get serviceRegistry(): PluginServiceRegistry {
return this._serviceRegistry;
}
// ============================================================================
// 热更新 | Hot Reload
// ============================================================================
/**
* @zh 热更新组件类
* @en Hot reload component class
*
* @zh 更新已注册组件的类定义,保持位索引不变。
* @en Update registered component class definition while keeping bit index unchanged.
*
* @param componentClass - @zh 新的组件类 @en New component class
* @returns @zh 是否成功更新 @en Whether update succeeded
*/
hotReloadComponent<T extends Component>(
componentClass: new (...args: unknown[]) => T
): boolean {
this._ensureNotDisposed();
if (!this._config.hotReloadEnabled) {
logger.warn('Hot reload is disabled');
return false;
}
const name = componentClass.name;
const existing = this._components.get(name);
if (!existing) {
// 新组件,直接注册 | New component, register directly
this.registerComponent(componentClass);
return true;
}
// 复用位索引,更新类引用 | Reuse bit index, update class reference
const bitIndex = this._componentRegistry.register(componentClass as any);
this._components.set(name, {
name,
componentClass,
bitIndex
});
// 更新全局注册表 | Update global registry
try {
GlobalComponentRegistry.register(componentClass as any);
} catch {
// 忽略 | Ignore
}
logger.debug(`Component hot reloaded: ${name}`, { bitIndex });
return true;
}
/**
* @zh 热更新系统
* @en Hot reload systems
*
* @zh 移除旧系统实例,创建新系统实例。
* @en Remove old system instances and create new ones.
*
* @param systemClasses - @zh 新的系统类列表 @en New system class list
* @param scene - @zh 目标场景 @en Target scene
* @returns @zh 新创建的系统实例 @en Newly created system instances
*/
hotReloadSystems<T extends EntitySystem>(
systemClasses: Array<new (...args: unknown[]) => T>,
scene: IScene
): T[] {
this._ensureNotDisposed();
// 移除场景的旧系统 | Remove old systems from scene
this.removeSystemsFromScene(scene);
// 创建新系统 | Create new systems
const newSystems: T[] = [];
for (const systemClass of systemClasses) {
const metadata = (systemClass as any).__systemMetadata__;
const updateOrder = metadata?.updateOrder ?? 0;
const system = this.createSystem(systemClass, scene, updateOrder);
newSystems.push(system);
}
logger.info(`Hot reloaded ${newSystems.length} systems`);
return newSystems;
}
// ============================================================================
// 生命周期 | Lifecycle
// ============================================================================
/**
* @zh 重置隔离域
* @en Reset the realm
*
* @zh 清除所有用户组件、系统和服务,但不释放隔离域。
* @en Clear all user components, systems, and services without disposing the realm.
*/
reset(): void {
this._ensureNotDisposed();
// 移除所有系统 | Remove all systems
for (const info of this._systems) {
try {
info.scene.removeSystem(info.instance);
} catch {
// 忽略错误 | Ignore errors
}
}
this._systems = [];
// 清除组件记录(不重置注册表,保持引擎组件)
// Clear component records (don't reset registry, keep engine components)
this._components.clear();
// 清除服务 | Clear services
this._serviceRegistry.clear();
logger.info('UserCodeRealm reset');
}
/**
* @zh 释放隔离域
* @en Dispose the realm
*
* @zh 完全清理所有资源。释放后隔离域不可再使用。
* @en Completely clean up all resources. Realm cannot be used after disposal.
*/
dispose(): void {
if (this._disposed) {
return;
}
// 移除所有系统 | Remove all systems
for (const info of this._systems) {
try {
info.scene.removeSystem(info.instance);
} catch {
// 忽略错误 | Ignore errors
}
}
this._systems = [];
// 清除组件 | Clear components
this._components.clear();
this._componentRegistry.reset();
// 清除服务 | Clear services
this._serviceRegistry.dispose();
this._disposed = true;
logger.info('UserCodeRealm disposed');
}
/**
* @zh 检查是否已释放
* @en Check if disposed
*/
get isDisposed(): boolean {
return this._disposed;
}
// ============================================================================
// 私有方法 | Private Methods
// ============================================================================
/**
* @zh 确保未释放
* @en Ensure not disposed
*/
private _ensureNotDisposed(): void {
if (this._disposed) {
throw new Error('UserCodeRealm has been disposed');
}
}
}
/**
* @zh 用户代码隔离域服务令牌
* @en User Code Realm service token
*/
export const UserCodeRealmToken = createServiceToken<UserCodeRealm>('userCodeRealm');

View File

@@ -7,6 +7,19 @@ export {
type IRuntimePlugin
} from './PluginManager';
// Plugin Lifecycle State
export {
PluginLifecycleState,
VALID_STATE_TRANSITIONS,
isValidStateTransition,
isPluginOperable,
isPluginLoading,
isPluginAvailable,
type PluginState,
type PluginStateChangeEvent,
type PluginStateChangeListener
} from './PluginState';
export {
createPlugin,
registerPlugin,
@@ -16,15 +29,22 @@ export {
type RuntimeConfig
} from './RuntimeBootstrap';
// Plugin Loader
export {
PluginLoader,
loadPlugin,
loadEnabledPlugins,
registerStaticPlugin,
getLoadedPlugins,
resetPluginLoader,
type PluginLoadState,
type PluginSourceType,
type PluginPackageInfo,
type PluginConfig,
type ProjectPluginConfig
type ProjectPluginConfig,
type PluginLoadConfig,
type PluginLoadInfo,
type PluginLoaderConfig
} from './PluginLoader';
export {
@@ -32,7 +52,9 @@ export {
createDefaultProjectConfig,
mergeProjectConfig,
createProjectConfigFromEnabledList,
type ProjectConfig
convertToPluginLoadConfigs,
type ProjectConfig,
type ExtendedPluginPackageInfo
} from './ProjectConfig';
// Platform Adapter
@@ -52,6 +74,38 @@ export {
type RuntimeState
} from './GameRuntime';
// Runtime Mode
export {
RuntimeMode,
getRuntimeModeConfig,
isEditorMode,
shouldEnableGameLogic,
type RuntimeModeConfig
} from './RuntimeMode';
// User Code Realm
export {
UserCodeRealm,
UserCodeRealmToken,
type UserCodeRealmConfig,
type UserSystemInfo,
type UserComponentInfo
} from './UserCodeRealm';
// ImportMap Generator
export {
generateImportMap,
generateImportMapEntries,
generateImportMapScript,
extractModuleId,
getPackageName,
collectExternalDependencies,
sortModulesByDependencies,
type ImportMapMode,
type ImportMapConfig,
type ImportMapEntry
} from './ImportMapGenerator';
// Platform Adapters
export {
BrowserPlatformAdapter,
@@ -80,16 +134,14 @@ export {
type SceneLoader
} from './services/RuntimeSceneManager';
// Re-export catalog types from asset-system (canonical source)
// 从 asset-system 重新导出目录类型(规范来源)
export type {
IAssetCatalog,
IAssetCatalogEntry,
IAssetBundleInfo,
AssetLoadStrategy
} from '@esengine/asset-system';
// ============================================================================
// 便捷 Re-exports | Convenience Re-exports
// ============================================================================
// 以下是常用类型的便捷 re-export让运行时消费者无需添加额外依赖
// These are convenience re-exports for common types, so runtime consumers
// don't need to add extra dependencies
// Re-export Input System from engine-core for convenience
// 输入系统(运行时常用)| Input System (commonly used in runtime)
export {
Input,
InputManager,
@@ -105,40 +157,48 @@ export {
type TouchEvent
} from '@esengine/engine-core';
// Re-export vector interfaces from math
// 向量接口(运行时常用)| Vector interfaces (commonly used in runtime)
export type { IVector2, IVector3 } from '@esengine/ecs-framework-math';
// Re-export Plugin Service Registry from ecs-framework (canonical source)
// 服务注册基础设施(创建和使用 Token 必需)
// Service registry infrastructure (required for creating and using tokens)
export {
PluginServiceRegistry,
createServiceToken,
type ServiceToken
} from '@esengine/ecs-framework';
// Re-export engine-specific tokens from engine-core
export { TransformTypeToken } from '@esengine/engine-core';
// ============================================================================
// 注意:服务 Token 应从其定义模块导入
// Note: Service tokens should be imported from their defining modules
// ============================================================================
// 以下 Token 已移除,请直接从源模块导入:
// The following tokens have been removed, import from source:
//
// - TransformTypeToken -> @esengine/engine-core
// - RenderSystemToken -> @esengine/ecs-engine-bindgen
// - EngineIntegrationToken -> @esengine/ecs-engine-bindgen
// - TextureServiceToken -> @esengine/ecs-engine-bindgen
// - AssetManagerToken -> @esengine/asset-system
//
// 这遵循 "谁定义接口,谁导出 Token" 原则
// This follows the "whoever defines the interface, exports the token" principle
// Re-export service tokens from their respective modules
// Dependency Utils
export {
RenderSystemToken,
EngineIntegrationToken,
// 新的单一职责服务令牌 | New single-responsibility service tokens
TextureServiceToken,
DynamicAtlasServiceToken,
CoordinateServiceToken,
RenderConfigServiceToken,
// 类型 | Types
type IRenderSystem,
type IRenderDataProvider,
type IEngineIntegration,
type ITextureService,
type IDynamicAtlasService,
type ICoordinateService,
type IRenderConfigService
} from '@esengine/ecs-engine-bindgen';
export {
AssetManagerToken,
type IAssetManager,
type IAssetLoadResult
} from '@esengine/asset-system';
type IDependable,
type TopologicalSortOptions,
type TopologicalSortResult,
type DependencyValidationResult,
// 依赖 ID 解析 | Dependency ID Resolution
resolveDependencyId,
extractShortId,
getPackageName as getPackageNameFromId,
// 拓扑排序 | Topological Sort
topologicalSort,
// 依赖验证 | Dependency Validation
validateDependencies as validateItemDependencies,
getAllDependencies,
getReverseDependencies
} from './utils/DependencyUtils';

View File

@@ -0,0 +1,472 @@
/**
* @zh 依赖管理工具
* @en Dependency Management Utilities
*
* @zh 提供统一的依赖解析、拓扑排序和验证功能
* @en Provides unified dependency resolution, topological sorting, and validation
*
* @zh 设计原则 | Design principles:
* 1. 单一实现 - 所有依赖处理逻辑集中在这里
* 2. 泛型设计 - 支持任何带有 id 和 dependencies 的对象
* 3. 算法可选 - 支持 DFS 和 Kahn 两种排序算法
*/
import { createLogger } from '@esengine/ecs-framework';
const logger = createLogger('DependencyUtils');
// ============================================================================
// 类型定义 | Type Definitions
// ============================================================================
/**
* @zh 可排序的依赖项接口
* @en Interface for sortable dependency items
*/
export interface IDependable {
/**
* @zh 唯一标识符
* @en Unique identifier
*/
id: string;
/**
* @zh 依赖项 ID 列表
* @en List of dependency IDs
*/
dependencies?: string[];
}
/**
* @zh 拓扑排序选项
* @en Topological sort options
*/
export interface TopologicalSortOptions {
/**
* @zh 排序算法
* @en Sorting algorithm
* @default 'kahn'
*/
algorithm?: 'dfs' | 'kahn';
/**
* @zh 是否检测循环依赖
* @en Whether to detect circular dependencies
* @default true
*/
detectCycles?: boolean;
/**
* @zh ID 解析函数(将短 ID 转换为完整 ID
* @en ID resolver function (convert short ID to full ID)
*/
resolveId?: (id: string) => string;
}
/**
* @zh 拓扑排序结果
* @en Topological sort result
*/
export interface TopologicalSortResult<T> {
/**
* @zh 排序后的项目列表
* @en Sorted items list
*/
sorted: T[];
/**
* @zh 是否存在循环依赖
* @en Whether circular dependencies exist
*/
hasCycles: boolean;
/**
* @zh 循环依赖中的项目 ID
* @en IDs of items in circular dependencies
*/
cycleIds?: string[];
}
/**
* @zh 依赖验证结果
* @en Dependency validation result
*/
export interface DependencyValidationResult {
/**
* @zh 是否验证通过
* @en Whether validation passed
*/
valid: boolean;
/**
* @zh 缺失依赖的映射(项目 ID -> 缺失的依赖 ID 列表)
* @en Map of missing dependencies (item ID -> missing dependency IDs)
*/
missingDependencies: Map<string, string[]>;
/**
* @zh 循环依赖的项目 ID
* @en IDs involved in circular dependencies
*/
circularDependencies?: string[];
}
// ============================================================================
// 依赖 ID 解析 | Dependency ID Resolution
// ============================================================================
/**
* @zh 解析依赖 ID短 ID 转完整包名)
* @en Resolve dependency ID (short ID to full package name)
*
* @example
* resolveDependencyId('sprite') // '@esengine/sprite'
* resolveDependencyId('@esengine/sprite') // '@esengine/sprite'
* resolveDependencyId('@dimforge/rapier2d') // '@dimforge/rapier2d'
*/
export function resolveDependencyId(depId: string, scope = '@esengine'): string {
if (depId.startsWith('@')) {
return depId;
}
return `${scope}/${depId}`;
}
/**
* @zh 从完整包名提取短 ID
* @en Extract short ID from full package name
*
* @example
* extractShortId('@esengine/sprite') // 'sprite'
* extractShortId('@esengine/ecs-framework') // 'core' (特殊映射)
*/
export function extractShortId(packageName: string): string {
if (packageName.startsWith('@esengine/')) {
const name = packageName.slice(10);
if (name === 'ecs-framework') return 'core';
if (name === 'ecs-framework-math') return 'math';
return name;
}
const scopeMatch = packageName.match(/^@[^/]+\/(.+)$/);
if (scopeMatch) {
return scopeMatch[1];
}
return packageName;
}
/**
* @zh 从短 ID 获取完整包名
* @en Get full package name from short ID
*
* @example
* getPackageName('core') // '@esengine/ecs-framework'
* getPackageName('sprite') // '@esengine/sprite'
*/
export function getPackageName(shortId: string): string {
if (shortId === 'core') return '@esengine/ecs-framework';
if (shortId === 'math') return '@esengine/ecs-framework-math';
return `@esengine/${shortId}`;
}
// ============================================================================
// 拓扑排序 | Topological Sort
// ============================================================================
/**
* @zh 使用 Kahn 算法进行拓扑排序
* @en Topological sort using Kahn's algorithm
*
* @zh Kahn 算法优势:
* - 能够检测循环依赖
* - 返回所有循环中的节点
* - 时间复杂度 O(V + E)
*/
function kahnSort<T extends IDependable>(
items: T[],
resolveId: (id: string) => string
): TopologicalSortResult<T> {
const itemMap = new Map<string, T>();
const graph = new Map<string, Set<string>>();
const inDegree = new Map<string, number>();
// 构建节点映射
for (const item of items) {
itemMap.set(item.id, item);
graph.set(item.id, new Set());
inDegree.set(item.id, 0);
}
// 构建边(依赖 -> 被依赖者)
for (const item of items) {
for (const dep of item.dependencies || []) {
const depId = resolveId(dep);
if (itemMap.has(depId)) {
graph.get(depId)!.add(item.id);
inDegree.set(item.id, (inDegree.get(item.id) || 0) + 1);
}
}
}
// 收集入度为 0 的节点
const queue: string[] = [];
for (const [id, degree] of inDegree) {
if (degree === 0) {
queue.push(id);
}
}
// BFS 处理
const sorted: T[] = [];
while (queue.length > 0) {
const current = queue.shift()!;
sorted.push(itemMap.get(current)!);
for (const neighbor of graph.get(current) || []) {
const newDegree = (inDegree.get(neighbor) || 0) - 1;
inDegree.set(neighbor, newDegree);
if (newDegree === 0) {
queue.push(neighbor);
}
}
}
// 检查循环依赖
if (sorted.length !== items.length) {
const cycleIds = items
.filter(item => !sorted.includes(item))
.map(item => item.id);
return { sorted, hasCycles: true, cycleIds };
}
return { sorted, hasCycles: false };
}
/**
* @zh 使用 DFS 进行拓扑排序
* @en Topological sort using DFS
*
* @zh DFS 算法特点:
* - 实现简单
* - 递归方式,栈溢出风险(极端情况)
*/
function dfsSort<T extends IDependable>(
items: T[],
resolveId: (id: string) => string
): TopologicalSortResult<T> {
const itemMap = new Map<string, T>();
for (const item of items) {
itemMap.set(item.id, item);
}
const sorted: T[] = [];
const visited = new Set<string>();
const visiting = new Set<string>(); // 用于检测循环
const cycleIds: string[] = [];
const visit = (item: T): boolean => {
if (visited.has(item.id)) return true;
if (visiting.has(item.id)) {
cycleIds.push(item.id);
return false; // 发现循环
}
visiting.add(item.id);
for (const dep of item.dependencies || []) {
const depId = resolveId(dep);
const depItem = itemMap.get(depId);
if (depItem && !visit(depItem)) {
cycleIds.push(item.id);
return false;
}
}
visiting.delete(item.id);
visited.add(item.id);
sorted.push(item);
return true;
};
for (const item of items) {
if (!visited.has(item.id)) {
visit(item);
}
}
return {
sorted,
hasCycles: cycleIds.length > 0,
cycleIds: cycleIds.length > 0 ? [...new Set(cycleIds)] : undefined
};
}
/**
* @zh 拓扑排序(统一入口)
* @en Topological sort (unified entry)
*
* @zh 按依赖关系对项目进行排序,确保被依赖的项目在前
* @en Sort items by dependencies, ensuring dependencies come first
*
* @param items - @zh 待排序的项目列表 @en Items to sort
* @param options - @zh 排序选项 @en Sort options
* @returns @zh 排序结果 @en Sort result
*
* @example
* ```typescript
* const plugins = [
* { id: '@esengine/sprite', dependencies: ['engine-core'] },
* { id: '@esengine/engine-core', dependencies: [] },
* { id: '@esengine/tilemap', dependencies: ['sprite'] }
* ];
*
* const result = topologicalSort(plugins);
* // result.sorted: [engine-core, sprite, tilemap]
* ```
*/
export function topologicalSort<T extends IDependable>(
items: T[],
options: TopologicalSortOptions = {}
): TopologicalSortResult<T> {
const {
algorithm = 'kahn',
detectCycles = true,
resolveId = resolveDependencyId
} = options;
if (items.length === 0) {
return { sorted: [], hasCycles: false };
}
const result = algorithm === 'kahn'
? kahnSort(items, resolveId)
: dfsSort(items, resolveId);
if (result.hasCycles && detectCycles) {
logger.warn(`Circular dependency detected among: ${result.cycleIds?.join(', ')}`);
}
return result;
}
// ============================================================================
// 依赖验证 | Dependency Validation
// ============================================================================
/**
* @zh 验证依赖完整性
* @en Validate dependency completeness
*
* @zh 检查所有启用的项目的依赖是否都已启用
* @en Check if all dependencies of enabled items are also enabled
*
* @param items - @zh 所有项目 @en All items
* @param enabledIds - @zh 已启用的项目 ID 集合 @en Set of enabled item IDs
* @param options - @zh 选项 @en Options
* @returns @zh 验证结果 @en Validation result
*/
export function validateDependencies<T extends IDependable>(
items: T[],
enabledIds: Set<string>,
options: { resolveId?: (id: string) => string } = {}
): DependencyValidationResult {
const { resolveId = resolveDependencyId } = options;
const missingDependencies = new Map<string, string[]>();
for (const item of items) {
if (!enabledIds.has(item.id)) continue;
const missing: string[] = [];
for (const dep of item.dependencies || []) {
const depId = resolveId(dep);
if (!enabledIds.has(depId)) {
missing.push(depId);
}
}
if (missing.length > 0) {
missingDependencies.set(item.id, missing);
}
}
// 检查循环依赖
const enabledItems = items.filter(item => enabledIds.has(item.id));
const sortResult = topologicalSort(enabledItems, { resolveId });
return {
valid: missingDependencies.size === 0 && !sortResult.hasCycles,
missingDependencies,
circularDependencies: sortResult.cycleIds
};
}
/**
* @zh 获取项目的所有依赖(包括传递依赖)
* @en Get all dependencies of an item (including transitive)
*
* @param itemId - @zh 项目 ID @en Item ID
* @param items - @zh 所有项目 @en All items
* @param options - @zh 选项 @en Options
* @returns @zh 所有依赖 ID 的集合 @en Set of all dependency IDs
*/
export function getAllDependencies<T extends IDependable>(
itemId: string,
items: T[],
options: { resolveId?: (id: string) => string } = {}
): Set<string> {
const { resolveId = resolveDependencyId } = options;
const itemMap = new Map<string, T>();
for (const item of items) {
itemMap.set(item.id, item);
}
const allDeps = new Set<string>();
const visited = new Set<string>();
const collect = (id: string) => {
if (visited.has(id)) return;
visited.add(id);
const item = itemMap.get(id);
if (!item) return;
for (const dep of item.dependencies || []) {
const depId = resolveId(dep);
allDeps.add(depId);
collect(depId);
}
};
collect(itemId);
return allDeps;
}
/**
* @zh 获取依赖于指定项目的所有项目(反向依赖)
* @en Get all items that depend on the specified item (reverse dependencies)
*
* @param itemId - @zh 项目 ID @en Item ID
* @param items - @zh 所有项目 @en All items
* @param options - @zh 选项 @en Options
* @returns @zh 所有依赖此项目的 ID 集合 @en Set of IDs that depend on this item
*/
export function getReverseDependencies<T extends IDependable>(
itemId: string,
items: T[],
options: { resolveId?: (id: string) => string } = {}
): Set<string> {
const { resolveId = resolveDependencyId } = options;
const reverseDeps = new Set<string>();
for (const item of items) {
for (const dep of item.dependencies || []) {
const depId = resolveId(dep);
if (depId === itemId) {
reverseDeps.add(item.id);
break;
}
}
}
return reverseDeps;
}

View File

@@ -0,0 +1,22 @@
/**
* @zh 运行时核心工具模块
* @en Runtime Core Utilities
*/
export {
// 类型
type IDependable,
type TopologicalSortOptions,
type TopologicalSortResult,
type DependencyValidationResult,
// 依赖 ID 解析
resolveDependencyId,
extractShortId,
getPackageName,
// 拓扑排序
topologicalSort,
// 依赖验证
validateDependencies,
getAllDependencies,
getReverseDependencies
} from './DependencyUtils';