feat: 添加跨平台运行时、资产系统和UI适配功能 (#256)
* feat(platform-common): 添加WASM加载器和环境检测API * feat(rapier2d): 新增Rapier2D WASM绑定包 * feat(physics-rapier2d): 添加跨平台WASM加载器 * feat(asset-system): 添加运行时资产目录和bundle格式 * feat(asset-system-editor): 新增编辑器资产管理包 * feat(editor-core): 添加构建系统和模块管理 * feat(editor-app): 重构浏览器预览使用import maps * feat(platform-web): 添加BrowserRuntime和资产读取 * feat(engine): 添加材质系统和着色器管理 * feat(material): 新增材质系统和着色器编辑器 * feat(tilemap): 增强tilemap编辑器和动画系统 * feat(modules): 添加module.json配置 * feat(core): 添加module.json和类型定义更新 * chore: 更新依赖和构建配置 * refactor(plugins): 更新插件模板使用ModuleManifest * chore: 添加第三方依赖库 * chore: 移除BehaviourTree-ai和ecs-astar子模块 * docs: 更新README和文档主题样式 * fix: 修复Rust文档测试和添加rapier2d WASM绑定 * fix(tilemap-editor): 修复画布高DPI屏幕分辨率适配问题 * feat(ui): 添加UI屏幕适配系统(CanvasScaler/SafeArea) * fix(ecs-engine-bindgen): 添加缺失的ecs-framework-math依赖 * fix: 添加缺失的包依赖修复CI构建 * fix: 修复CodeQL检测到的代码问题 * fix: 修复构建错误和缺失依赖 * fix: 修复类型检查错误 * fix(material-system): 修复tsconfig配置支持TypeScript项目引用 * fix(editor-core): 修复Rollup构建配置添加tauri external * fix: 修复CodeQL检测到的代码问题 * fix: 修复CodeQL检测到的代码问题
This commit is contained in:
389
packages/editor-app/src-tauri/src/commands/compiler.rs
Normal file
389
packages/editor-app/src-tauri/src/commands/compiler.rs
Normal file
@@ -0,0 +1,389 @@
|
||||
//! 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<String>,
|
||||
/// 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<String>,
|
||||
/// Line number | 行号
|
||||
pub line: Option<u32>,
|
||||
/// Column number | 列号
|
||||
pub column: Option<u32>,
|
||||
}
|
||||
|
||||
/// Compilation result.
|
||||
/// 编译结果。
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CompileResult {
|
||||
/// Whether compilation succeeded | 是否编译成功
|
||||
pub success: bool,
|
||||
/// Compilation errors | 编译错误
|
||||
pub errors: Vec<CompileError>,
|
||||
/// Output file path (if successful) | 输出文件路径(如果成功)
|
||||
pub output_path: Option<String>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
/// Compile TypeScript using esbuild.
|
||||
/// 使用 esbuild 编译 TypeScript。
|
||||
///
|
||||
/// # Arguments | 参数
|
||||
/// * `options` - Compilation options | 编译选项
|
||||
///
|
||||
/// # Returns | 返回
|
||||
/// Compilation result | 编译结果
|
||||
#[command]
|
||||
pub async fn compile_typescript(options: CompileOptions) -> Result<CompileResult, String> {
|
||||
// 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<Event, notify::Error>| {
|
||||
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<String> = 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<String>,
|
||||
) -> 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<String, String> {
|
||||
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<CompileError> {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user