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

@@ -22,10 +22,11 @@ if (!fs.existsSync(bundleDir)) {
}
// Files to bundle
// 需要打包的文件
const filesToBundle = [
{
src: path.join(rootPath, 'packages/platform-web/dist/runtime.browser.js'),
dst: path.join(bundleDir, 'runtime.browser.js')
src: path.join(rootPath, 'packages/platform-web/dist/index.mjs'),
dst: path.join(bundleDir, 'platform-web.mjs')
},
{
src: path.join(rootPath, 'packages/engine/pkg/es_engine_bg.wasm'),
@@ -96,34 +97,101 @@ for (const { src, dst } of typeFilesToBundle) {
}
}
// Update tauri.conf.json to include runtime directory
if (success) {
const tauriConfigPath = path.join(editorPath, 'src-tauri', 'tauri.conf.json');
const config = JSON.parse(fs.readFileSync(tauriConfigPath, 'utf8'));
// Copy engine modules directory from dist/engine to src-tauri/engine
// 复制引擎模块目录从 dist/engine 到 src-tauri/engine
const engineSrcDir = path.join(editorPath, 'dist', 'engine');
const engineDstDir = path.join(editorPath, 'src-tauri', 'engine');
// Add runtime directory to resources
if (!config.bundle) {
config.bundle = {};
}
if (!config.bundle.resources) {
config.bundle.resources = {};
/**
* Recursively copy directory
* 递归复制目录
*/
function copyDirRecursive(src, dst) {
if (!fs.existsSync(src)) {
return false;
}
// Handle both array and object format for resources
if (Array.isArray(config.bundle.resources)) {
if (!config.bundle.resources.includes('runtime/**/*')) {
config.bundle.resources.push('runtime/**/*');
fs.writeFileSync(tauriConfigPath, JSON.stringify(config, null, 2));
console.log('✓ Updated tauri.conf.json with runtime resources');
}
} else if (typeof config.bundle.resources === 'object') {
// Object format - add runtime files if not present
if (!config.bundle.resources['runtime/**/*']) {
config.bundle.resources['runtime/**/*'] = '.';
fs.writeFileSync(tauriConfigPath, JSON.stringify(config, null, 2));
console.log('✓ Updated tauri.conf.json with runtime resources');
if (!fs.existsSync(dst)) {
fs.mkdirSync(dst, { recursive: true });
}
const entries = fs.readdirSync(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const dstPath = path.join(dst, entry.name);
if (entry.isDirectory()) {
copyDirRecursive(srcPath, dstPath);
} else {
fs.copyFileSync(srcPath, dstPath);
}
}
return true;
}
if (fs.existsSync(engineSrcDir)) {
// Remove old engine directory if exists
if (fs.existsSync(engineDstDir)) {
fs.rmSync(engineDstDir, { recursive: true });
}
if (copyDirRecursive(engineSrcDir, engineDstDir)) {
// Count files
let fileCount = 0;
function countFiles(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
countFiles(path.join(dir, entry.name));
} else {
fileCount++;
}
}
}
countFiles(engineDstDir);
console.log(`✓ Copied engine modules directory (${fileCount} files)`);
}
} else {
console.warn(`Engine modules directory not found: ${engineSrcDir}`);
console.log(' Build editor-app first: pnpm --filter @esengine/editor-app build');
}
// Copy esbuild binary for user code compilation
// 复制 esbuild 二进制文件用于用户代码编译
const binDir = path.join(editorPath, 'src-tauri', 'bin');
if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir, { recursive: true });
console.log(`Created bin directory: ${binDir}`);
}
// Platform-specific esbuild binary paths
// 平台特定的 esbuild 二进制路径
const esbuildSources = {
win32: path.join(rootPath, 'node_modules/@esbuild/win32-x64/esbuild.exe'),
darwin: path.join(rootPath, 'node_modules/@esbuild/darwin-x64/bin/esbuild'),
linux: path.join(rootPath, 'node_modules/@esbuild/linux-x64/bin/esbuild'),
};
const platform = process.platform;
const esbuildSrc = esbuildSources[platform];
const esbuildDst = path.join(binDir, platform === 'win32' ? 'esbuild.exe' : 'esbuild');
if (esbuildSrc && fs.existsSync(esbuildSrc)) {
try {
fs.copyFileSync(esbuildSrc, esbuildDst);
// Ensure executable permission on Unix
if (platform !== 'win32') {
fs.chmodSync(esbuildDst, 0o755);
}
const stats = fs.statSync(esbuildDst);
console.log(`✓ Bundled esbuild binary (${(stats.size / 1024 / 1024).toFixed(2)} MB)`);
} catch (error) {
console.warn(`Failed to bundle esbuild: ${error.message}`);
console.log(' User code compilation will require global esbuild installation');
}
} else {
console.warn(`esbuild binary not found for platform ${platform}: ${esbuildSrc}`);
console.log(' User code compilation will require global esbuild installation');
}
if (!success) {