Files
esengine/packages/editor-app/src-tauri/src/commands/modules.rs
YHH a716d8006c fix(build): 修复 Web 构建组件注册和用户脚本打包问题 (#302)
* refactor(build): 重构 Web 构建管线,支持配置驱动的 Import Maps

- 重构 WebBuildPipeline 支持 split-bundles 和 single-bundle 两种构建模式
- 使用 module.json 的 isCore 字段识别核心模块,消除硬编码列表
- 动态生成 Import Map,从模块清单的 name 字段获取包名映射
- 动态扫描 module.json 文件,不再依赖固定模块列表
- 添加 HTTP 服务器启动脚本 (start-server.bat/sh) 支持 ESM 模块
- 更新 BuildSettingsPanel UI 支持新的构建模式选项
- 添加多语言支持 (zh/en/es)

* fix(build): 修复 Web 构建组件注册和用户脚本打包问题

主要修复:
- 修复组件反序列化时找不到类型的问题
- @ECSComponent 装饰器现在自动注册到 ComponentRegistry
- 添加未使用装饰器的组件警告
- 构建管线自动扫描用户脚本(无需入口文件)

架构改进:
- 解决 Decorators ↔ ComponentRegistry 循环依赖
- 新建 ComponentTypeUtils.ts 作为底层无依赖模块
- 移除冗余的防御性 register 调用
- 统一 ComponentType 定义位置

* refactor(build): 统一 WASM 配置架构,移除硬编码

- 新增 wasmConfig 统一配置替代 wasmPaths/wasmBindings
- wasmConfig.files 支持多候选源路径和明确目标路径
- wasmConfig.runtimePath 指定运行时加载路径
- 重构 _copyWasmFiles 使用统一配置
- HTML 生成使用配置中的 runtimePath
- 移除 physics-rapier2d 的冗余 WASM 配置(由 rapier2d 负责)
- IBuildFileSystem 新增 deleteFile 方法

* feat(build): 单文件构建模式完善和场景配置驱动

## 主要改动

### 单文件构建(single-file mode)
- 修复 WASM 初始化问题,支持 initSync 同步初始化
- 配置驱动的 WASM 识别,通过 wasmConfig.isEngineCore 标识核心引擎模块
- 从 wasmConfig.files 动态获取 JS 绑定路径,消除硬编码

### 场景配置
- 构建验证:必须选择至少一个场景才能构建
- 自动扫描:项目加载时扫描 scenes 目录
- 抽取 _filterScenesByWhitelist 公共方法统一过滤逻辑

### 构建面板优化
- availableScenes prop 传递场景列表
- 场景复选框可点击切换启用状态
- 移除动态 import,使用 prop 传入数据

* chore(build): 补充构建相关的辅助改动

- 添加 BuildFileSystemService 的 listFilesByExtension 优化
- 更新 module.json 添加 externalDependencies 配置
- BrowserRuntime 支持 wasmModule 参数传递
- GameRuntime 添加 loadSceneFromData 方法
- Rust 构建命令更新
- 国际化文案更新

* feat(build): 持久化构建设置到项目配置

## 设计架构

### ProjectService 扩展
- 新增 BuildSettingsConfig 接口定义构建配置字段
- ProjectConfig 添加 buildSettings 字段
- 新增 getBuildSettings / updateBuildSettings 方法

### BuildSettingsPanel
- 组件挂载时从 projectService 加载已保存配置
- 设置变化时自动保存(500ms 防抖)
- 场景选择状态与项目配置同步

### 配置保存位置
保存在项目的 ecs-editor.config.json 中:
- scenes: 选中的场景列表
- buildMode: 构建模式
- companyName/productName/version: 产品信息
- developmentBuild/sourceMap: 构建选项

* fix(editor): Ctrl+S 仅在主编辑区域触发保存场景

- 模态窗口打开时跳过(构建设置、设置、关于等)
- 焦点在 input/textarea/contenteditable 时跳过

* fix(tests): 修复 ECS 测试中 Component 注册问题

- 为所有测试 Component 类添加 @ECSComponent 装饰器
- 移除 beforeEach 中的 ComponentRegistry.reset() 调用
- 将内联 Component 类移到文件顶层以支持装饰器
- 更新测试预期值匹配新的组件类型名称
- 添加缺失的 HierarchyComponent 导入

所有 1388 个测试现已通过。
2025-12-10 18:23:29 +08:00

194 lines
6.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Engine Module Commands
//! 引擎模块命令
//!
//! Commands for reading engine module configurations.
//! 用于读取引擎模块配置的命令。
use std::path::PathBuf;
use tauri::{command, AppHandle};
#[cfg(not(debug_assertions))]
use tauri::Manager;
/// Module index structure.
/// 模块索引结构。
#[derive(serde::Serialize, serde::Deserialize)]
pub struct ModuleIndex {
pub version: String,
#[serde(rename = "generatedAt")]
pub generated_at: String,
pub modules: Vec<ModuleIndexEntry>,
}
/// Module index entry.
/// 模块索引条目。
#[derive(serde::Serialize, serde::Deserialize)]
pub struct ModuleIndexEntry {
pub id: String,
pub name: String,
#[serde(rename = "displayName")]
pub display_name: String,
#[serde(rename = "hasRuntime")]
pub has_runtime: bool,
#[serde(rename = "editorPackage")]
pub editor_package: Option<String>,
#[serde(rename = "isCore")]
pub is_core: bool,
pub category: String,
/// JS bundle size in bytes | JS 包大小(字节)
#[serde(rename = "jsSize")]
pub js_size: Option<u64>,
/// Whether this module requires WASM | 是否需要 WASM
#[serde(rename = "requiresWasm")]
pub requires_wasm: Option<bool>,
/// WASM file size in bytes | WASM 文件大小(字节)
#[serde(rename = "wasmSize")]
pub wasm_size: Option<u64>,
}
/// Get the engine modules directory path.
/// 获取引擎模块目录路径。
///
/// In dev mode: First tries dist/engine, then falls back to packages/ source directory.
/// 在开发模式下:首先尝试 dist/engine然后回退到 packages/ 源目录。
///
/// In production: Uses the bundled resource directory.
/// 在生产模式下:使用打包的资源目录。
#[allow(unused_variables)]
fn get_engine_modules_path(app: &AppHandle) -> Result<PathBuf, String> {
// In development mode, use compile-time path
// 在开发模式下,使用编译时路径
#[cfg(debug_assertions)]
{
// CARGO_MANIFEST_DIR is set at compile time, pointing to src-tauri
// CARGO_MANIFEST_DIR 在编译时设置,指向 src-tauri
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
// Try dist/engine first (if modules have been copied with actual content)
// 首先尝试 dist/engine如果模块已复制且包含实际内容
let dist_engine_path = manifest_dir
.parent()
.map(|p| p.join("dist/engine"))
.unwrap_or_else(|| PathBuf::from("dist/engine"));
// Check if dist/engine has actual module content (not just empty directories)
// 检查 dist/engine 是否有实际模块内容(而不仅是空目录)
let dist_core_output = dist_engine_path.join("core/dist/index.mjs");
if dist_core_output.exists() {
println!("[modules] Using dist/engine path: {:?}", dist_engine_path);
return Ok(dist_engine_path);
}
// Fallback: use packages/ source directory directly (dev mode without copy)
// 回退:直接使用 packages/ 源目录(开发模式无需复制)
// This allows building without running copy-modules first
// 这样可以在不运行 copy-modules 的情况下进行构建
let packages_path = manifest_dir
.parent() // editor-app
.and_then(|p| p.parent()) // packages
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("packages"));
// Verify packages directory has module.json files
// 验证 packages 目录包含 module.json 文件
let core_module = packages_path.join("core/module.json");
if core_module.exists() {
println!("[modules] Using packages source path: {:?}", packages_path);
return Ok(packages_path);
}
return Err(format!(
"Engine modules directory not found in dev mode. Tried: {:?}, {:?}. \
Either run 'pnpm copy-modules' or ensure packages/ directory exists.",
dist_engine_path, packages_path
));
}
// Production: use resource directory
// 生产环境:使用资源目录
#[cfg(not(debug_assertions))]
{
let resource_path = app
.path()
.resource_dir()
.map_err(|e| format!("Failed to get resource dir: {}", e))?;
let prod_path = resource_path.join("engine");
if prod_path.exists() {
return Ok(prod_path);
}
// Fallback: try exe directory
// 回退:尝试可执行文件目录
let exe_path = std::env::current_exe()
.map_err(|e| format!("Failed to get exe path: {}", e))?;
let exe_dir = exe_path.parent()
.ok_or("Failed to get exe directory")?;
let exe_engine_path = exe_dir.join("engine");
if exe_engine_path.exists() {
return Ok(exe_engine_path);
}
Err(format!(
"Engine modules directory not found. Tried: {:?}, {:?}",
prod_path, exe_engine_path
))
}
}
/// Read the engine modules index.
/// 读取引擎模块索引。
#[command]
pub async fn read_engine_modules_index(app: AppHandle) -> Result<ModuleIndex, String> {
println!("[modules] read_engine_modules_index called");
let engine_path = get_engine_modules_path(&app)?;
println!("[modules] engine_path: {:?}", engine_path);
let index_path = engine_path.join("index.json");
if !index_path.exists() {
return Err(format!(
"Module index not found at {:?}. Run 'pnpm copy-modules' first.",
index_path
));
}
let content = std::fs::read_to_string(&index_path)
.map_err(|e| format!("Failed to read index.json: {}", e))?;
serde_json::from_str(&content)
.map_err(|e| format!("Failed to parse index.json: {}", e))
}
/// Read a specific module's manifest.
/// 读取特定模块的清单。
#[command]
pub async fn read_module_manifest(app: AppHandle, module_id: String) -> Result<serde_json::Value, String> {
let engine_path = get_engine_modules_path(&app)?;
let manifest_path = engine_path.join(&module_id).join("module.json");
if !manifest_path.exists() {
return Err(format!(
"Module manifest not found for '{}' at {:?}",
module_id, manifest_path
));
}
let content = std::fs::read_to_string(&manifest_path)
.map_err(|e| format!("Failed to read module.json for {}: {}", module_id, e))?;
serde_json::from_str(&content)
.map_err(|e| format!("Failed to parse module.json for {}: {}", module_id, e))
}
/// Get the base path to engine modules directory.
/// 获取引擎模块目录的基础路径。
#[command]
pub async fn get_engine_modules_base_path(app: AppHandle) -> Result<String, String> {
let path = get_engine_modules_path(&app)?;
path.to_str()
.map(|s| s.to_string())
.ok_or_else(|| "Failed to convert path to string".to_string())
}