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:
@@ -67,6 +67,34 @@ pub struct CompileResult {
|
||||
pub output_path: Option<String>,
|
||||
}
|
||||
|
||||
/// Environment check result.
|
||||
/// 环境检测结果。
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EnvironmentCheckResult {
|
||||
/// Whether all required tools are available | 所有必需工具是否可用
|
||||
pub ready: bool,
|
||||
/// esbuild availability status | esbuild 可用性状态
|
||||
pub esbuild: ToolStatus,
|
||||
}
|
||||
|
||||
/// Tool availability status.
|
||||
/// 工具可用性状态。
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToolStatus {
|
||||
/// Whether the tool is available | 工具是否可用
|
||||
pub available: bool,
|
||||
/// Tool version (if available) | 工具版本(如果可用)
|
||||
pub version: Option<String>,
|
||||
/// Tool path (if available) | 工具路径(如果可用)
|
||||
pub path: Option<String>,
|
||||
/// Source of the tool: "bundled", "local", "global" | 工具来源
|
||||
pub source: Option<String>,
|
||||
/// Error message (if not available) | 错误信息(如果不可用)
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// File change event sent to frontend.
|
||||
/// 发送到前端的文件变更事件。
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
@@ -78,6 +106,82 @@ pub struct FileChangeEvent {
|
||||
pub paths: Vec<String>,
|
||||
}
|
||||
|
||||
/// Check development environment.
|
||||
/// 检测开发环境。
|
||||
///
|
||||
/// Checks if all required tools (esbuild, etc.) are available.
|
||||
/// 检查所有必需的工具是否可用。
|
||||
#[command]
|
||||
pub async fn check_environment() -> Result<EnvironmentCheckResult, String> {
|
||||
let esbuild_status = check_esbuild_status();
|
||||
|
||||
Ok(EnvironmentCheckResult {
|
||||
ready: esbuild_status.available,
|
||||
esbuild: esbuild_status,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check esbuild availability and get its status.
|
||||
/// 检查 esbuild 可用性并获取其状态。
|
||||
fn check_esbuild_status() -> ToolStatus {
|
||||
// Try bundled esbuild first | 首先尝试打包的 esbuild
|
||||
if let Some(bundled_path) = find_bundled_esbuild() {
|
||||
match get_esbuild_version(&bundled_path) {
|
||||
Ok(version) => {
|
||||
return ToolStatus {
|
||||
available: true,
|
||||
version: Some(version),
|
||||
path: Some(bundled_path),
|
||||
source: Some("bundled".to_string()),
|
||||
error: None,
|
||||
};
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[Environment] Bundled esbuild found but failed to get version: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try global esbuild | 尝试全局 esbuild
|
||||
let global_esbuild = if cfg!(windows) { "esbuild.cmd" } else { "esbuild" };
|
||||
match get_esbuild_version(global_esbuild) {
|
||||
Ok(version) => {
|
||||
ToolStatus {
|
||||
available: true,
|
||||
version: Some(version),
|
||||
path: Some(global_esbuild.to_string()),
|
||||
source: Some("global".to_string()),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
ToolStatus {
|
||||
available: false,
|
||||
version: None,
|
||||
path: None,
|
||||
source: None,
|
||||
error: Some("esbuild not found | 未找到 esbuild".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get esbuild version.
|
||||
/// 获取 esbuild 版本。
|
||||
fn get_esbuild_version(esbuild_path: &str) -> Result<String, String> {
|
||||
let output = Command::new(esbuild_path)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to run esbuild: {}", e))?;
|
||||
|
||||
if output.status.success() {
|
||||
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
Ok(version)
|
||||
} else {
|
||||
Err("esbuild --version failed".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Compile TypeScript using esbuild.
|
||||
/// 使用 esbuild 编译 TypeScript。
|
||||
///
|
||||
@@ -254,6 +358,11 @@ pub async fn watch_scripts(
|
||||
|
||||
println!("[UserCode] Started watching: {}", watch_path_clone.display());
|
||||
|
||||
// Debounce state | 防抖状态
|
||||
let mut pending_paths: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
let mut last_event_time = std::time::Instant::now();
|
||||
let debounce_duration = Duration::from_millis(300);
|
||||
|
||||
// Event loop | 事件循环
|
||||
loop {
|
||||
// Check for shutdown | 检查关闭信号
|
||||
@@ -277,19 +386,28 @@ pub async fn watch_scripts(
|
||||
.collect();
|
||||
|
||||
if !ts_paths.is_empty() {
|
||||
let change_type = match event.kind {
|
||||
EventKind::Create(_) => "create",
|
||||
EventKind::Modify(_) => "modify",
|
||||
EventKind::Remove(_) => "remove",
|
||||
// Only handle create/modify/remove events | 只处理创建/修改/删除事件
|
||||
match event.kind {
|
||||
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => {
|
||||
// Add to pending paths and update last event time | 添加到待处理路径并更新最后事件时间
|
||||
for path in ts_paths {
|
||||
pending_paths.insert(path);
|
||||
}
|
||||
last_event_time = std::time::Instant::now();
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
|
||||
// Check if we should emit pending events (debounce) | 检查是否应该发送待处理事件(防抖)
|
||||
if !pending_paths.is_empty() && last_event_time.elapsed() >= debounce_duration {
|
||||
let file_event = FileChangeEvent {
|
||||
change_type: change_type.to_string(),
|
||||
paths: ts_paths,
|
||||
change_type: "modify".to_string(),
|
||||
paths: pending_paths.drain().collect(),
|
||||
};
|
||||
|
||||
println!("[UserCode] File change detected: {:?}", file_event);
|
||||
println!("[UserCode] File change detected (debounced): {:?}", file_event);
|
||||
|
||||
// Emit event to frontend | 向前端发送事件
|
||||
if let Err(e) = app_clone.emit("user-code:file-changed", file_event) {
|
||||
@@ -297,9 +415,6 @@ pub async fn watch_scripts(
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
|
||||
// No events, continue | 无事件,继续
|
||||
}
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
|
||||
println!("[UserCode] Watcher channel disconnected");
|
||||
break;
|
||||
@@ -352,10 +467,21 @@ pub async fn stop_watch_scripts(
|
||||
|
||||
/// Find esbuild executable path.
|
||||
/// 查找 esbuild 可执行文件路径。
|
||||
///
|
||||
/// Search order | 搜索顺序:
|
||||
/// 1. Bundled esbuild in app resources | 应用资源中打包的 esbuild
|
||||
/// 2. Local node_modules | 本地 node_modules
|
||||
/// 3. Global esbuild | 全局 esbuild
|
||||
fn find_esbuild(project_root: &str) -> Result<String, String> {
|
||||
let project_path = Path::new(project_root);
|
||||
|
||||
// Try local node_modules first | 首先尝试本地 node_modules
|
||||
// Try bundled esbuild first (in app resources) | 首先尝试打包的 esbuild(在应用资源中)
|
||||
if let Some(bundled) = find_bundled_esbuild() {
|
||||
println!("[Compiler] Using bundled esbuild: {}", bundled);
|
||||
return Ok(bundled);
|
||||
}
|
||||
|
||||
// Try local node_modules | 尝试本地 node_modules
|
||||
let local_esbuild = if cfg!(windows) {
|
||||
project_path.join("node_modules/.bin/esbuild.cmd")
|
||||
} else {
|
||||
@@ -363,6 +489,7 @@ fn find_esbuild(project_root: &str) -> Result<String, String> {
|
||||
};
|
||||
|
||||
if local_esbuild.exists() {
|
||||
println!("[Compiler] Using local esbuild: {}", local_esbuild.display());
|
||||
return Ok(local_esbuild.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
@@ -375,11 +502,51 @@ fn find_esbuild(project_root: &str) -> Result<String, String> {
|
||||
.output();
|
||||
|
||||
match check {
|
||||
Ok(output) if output.status.success() => Ok(global_esbuild.to_string()),
|
||||
Ok(output) if output.status.success() => {
|
||||
println!("[Compiler] Using global esbuild");
|
||||
Ok(global_esbuild.to_string())
|
||||
},
|
||||
_ => Err("esbuild not found. Please install esbuild: npm install -g esbuild | 未找到 esbuild,请安装: npm install -g esbuild".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Find bundled esbuild in app resources.
|
||||
/// 在应用资源中查找打包的 esbuild。
|
||||
fn find_bundled_esbuild() -> Option<String> {
|
||||
// Get the executable path | 获取可执行文件路径
|
||||
let exe_path = std::env::current_exe().ok()?;
|
||||
let exe_dir = exe_path.parent()?;
|
||||
|
||||
// In development, resources are in src-tauri directory | 开发模式下,资源在 src-tauri 目录
|
||||
// In production, resources are next to the executable | 生产模式下,资源在可执行文件旁边
|
||||
let esbuild_name = if cfg!(windows) { "esbuild.exe" } else { "esbuild" };
|
||||
|
||||
// Try production path (resources next to exe) | 尝试生产路径(资源在 exe 旁边)
|
||||
let prod_path = exe_dir.join("bin").join(esbuild_name);
|
||||
if prod_path.exists() {
|
||||
return Some(prod_path.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
// Try development path (in src-tauri/bin) | 尝试开发路径(在 src-tauri/bin 中)
|
||||
// This handles running via `cargo tauri dev`
|
||||
let dev_path = exe_dir
|
||||
.ancestors()
|
||||
.find_map(|p| {
|
||||
let candidate = p.join("src-tauri").join("bin").join(esbuild_name);
|
||||
if candidate.exists() {
|
||||
Some(candidate)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(path) = dev_path {
|
||||
return Some(path.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Parse esbuild error output.
|
||||
/// 解析 esbuild 错误输出。
|
||||
fn parse_esbuild_errors(stderr: &str) -> Vec<CompileError> {
|
||||
|
||||
@@ -93,6 +93,7 @@ fn main() {
|
||||
commands::compile_typescript,
|
||||
commands::watch_scripts,
|
||||
commands::stop_watch_scripts,
|
||||
commands::check_environment,
|
||||
// Build commands | 构建命令
|
||||
commands::prepare_build_directory,
|
||||
commands::copy_directory,
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"createUpdaterArtifacts": true,
|
||||
"resources": {
|
||||
"runtime/**/*": "."
|
||||
},
|
||||
"resources": [
|
||||
"runtime/**/*",
|
||||
"engine/**/*",
|
||||
"bin/*"
|
||||
],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
|
||||
Reference in New Issue
Block a user