//! User code compilation commands. //! 用户代码编译命令。 //! //! Provides TypeScript compilation using esbuild for user scripts. //! 使用 esbuild 为用户脚本提供 TypeScript 编译。 use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher, Event, EventKind}; use serde::{Deserialize, Serialize}; use std::path::Path; use std::process::Command; use std::sync::mpsc::channel; use std::time::Duration; use tauri::{command, AppHandle, Emitter, State}; use crate::state::ScriptWatcherState; /// Compilation options. /// 编译选项。 #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CompileOptions { /// Entry file path | 入口文件路径 pub entry_path: String, /// Output file path | 输出文件路径 pub output_path: String, /// Output format (esm or iife) | 输出格式 pub format: String, /// Whether to generate source map | 是否生成 source map pub source_map: bool, /// Whether to minify | 是否压缩 pub minify: bool, /// External dependencies | 外部依赖 pub external: Vec, /// Project root for resolving imports | 项目根目录用于解析导入 pub project_root: String, } /// Compilation error. /// 编译错误。 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct CompileError { /// Error message | 错误信息 pub message: String, /// File path | 文件路径 pub file: Option, /// Line number | 行号 pub line: Option, /// Column number | 列号 pub column: Option, } /// Compilation result. /// 编译结果。 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CompileResult { /// Whether compilation succeeded | 是否编译成功 pub success: bool, /// Compilation errors | 编译错误 pub errors: Vec, /// Output file path (if successful) | 输出文件路径(如果成功) pub output_path: Option, } /// File change event sent to frontend. /// 发送到前端的文件变更事件。 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct FileChangeEvent { /// Type of change: "create", "modify", "remove" | 变更类型 pub change_type: String, /// File paths that changed | 发生变更的文件路径 pub paths: Vec, } /// Compile TypeScript using esbuild. /// 使用 esbuild 编译 TypeScript。 /// /// # Arguments | 参数 /// * `options` - Compilation options | 编译选项 /// /// # Returns | 返回 /// Compilation result | 编译结果 #[command] pub async fn compile_typescript(options: CompileOptions) -> Result { // Check if esbuild is available | 检查 esbuild 是否可用 let esbuild_path = find_esbuild(&options.project_root)?; // Build esbuild arguments | 构建 esbuild 参数 let mut args = vec![ options.entry_path.clone(), "--bundle".to_string(), format!("--outfile={}", options.output_path), format!("--format={}", options.format), "--platform=browser".to_string(), "--target=es2020".to_string(), ]; // Add source map option | 添加 source map 选项 if options.source_map { args.push("--sourcemap".to_string()); } // Add minify option | 添加压缩选项 if options.minify { args.push("--minify".to_string()); } // Add external dependencies | 添加外部依赖 for external in &options.external { args.push(format!("--external:{}", external)); } // Run esbuild | 运行 esbuild let output = Command::new(&esbuild_path) .args(&args) .current_dir(&options.project_root) .output() .map_err(|e| format!("Failed to run esbuild | 运行 esbuild 失败: {}", e))?; if output.status.success() { Ok(CompileResult { success: true, errors: vec![], output_path: Some(options.output_path), }) } else { let stderr = String::from_utf8_lossy(&output.stderr); let errors = parse_esbuild_errors(&stderr); Ok(CompileResult { success: false, errors, output_path: None, }) } } /// Watch for file changes in scripts directory. /// 监视脚本目录中的文件变更。 /// /// Emits "user-code:file-changed" events when files change. /// 当文件发生变更时触发 "user-code:file-changed" 事件。 #[command] pub async fn watch_scripts( app: AppHandle, watcher_state: State<'_, ScriptWatcherState>, project_path: String, scripts_dir: String, ) -> Result<(), String> { let watch_path = Path::new(&project_path).join(&scripts_dir); if !watch_path.exists() { return Err(format!( "Scripts directory does not exist | 脚本目录不存在: {}", watch_path.display() )); } // Check if already watching this project | 检查是否已在监视此项目 { let watchers = watcher_state.watchers.lock().await; if watchers.contains_key(&project_path) { println!("[UserCode] Already watching: {}", project_path); return Ok(()); } } // Create a channel for shutdown signal | 创建关闭信号通道 let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel::<()>(); // Clone values for the spawned task | 克隆值以供任务使用 let project_path_clone = project_path.clone(); let watch_path_clone = watch_path.clone(); let app_clone = app.clone(); // Spawn file watcher task | 启动文件监视任务 tokio::spawn(async move { // Create notify watcher | 创建 notify 监视器 let (tx, rx) = channel(); let mut watcher = match RecommendedWatcher::new( move |res: Result| { if let Ok(event) = res { let _ = tx.send(event); } }, Config::default().with_poll_interval(Duration::from_millis(500)), ) { Ok(w) => w, Err(e) => { eprintln!("[UserCode] Failed to create watcher: {}", e); return; } }; // Start watching | 开始监视 if let Err(e) = watcher.watch(&watch_path_clone, RecursiveMode::Recursive) { eprintln!("[UserCode] Failed to watch path: {}", e); return; } println!("[UserCode] Started watching: {}", watch_path_clone.display()); // Event loop | 事件循环 loop { // Check for shutdown | 检查关闭信号 if shutdown_rx.try_recv().is_ok() { println!("[UserCode] Stopping watcher for: {}", project_path_clone); break; } // Check for file events with timeout | 带超时检查文件事件 match rx.recv_timeout(Duration::from_millis(100)) { Ok(event) => { // Filter for TypeScript/JavaScript files | 过滤 TypeScript/JavaScript 文件 let ts_paths: Vec = event .paths .iter() .filter(|p| { let ext = p.extension().and_then(|e| e.to_str()).unwrap_or(""); matches!(ext, "ts" | "tsx" | "js" | "jsx") }) .map(|p| p.to_string_lossy().to_string()) .collect(); if !ts_paths.is_empty() { let change_type = match event.kind { EventKind::Create(_) => "create", EventKind::Modify(_) => "modify", EventKind::Remove(_) => "remove", _ => continue, }; let file_event = FileChangeEvent { change_type: change_type.to_string(), paths: ts_paths, }; println!("[UserCode] File change detected: {:?}", file_event); // Emit event to frontend | 向前端发送事件 if let Err(e) = app_clone.emit("user-code:file-changed", file_event) { eprintln!("[UserCode] Failed to emit event: {}", e); } } } Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { // No events, continue | 无事件,继续 } Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => { println!("[UserCode] Watcher channel disconnected"); break; } } } }); // Store watcher handle | 存储监视器句柄 { let mut watchers = watcher_state.watchers.lock().await; watchers.insert( project_path.clone(), crate::state::WatcherHandle { shutdown_tx }, ); } println!("[UserCode] Watch scripts started for: {}/{}", project_path, scripts_dir); Ok(()) } /// Stop watching for file changes. /// 停止监视文件变更。 #[command] pub async fn stop_watch_scripts( watcher_state: State<'_, ScriptWatcherState>, project_path: Option, ) -> Result<(), String> { let mut watchers = watcher_state.watchers.lock().await; match project_path { Some(path) => { // Stop specific watcher | 停止特定监视器 if let Some(handle) = watchers.remove(&path) { let _ = handle.shutdown_tx.send(()); println!("[UserCode] Stopped watching: {}", path); } } None => { // Stop all watchers | 停止所有监视器 for (path, handle) in watchers.drain() { let _ = handle.shutdown_tx.send(()); println!("[UserCode] Stopped watching: {}", path); } } } Ok(()) } /// Find esbuild executable path. /// 查找 esbuild 可执行文件路径。 fn find_esbuild(project_root: &str) -> Result { let project_path = Path::new(project_root); // Try local node_modules first | 首先尝试本地 node_modules let local_esbuild = if cfg!(windows) { project_path.join("node_modules/.bin/esbuild.cmd") } else { project_path.join("node_modules/.bin/esbuild") }; if local_esbuild.exists() { return Ok(local_esbuild.to_string_lossy().to_string()); } // Try global esbuild | 尝试全局 esbuild let global_esbuild = if cfg!(windows) { "esbuild.cmd" } else { "esbuild" }; // Check if global esbuild exists | 检查全局 esbuild 是否存在 let check = Command::new(global_esbuild) .arg("--version") .output(); match check { Ok(output) if output.status.success() => Ok(global_esbuild.to_string()), _ => Err("esbuild not found. Please install esbuild: npm install -g esbuild | 未找到 esbuild,请安装: npm install -g esbuild".to_string()) } } /// Parse esbuild error output. /// 解析 esbuild 错误输出。 fn parse_esbuild_errors(stderr: &str) -> Vec { let mut errors = Vec::new(); // Simple error parsing - esbuild outputs errors in a specific format // 简单的错误解析 - esbuild 以特定格式输出错误 for line in stderr.lines() { if line.contains("error:") || line.contains("Error:") { // Try to parse file:line:column format | 尝试解析 file:line:column 格式 let parts: Vec<&str> = line.splitn(2, ": ").collect(); if parts.len() == 2 { let location = parts[0]; let message = parts[1].to_string(); // Parse location (file:line:column) | 解析位置 let loc_parts: Vec<&str> = location.split(':').collect(); let (file, line_num, column) = if loc_parts.len() >= 3 { ( Some(loc_parts[0].to_string()), loc_parts[1].parse().ok(), loc_parts[2].parse().ok(), ) } else { (None, None, None) }; errors.push(CompileError { message, file, line: line_num, column, }); } else { errors.push(CompileError { message: line.to_string(), file: None, line: None, column: None, }); } } } // If no specific errors found, add the whole stderr as one error // 如果没有找到特定错误,将整个 stderr 作为一个错误 if errors.is_empty() && !stderr.trim().is_empty() { errors.push(CompileError { message: stderr.to_string(), file: None, line: None, column: None, }); } errors }