feat(editor): 完善用户代码热更新和环境检测 (#275)

* fix: 更新 bundle-runtime 脚本使用正确的 platform-web 输出文件

原脚本引用的 runtime.browser.js 不存在,改为使用 index.mjs

* feat(editor): 完善用户代码热更新和环境检测

## 热更新改进
- 添加 hotReloadInstances() 方法,通过更新原型链实现真正的热更新
- 组件实例保留数据,仅更新方法
- ComponentRegistry 支持热更新时替换同名组件类

## 环境检测
- 启动时检测 esbuild 可用性
- 在启动页面底部显示环境状态指示器
- 添加 check_environment Rust 命令和前端 API

## esbuild 打包
- 将 esbuild 二进制文件打包到应用中
- 用户无需全局安装 esbuild
- 支持 Windows/macOS/Linux 平台

## 文件监视优化
- 添加 300ms 防抖,避免重复编译
- 修复路径分隔符混合问题

## 资源打包修复
- 修复 Tauri 资源配置,保留 engine 目录结构
- 添加 src-tauri/bin/ 和 src-tauri/engine/ 到 gitignore

* fix: 将热更新模式改为可选,修复测试失败

- ComponentRegistry 添加 hotReloadEnabled 标志,默认禁用
- 只有启用热更新模式时才会替换同名组件类
- 编辑器环境自动启用热更新模式
- reset() 方法中重置热更新标志

* test: 添加热更新模式的测试用例
This commit is contained in:
YHH
2025-12-05 14:24:09 +08:00
committed by GitHub
parent d7454e3ca4
commit 6702f0bfad
12 changed files with 755 additions and 57 deletions

View File

@@ -115,7 +115,9 @@ export class UserCodeService implements IService, IUserCodeService {
const sep = options.projectPath.includes('\\') ? '\\' : '/';
const scriptsDir = `${options.projectPath}${sep}${SCRIPTS_DIR}`;
const outputDir = options.outputDir || `${options.projectPath}${sep}${USER_CODE_OUTPUT_DIR}`;
// Ensure consistent path separators | 确保路径分隔符一致
const userCodeOutputDir = USER_CODE_OUTPUT_DIR.replace(/\//g, sep);
const outputDir = options.outputDir || `${options.projectPath}${sep}${userCodeOutputDir}`;
try {
// Scan scripts first | 先扫描脚本
@@ -379,6 +381,83 @@ export class UserCodeService implements IService, IUserCodeService {
});
}
/**
* Hot reload: update existing component instances to use new class prototype.
* 热更新:更新现有组件实例以使用新类的原型。
*
* This is the core of hot reload - it updates the prototype chain of existing
* instances so they use the new methods from the updated class while preserving
* their data (properties).
* 这是热更新的核心 - 它更新现有实例的原型链,使它们使用更新后类的新方法,
* 同时保留它们的数据(属性)。
*
* @param module - New user code module | 新的用户代码模块
* @returns Number of instances updated | 更新的实例数量
*/
hotReloadInstances(module: UserCodeModule): number {
if (module.target !== UserCodeTarget.Runtime) {
return 0;
}
// Access scene through Core.scene
// 通过 Core.scene 访问场景
const Core = (window as any).__ESENGINE__?.ecsFramework?.Core;
const scene = Core?.scene;
if (!scene || !scene.entities) {
logger.warn('No active scene for hot reload | 没有活动场景用于热更新');
return 0;
}
let updatedCount = 0;
// EntityList.buffer contains all entities
// EntityList.buffer 包含所有实体
const entities: any[] = scene.entities.buffer || [];
for (const entity of entities) {
if (!entity) continue;
// entity.components is a getter that returns readonly Component[]
// entity.components 是一个 getter返回 readonly Component[]
const components = entity.components;
if (!components || !Array.isArray(components)) continue;
for (const component of components) {
if (!component) continue;
// Get the component's type name
// 获取组件的类型名称
const typeName = component.constructor?.name;
if (!typeName) continue;
// Check if we have a new version of this component class
// 检查是否有此组件类的新版本
const newClass = module.exports[typeName];
if (!newClass || typeof newClass !== 'function') continue;
// Check if this is actually a different class (hot reload scenario)
// 检查这是否确实是不同的类(热更新场景)
if (component.constructor === newClass) continue;
// Update the prototype chain to use the new class
// 更新原型链以使用新类
try {
Object.setPrototypeOf(component, newClass.prototype);
updatedCount++;
logger.debug(`Hot reloaded component instance: ${typeName} on entity ${entity.name || entity.id}`);
} catch (err) {
logger.warn(`Failed to hot reload ${typeName}:`, err);
}
}
}
if (updatedCount > 0) {
logger.info(`Hot reload: updated ${updatedCount} component instances | 热更新:更新了 ${updatedCount} 个组件实例`);
}
return updatedCount;
}
/**
* Register editor extensions from user module.
* 从用户模块注册编辑器扩展。