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:
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
247
packages/runtime-core/src/ImportMapGenerator.ts
Normal file
247
packages/runtime-core/src/ImportMapGenerator.ts
Normal 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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
189
packages/runtime-core/src/PluginState.ts
Normal file
189
packages/runtime-core/src/PluginState.ts
Normal 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;
|
||||
@@ -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 }
|
||||
|
||||
190
packages/runtime-core/src/RuntimeMode.ts
Normal file
190
packages/runtime-core/src/RuntimeMode.ts
Normal 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 特性:
|
||||
* - 所有游戏逻辑系统禁用(物理、行为树、动画)
|
||||
* - 显示编辑器 UI(Grid、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;
|
||||
}
|
||||
641
packages/runtime-core/src/UserCodeRealm.ts
Normal file
641
packages/runtime-core/src/UserCodeRealm.ts
Normal 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');
|
||||
@@ -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';
|
||||
|
||||
472
packages/runtime-core/src/utils/DependencyUtils.ts
Normal file
472
packages/runtime-core/src/utils/DependencyUtils.ts
Normal 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;
|
||||
}
|
||||
22
packages/runtime-core/src/utils/index.ts
Normal file
22
packages/runtime-core/src/utils/index.ts
Normal 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';
|
||||
Reference in New Issue
Block a user